mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 18:04:33 +01:00
Spotify New Link Schema support,Progress Visibility, Failure Icons and more.
This commit is contained in:
parent
d80821f759
commit
9a9b25db27
@ -23,6 +23,8 @@ plugins {
|
||||
id 'androidx.navigation.safeargs.kotlin'
|
||||
id 'dagger.hilt.android.plugin'
|
||||
id 'kotlinx-serialization'
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -83,14 +85,17 @@ android {
|
||||
|
||||
dependencies {
|
||||
//Android
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.10"
|
||||
//noinspection DifferentStdlibGradleVersion
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.webkit:webkit:1.3.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.5"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
|
||||
@ -105,7 +110,6 @@ dependencies {
|
||||
|
||||
//Room: Local SQL-lite Database
|
||||
implementation "androidx.room:room-runtime:2.2.5"
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
|
||||
kapt "androidx.room:room-compiler:2.2.5"
|
||||
implementation "androidx.room:room-ktx:2.2.5"
|
||||
|
||||
@ -131,11 +135,17 @@ dependencies {
|
||||
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
|
||||
implementation 'com.beust:klaxon:5.4'
|
||||
|
||||
//Crashlytics & Analytics
|
||||
implementation platform('com.google.firebase:firebase-bom:26.1.0')
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx'
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
|
||||
//Extras
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
|
||||
implementation 'com.github.javiersantos:AppUpdater:2.7'
|
||||
implementation 'com.github.lzyzsd:circleprogress:1.2.1'
|
||||
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
|
||||
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4'
|
||||
|
||||
|
@ -20,6 +20,12 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.shabinder.spotiflyer">
|
||||
|
||||
<queries>
|
||||
<package android:name="com.gaana" />
|
||||
<package android:name="com.spotify.music" />
|
||||
<package android:name="com.google.android.youtube" />
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@ -28,6 +34,8 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_STORAGE_PERMISSION" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" /><!--For UPI Apps-->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
@ -31,6 +31,7 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import com.github.javiersantos.appupdater.AppUpdater
|
||||
@ -42,7 +43,6 @@ import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
|
||||
import com.shabinder.spotiflyer.utils.NetworkInterceptor
|
||||
import com.shabinder.spotiflyer.utils.createDirectories
|
||||
import com.shabinder.spotiflyer.utils.showMessage
|
||||
import com.shabinder.spotiflyer.utils.startService
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
@ -76,17 +76,15 @@ class MainActivity : AppCompatActivity(){
|
||||
navController = findNavController(R.id.navHostFragment)
|
||||
snackBarAnchor = binding.snackBarPosition
|
||||
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
|
||||
|
||||
//starting Notification and Downloader Service!
|
||||
startService(this)
|
||||
|
||||
authenticateSpotify()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
requestPermission()
|
||||
disableDozeMode()
|
||||
checkIfLatestVersion()
|
||||
createDirectories()
|
||||
|
||||
handleIntentFromExternalActivity()
|
||||
}
|
||||
|
||||
@ -154,9 +152,8 @@ class MainActivity : AppCompatActivity(){
|
||||
sharedViewModel.spotifyService.value = spotifyService
|
||||
}
|
||||
|
||||
|
||||
fun authenticateSpotify() {
|
||||
sharedViewModel.uiScope.launch {
|
||||
sharedViewModel.viewModelScope.launch {
|
||||
Log.i("Spotify Authentication","Started")
|
||||
val token = spotifyServiceTokenRequest.getToken()
|
||||
token.value?.let {
|
||||
@ -210,7 +207,7 @@ class MainActivity : AppCompatActivity(){
|
||||
|
||||
companion object{
|
||||
private lateinit var instance: MainActivity
|
||||
fun getInstance():MainActivity = instance
|
||||
fun getInstance():MainActivity = this.instance
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -22,21 +22,10 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class SharedViewModel @ViewModelInject constructor(
|
||||
val youtubeMusicApi: YoutubeMusicApi
|
||||
) : ViewModel() {
|
||||
var intentString = MutableLiveData<String>()
|
||||
var spotifyService = MutableLiveData<SpotifyService>()
|
||||
|
||||
private var viewModelJob = Job()
|
||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
viewModelJob.cancel()
|
||||
}
|
||||
}
|
@ -18,14 +18,14 @@
|
||||
package com.shabinder.spotiflyer.downloadHelper
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
@ -34,6 +34,8 @@ import com.shabinder.spotiflyer.networking.makeJsonBody
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||
import com.tonyodev.fetch2.Status
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -46,7 +48,6 @@ object DownloadHelper {
|
||||
|
||||
var statusBar:TextView? = null
|
||||
var youtubeMusicApi: YoutubeMusicApi? = null
|
||||
var sharedViewModel: SharedViewModel? = null
|
||||
|
||||
private var total = 0
|
||||
private var processed = 0
|
||||
@ -61,7 +62,7 @@ object DownloadHelper {
|
||||
trackList: List<TrackDetails>) {
|
||||
resetStatusBar()// For New Download Request's Status
|
||||
val downloadList = ArrayList<DownloadObject>()
|
||||
withContext(Dispatchers.Main){
|
||||
withContext(Dispatchers.IO){
|
||||
total += trackList.size // Adding New Download List Count to StatusBar
|
||||
trackList.forEachIndexed { index, it ->
|
||||
if(!isOnline()){
|
||||
@ -71,12 +72,10 @@ object DownloadHelper {
|
||||
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
|
||||
processed++
|
||||
if(index == (trackList.size-1)){//LastElement
|
||||
Handler().postDelayed({
|
||||
Handler(Looper.myLooper()!!).postDelayed({
|
||||
//Delay is Added ,if a request is in processing it may finish
|
||||
Log.i("Spotify Helper","Download Request Sent")
|
||||
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
|
||||
showMessage("Download Started, Now You can leave the App!")
|
||||
}
|
||||
startService(mainActivity,downloadList)
|
||||
},3000)
|
||||
}
|
||||
@ -86,7 +85,6 @@ object DownloadHelper {
|
||||
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
|
||||
object : Callback<String>{
|
||||
override fun onResponse(call: Call<String>, response: Response<String>) {
|
||||
sharedViewModel?.uiScope?.launch {
|
||||
val videoId = sortByBestMatch(
|
||||
getYTTracks(response.body().toString()),
|
||||
trackName = it.title,
|
||||
@ -94,8 +92,14 @@ object DownloadHelper {
|
||||
trackDurationSec = it.durationSec
|
||||
).keys.firstOrNull()
|
||||
Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
|
||||
|
||||
if(videoId.isNullOrBlank()) {notFound++ ; updateStatusBar()}
|
||||
if(videoId.isNullOrBlank()) {
|
||||
//Track Not Found
|
||||
notFound++ ; updateStatusBar()
|
||||
val intent = Intent()
|
||||
.setAction(Status.FAILED.name)
|
||||
.putExtra("track",it)
|
||||
statusBar?.context?.sendBroadcast(intent)
|
||||
}
|
||||
else {//Found Youtube Video ID
|
||||
val outputFile: String =
|
||||
defaultDir +
|
||||
@ -110,20 +114,18 @@ object DownloadHelper {
|
||||
outputFile = outputFile
|
||||
)
|
||||
processed++
|
||||
sharedViewModel?.uiScope?.launch(Dispatchers.Main) {
|
||||
updateStatusBar()
|
||||
}
|
||||
downloadList.add(downloadObject)
|
||||
}
|
||||
if(index == (trackList.size-1)){//LastElement
|
||||
Handler().postDelayed({
|
||||
statusBar?.clearAnimation()
|
||||
if(downloadList.size > 0) {
|
||||
Handler(Looper.myLooper()!!).postDelayed({
|
||||
//Delay is Added ,if a request is in processing it may finish
|
||||
Log.i("Spotify Helper","Download Request Sent")
|
||||
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
|
||||
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
startService(mainActivity,downloadList)
|
||||
},5000)
|
||||
}
|
||||
Log.i("Spotify Helper", "Download Request Sent")
|
||||
showMessage("Download Started, Now You can leave the App!")
|
||||
startService(mainActivity, downloadList)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -157,8 +159,10 @@ object DownloadHelper {
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun updateStatusBar() {
|
||||
private fun updateStatusBar() {
|
||||
CoroutineScope(Dispatchers.Main).launch{
|
||||
statusBar!!.visibility = View.VISIBLE
|
||||
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound"
|
||||
}
|
||||
}
|
||||
}
|
@ -18,20 +18,16 @@
|
||||
package com.shabinder.spotiflyer.downloadHelper
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||
import com.shabinder.spotiflyer.utils.isOnline
|
||||
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||
import com.shabinder.spotiflyer.utils.showNoConnectionAlert
|
||||
import com.shabinder.spotiflyer.utils.startService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
object YTDownloadHelper {
|
||||
interface YTDownloadHelper {
|
||||
suspend fun downloadYTTracks(
|
||||
type:String,
|
||||
subFolder: String?,
|
||||
@ -60,7 +56,7 @@ object YTDownloadHelper {
|
||||
}
|
||||
Log.i("YT Downloader Helper","Download Request Sent")
|
||||
withContext(Dispatchers.Main){
|
||||
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
|
||||
showMessage("Download Started, Now You can leave the App!")
|
||||
startService(mainActivity,downloadList)
|
||||
}
|
||||
}
|
||||
|
@ -42,11 +42,15 @@ data class TrackDetails(
|
||||
var albumArt: File,
|
||||
var albumArtURL: String,
|
||||
var source: Source,
|
||||
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
|
||||
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
||||
var progress: Int = 0
|
||||
):Parcelable
|
||||
|
||||
enum class DownloadStatus{
|
||||
Downloaded,
|
||||
Downloading,
|
||||
NotDownloaded
|
||||
Queued,
|
||||
NotDownloaded,
|
||||
Converting,
|
||||
Failed
|
||||
}
|
@ -19,8 +19,11 @@ package com.shabinder.spotiflyer.networking
|
||||
|
||||
import com.shabinder.spotiflyer.models.Optional
|
||||
import com.shabinder.spotiflyer.models.gaana.*
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.Url
|
||||
|
||||
const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990"
|
||||
|
||||
@ -98,4 +101,9 @@ interface GaanaInterface {
|
||||
@Query("limit") limit: Int = 50
|
||||
): Optional<GaanaArtistTracks>
|
||||
|
||||
/*
|
||||
* Dynamic Url Requests
|
||||
* */
|
||||
@GET
|
||||
fun getResponse(@Url url:String): Call<ResponseBody>
|
||||
}
|
@ -21,6 +21,7 @@ import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@ -31,10 +32,11 @@ import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()) {
|
||||
class TrackListAdapter(private val viewModel : TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()),YTDownloadHelper {
|
||||
|
||||
var source:Source =Source.Spotify
|
||||
|
||||
@ -51,36 +53,69 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{
|
||||
viewModel.uiScope.launch {
|
||||
viewModel.viewModelScope.launch {
|
||||
bindImage(holder.binding.imageUrl,item.albumArtURL, source)
|
||||
}
|
||||
}
|
||||
|
||||
when (item.downloaded) {
|
||||
DownloadStatus.Downloaded -> {
|
||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
|
||||
holder.binding.btnDownload.clearAnimation()
|
||||
holder.binding.btnDownloadProgress.invisible()
|
||||
holder.binding.btnDownload.apply{
|
||||
setImageResource(R.drawable.ic_tick)
|
||||
clearAnimation()
|
||||
visible()
|
||||
}
|
||||
}
|
||||
DownloadStatus.Queued -> {
|
||||
holder.binding.btnDownloadProgress.invisible()
|
||||
holder.binding.btnDownload.apply{
|
||||
setImageResource(R.drawable.ic_refresh)
|
||||
rotate()
|
||||
visible()
|
||||
}
|
||||
}
|
||||
DownloadStatus.Failed -> {
|
||||
holder.binding.btnDownloadProgress.invisible()
|
||||
holder.binding.btnDownload.apply{
|
||||
setImageResource(R.drawable.ic_error)
|
||||
clearAnimation()
|
||||
visible()
|
||||
}
|
||||
}
|
||||
DownloadStatus.Downloading -> {
|
||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
||||
rotateAnim(holder.binding.btnDownload)
|
||||
holder.binding.btnDownload.invisible()
|
||||
holder.binding.btnDownloadProgress.apply {
|
||||
progress = item.progress
|
||||
bottomText = "Downloading"
|
||||
visible()
|
||||
}
|
||||
}
|
||||
DownloadStatus.Converting -> {
|
||||
holder.binding.btnDownload.invisible()
|
||||
holder.binding.btnDownloadProgress.apply {
|
||||
visible()
|
||||
progress = 100
|
||||
bottomText = "Converting"
|
||||
}
|
||||
}
|
||||
DownloadStatus.NotDownloaded -> {
|
||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
|
||||
holder.binding.btnDownload.clearAnimation()
|
||||
holder.binding.btnDownload.setOnClickListener{
|
||||
holder.binding.btnDownloadProgress.invisible()
|
||||
holder.binding.btnDownload.apply{
|
||||
setImageResource(R.drawable.ic_arrow)
|
||||
clearAnimation()
|
||||
visible()
|
||||
setOnClickListener{
|
||||
if(!isOnline()){
|
||||
showNoConnectionAlert()
|
||||
return@setOnClickListener
|
||||
}
|
||||
showMessage("Processing!")
|
||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
||||
rotateAnim(it)
|
||||
item.downloaded = DownloadStatus.Downloading
|
||||
item.downloaded = DownloadStatus.Queued
|
||||
when(source){
|
||||
Source.YouTube -> {
|
||||
viewModel.uiScope.launch {
|
||||
YTDownloadHelper.downloadYTTracks(
|
||||
viewModel.viewModelScope.launch {
|
||||
downloadYTTracks(
|
||||
viewModel.folderType,
|
||||
viewModel.subFolder,
|
||||
listOf(item)
|
||||
@ -88,7 +123,7 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
viewModel.uiScope.launch {
|
||||
viewModel.viewModelScope.launch {
|
||||
DownloadHelper.downloadAllTracks(
|
||||
viewModel.folderType,
|
||||
viewModel.subFolder,
|
||||
@ -101,6 +136,7 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.binding.trackName.text = if(item.title.length > 20){"${item.title.subSequence(0,18)}..."}else{item.title}
|
||||
holder.binding.artist.text = "${item.artists.firstOrNull()}..."
|
||||
|
@ -20,6 +20,7 @@ package com.shabinder.spotiflyer.splash
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
@ -33,7 +34,7 @@ class SplashScreen : AppCompatActivity(){
|
||||
|
||||
val splashTimeout = 400
|
||||
val homeIntent = Intent(this@SplashScreen, MainActivity::class.java)
|
||||
Handler().postDelayed({
|
||||
Handler(Looper.myLooper()!!).postDelayed({
|
||||
//TODO:Bring Initial Setup here
|
||||
startActivity(homeIntent)
|
||||
finish()
|
||||
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Shabinder Singh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.ui.base
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
|
||||
abstract class BaseFragment<VB:ViewBinding,VM : ViewModel> : Fragment() {
|
||||
|
||||
protected val sharedViewModel: SharedViewModel by activityViewModels()
|
||||
protected abstract val binding: VB
|
||||
protected abstract val viewModel: VM
|
||||
protected val viewModelScope by lazy{viewModel.viewModelScope}
|
||||
|
||||
open fun applicationContext(): Context = requireActivity().applicationContext
|
||||
}
|
@ -15,37 +15,35 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.utils
|
||||
package com.shabinder.spotiflyer.ui.base.tracklistbase
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavArgs
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||
import com.shabinder.spotiflyer.ui.base.BaseFragment
|
||||
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||
import com.shabinder.spotiflyer.utils.bindImage
|
||||
import com.shabinder.spotiflyer.utils.isOnline
|
||||
import com.shabinder.spotiflyer.utils.showNoConnectionAlert
|
||||
import com.tonyodev.fetch2.Status
|
||||
|
||||
abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Fragment() {
|
||||
abstract class TrackListFragment<VM : TrackListViewModel, args: NavArgs> : BaseFragment<TrackListFragmentBinding,VM>() {
|
||||
|
||||
protected lateinit var sharedViewModel: SharedViewModel
|
||||
protected lateinit var binding: TrackListFragmentBinding
|
||||
protected abstract var viewModel: VM
|
||||
override lateinit var binding: TrackListFragmentBinding
|
||||
protected abstract var adapter: TrackListAdapter
|
||||
protected abstract var source: Source
|
||||
private var intentFilter: IntentFilter? = null
|
||||
@ -58,8 +56,6 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
|
||||
showNoConnectionAlert()
|
||||
mainActivity.navController.popBackStack()
|
||||
}
|
||||
Handler()
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -74,9 +70,7 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
|
||||
|
||||
private fun initializeAll() {
|
||||
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
|
||||
DownloadHelper.sharedViewModel = sharedViewModel
|
||||
DownloadHelper.statusBar = binding.statusBar
|
||||
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -98,7 +92,7 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
|
||||
})
|
||||
|
||||
viewModel.coverUrl.observe(viewLifecycleOwner, {
|
||||
it?.let{bindImage(binding.coverImage,it, source)}
|
||||
it?.let{ bindImage(binding.coverImage,it, source) }
|
||||
})
|
||||
|
||||
viewModel.title.observe(viewLifecycleOwner, {
|
||||
@ -108,6 +102,11 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
|
||||
|
||||
private fun initializeBroadcast() {
|
||||
intentFilter = IntentFilter()
|
||||
intentFilter?.addAction(Status.QUEUED.name)
|
||||
intentFilter?.addAction(Status.FAILED.name)
|
||||
intentFilter?.addAction(Status.DOWNLOADING.name)
|
||||
intentFilter?.addAction("Progress")
|
||||
intentFilter?.addAction("Converting")
|
||||
intentFilter?.addAction("track_download_completed")
|
||||
|
||||
updateUIReceiver = object : BroadcastReceiver() {
|
||||
@ -117,11 +116,33 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
|
||||
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
|
||||
trackDetails?.let {
|
||||
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
|
||||
Log.i("Track","Download Completed Intent :$position")
|
||||
Log.i("BroadCast Received","$position, ${intent.action} , ${trackDetails.title}")
|
||||
if(position != -1) {
|
||||
val track = viewModel.trackList.value?.get(position)
|
||||
track?.let{
|
||||
when(intent.action){
|
||||
Status.QUEUED.name -> {
|
||||
it.downloaded = DownloadStatus.Queued
|
||||
}
|
||||
Status.FAILED.name -> {
|
||||
it.downloaded = DownloadStatus.Failed
|
||||
}
|
||||
Status.DOWNLOADING.name -> {
|
||||
it.downloaded = DownloadStatus.Downloading
|
||||
}
|
||||
"Progress" -> {
|
||||
//Progress Update
|
||||
it.progress = intent.getIntExtra("progress",0)
|
||||
it.downloaded = DownloadStatus.Downloading
|
||||
}
|
||||
"Converting" -> {
|
||||
//Progress Update
|
||||
it.downloaded = DownloadStatus.Converting
|
||||
}
|
||||
"track_download_completed" -> {
|
||||
it.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
}
|
||||
viewModel.trackList.value?.set(position, it)
|
||||
adapter.notifyItemChanged(position)
|
||||
checkIfAllDownloaded()
|
||||
@ -144,7 +165,7 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
|
||||
requireActivity().unregisterReceiver(updateUIReceiver)
|
||||
}
|
||||
private fun checkIfAllDownloaded() {
|
||||
if(!viewModel.trackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
|
||||
if(!viewModel.trackList.value!!.any { it.downloaded == DownloadStatus.NotDownloaded || it.downloaded == DownloadStatus.Queued || it.downloaded == DownloadStatus.Converting }){
|
||||
//All Tracks Downloaded
|
||||
binding.btnDownloadAll.visibility = View.GONE
|
||||
binding.downloadingFab.apply{
|
||||
@ -154,5 +175,4 @@ abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Frag
|
||||
}
|
||||
}
|
||||
}
|
||||
open fun applicationContext(): Context = requireActivity().applicationContext
|
||||
}
|
@ -15,30 +15,19 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.utils
|
||||
package com.shabinder.spotiflyer.ui.base.tracklistbase
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import kotlinx.coroutines.CompletableJob
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
abstract class TrackListViewModel:ViewModel() {
|
||||
abstract var folderType:String
|
||||
abstract var subFolder:String
|
||||
open val trackList = MutableLiveData<MutableList<TrackDetails>>()
|
||||
|
||||
private val viewModelJob:CompletableJob = Job()
|
||||
open val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||
|
||||
private val loading = "Loading!"
|
||||
open var title = MutableLiveData<String>().apply { value = loading }
|
||||
open var coverUrl = MutableLiveData<String>()
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
viewModelJob.cancel()
|
||||
}
|
||||
}
|
@ -22,21 +22,23 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
|
||||
class GaanaFragment : TrackListFragment<GaanaViewModel, GaanaFragmentArgs>() {
|
||||
|
||||
override lateinit var viewModel: GaanaViewModel
|
||||
override val viewModel: GaanaViewModel by viewModels()
|
||||
override lateinit var adapter: TrackListAdapter
|
||||
override var source: Source = Source.Gaana
|
||||
override val args: GaanaFragmentArgs by navArgs()
|
||||
@ -46,7 +48,6 @@ class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
viewModel = ViewModelProvider(this).get(GaanaViewModel::class.java)
|
||||
adapter = TrackListAdapter(viewModel)
|
||||
|
||||
val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/")
|
||||
@ -70,28 +71,22 @@ class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
|
||||
showNoConnectionAlert()
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.btnDownloadAll.visibility = View.GONE
|
||||
binding.downloadingFab.visibility = View.VISIBLE
|
||||
|
||||
rotateAnim(binding.downloadingFab)
|
||||
binding.btnDownloadAll.gone()
|
||||
binding.downloadingFab.apply{
|
||||
visible()
|
||||
rotate()
|
||||
}
|
||||
for (track in viewModel.trackList.value!!){
|
||||
if(track.downloaded != DownloadStatus.Downloaded){
|
||||
track.downloaded = DownloadStatus.Downloading
|
||||
track.downloaded = DownloadStatus.Queued
|
||||
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
|
||||
}
|
||||
}
|
||||
showMessage("Processing!")
|
||||
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||
val urlList = arrayListOf<String>()
|
||||
viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
|
||||
//Appending Source
|
||||
urlList.add("gaana")
|
||||
loadAllImages(
|
||||
requireActivity(),
|
||||
urlList
|
||||
)
|
||||
sharedViewModel.viewModelScope.launch(Dispatchers.Default){
|
||||
loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.Gaana)
|
||||
}
|
||||
viewModel.uiScope.launch {
|
||||
viewModel.viewModelScope.launch {
|
||||
val finalList = viewModel.trackList.value
|
||||
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
|
||||
DownloadHelper.downloadAllTracks(
|
||||
|
@ -18,6 +18,7 @@
|
||||
package com.shabinder.spotiflyer.ui.gaana
|
||||
|
||||
import androidx.hilt.lifecycle.ViewModelInject
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
@ -25,8 +26,8 @@ import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.models.gaana.GaanaTrack
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.networking.GaanaInterface
|
||||
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
|
||||
import com.shabinder.spotiflyer.utils.Provider
|
||||
import com.shabinder.spotiflyer.utils.TrackListViewModel
|
||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -40,12 +41,13 @@ class GaanaViewModel @ViewModelInject constructor(
|
||||
|
||||
override var folderType:String = ""
|
||||
override var subFolder:String = ""
|
||||
|
||||
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
||||
|
||||
fun gaanaSearch(type:String,link:String){
|
||||
when(type){
|
||||
"song" -> {
|
||||
uiScope.launch {
|
||||
viewModelScope.launch {
|
||||
gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
|
||||
folderType = "Tracks"
|
||||
if(File(finalOutputDir(it.track_title,folderType,subFolder)).exists()){//Download Already Present!!
|
||||
@ -71,7 +73,7 @@ class GaanaViewModel @ViewModelInject constructor(
|
||||
}
|
||||
}
|
||||
"album" -> {
|
||||
uiScope.launch {
|
||||
viewModelScope.launch {
|
||||
gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
|
||||
folderType = "Albums"
|
||||
subFolder = link
|
||||
@ -98,7 +100,7 @@ class GaanaViewModel @ViewModelInject constructor(
|
||||
}
|
||||
}
|
||||
"playlist" -> {
|
||||
uiScope.launch {
|
||||
viewModelScope.launch {
|
||||
gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
|
||||
folderType = "Playlists"
|
||||
subFolder = link
|
||||
@ -126,7 +128,7 @@ class GaanaViewModel @ViewModelInject constructor(
|
||||
}
|
||||
}
|
||||
"artist" -> {
|
||||
uiScope.launch {
|
||||
viewModelScope.launch {
|
||||
folderType = "Artist"
|
||||
subFolder = link
|
||||
val artistDetails = gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()?.also {
|
||||
@ -157,7 +159,6 @@ class GaanaViewModel @ViewModelInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun List<GaanaTrack>.toTrackDetailsList() = this.map {
|
||||
TrackDetails(
|
||||
title = it.track_title,
|
||||
|
@ -23,7 +23,9 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
@ -33,6 +35,7 @@ import com.shabinder.spotiflyer.utils.*
|
||||
import com.shreyaspatil.easyupipayment.EasyUpiPayment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
@ -40,12 +43,11 @@ import javax.inject.Inject
|
||||
@AndroidEntryPoint
|
||||
class MainFragment : Fragment() {
|
||||
|
||||
private lateinit var mainViewModel: MainViewModel
|
||||
private lateinit var sharedViewModel: SharedViewModel
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
private val sharedViewModel: SharedViewModel by activityViewModels()
|
||||
private lateinit var binding: MainFragmentBinding
|
||||
@Inject lateinit var easyUpiPayment: EasyUpiPayment
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
@ -84,21 +86,27 @@ class MainFragment : Fragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
//starting Notification and Downloader Service!
|
||||
startService(requireContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Intent If there is any!
|
||||
**/
|
||||
private fun handleIntent() {
|
||||
sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
|
||||
sharedViewModel.uiScope.launch(Dispatchers.IO) {
|
||||
sharedViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
//Wait for any Authentication to Finish ,
|
||||
// this Wait prevents from multiple Authentication Requests
|
||||
Thread.sleep(1000)
|
||||
delay(1500)
|
||||
if(sharedViewModel.spotifyService.value == null){
|
||||
//Not Authenticated Yet
|
||||
Provider.mainActivity.authenticateSpotify()
|
||||
while (sharedViewModel.spotifyService.value == null) {
|
||||
//Waiting for Authentication to Finish
|
||||
Thread.sleep(1000)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,8 +122,6 @@ class MainFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun initializeAll() {
|
||||
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
binding.apply {
|
||||
btnGaana.openPlatformOnClick("com.gaana","http://gaana.com")
|
||||
btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com")
|
||||
@ -139,4 +145,5 @@ class MainFragment : Fragment() {
|
||||
.append(getText(R.string.d_three)).append("\n")
|
||||
.append(getText(R.string.d_four)).append("\n")
|
||||
}
|
||||
|
||||
}
|
@ -23,12 +23,15 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@ -36,12 +39,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>() {
|
||||
class SpotifyFragment : TrackListFragment<SpotifyViewModel, SpotifyFragmentArgs>() {
|
||||
|
||||
override lateinit var viewModel: SpotifyViewModel
|
||||
override val viewModel: SpotifyViewModel by viewModels()
|
||||
override val args: SpotifyFragmentArgs by navArgs()
|
||||
override lateinit var adapter: TrackListAdapter
|
||||
override var source: Source = Source.Spotify
|
||||
override val args: SpotifyFragmentArgs by navArgs()
|
||||
private val spotifyService:SpotifyService?
|
||||
get() = sharedViewModel.spotifyService.value
|
||||
lateinit var link:String
|
||||
lateinit var type:String
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreateView(
|
||||
@ -51,60 +58,72 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
initializeAll()
|
||||
|
||||
val spotifyLink = args.link.substringAfter("open.spotify.com/")
|
||||
var spotifyLink = "https://" + args.link.substringAfterLast("https://").substringBefore(" ").trim()
|
||||
Log.i("Spotify Fragment Link", spotifyLink)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
||||
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||
/*
|
||||
* New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
|
||||
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&_branch_match_id=862039436205270630
|
||||
* */
|
||||
if (!spotifyLink.contains("open.spotify")) {
|
||||
val resolvedLink = viewModel.resolveLink(spotifyLink)
|
||||
Log.d("Spotify Resolved Link", resolvedLink)
|
||||
spotifyLink = resolvedLink
|
||||
}
|
||||
|
||||
link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||
type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||
|
||||
Log.i("Spotify Fragment", "$type : $link")
|
||||
|
||||
|
||||
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
|
||||
if(isOnline()) mainActivity.authenticateSpotify()
|
||||
if (sharedViewModel.spotifyService.value == null) {//Authentication pending!!
|
||||
if (isOnline()) mainActivity.authenticateSpotify()
|
||||
}
|
||||
|
||||
when{
|
||||
when {
|
||||
type == "Error" || link == "Error" -> {
|
||||
showMessage("Please Check Your Link!")
|
||||
mainActivity.onBackPressed()
|
||||
}
|
||||
|
||||
else -> {
|
||||
if(type == "episode" || type == "show"){//TODO Implementation
|
||||
if (type == "episode" || type == "show") {//TODO Implementation
|
||||
showMessage("Implementing Soon, Stay Tuned!")
|
||||
}
|
||||
else{
|
||||
this.viewModel.spotifySearch(type,link)
|
||||
} else {
|
||||
viewModel.spotifySearch(type, link)
|
||||
|
||||
binding.btnDownloadAll.setOnClickListener {
|
||||
if(!isOnline()){
|
||||
if (!isOnline()) {
|
||||
showNoConnectionAlert()
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.btnDownloadAll.visibility = View.GONE
|
||||
binding.downloadingFab.visibility = View.VISIBLE
|
||||
|
||||
rotateAnim(binding.downloadingFab)
|
||||
for (track in this.viewModel.trackList.value ?: listOf()){
|
||||
if(track.downloaded != DownloadStatus.Downloaded){
|
||||
track.downloaded = DownloadStatus.Downloading
|
||||
adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
|
||||
binding.btnDownloadAll.gone()
|
||||
binding.downloadingFab.apply {
|
||||
visible()
|
||||
rotate()
|
||||
}
|
||||
for (track in viewModel.trackList.value ?: listOf()) {
|
||||
if (track.downloaded != DownloadStatus.Downloaded) {
|
||||
track.downloaded = DownloadStatus.Queued
|
||||
adapter.notifyItemChanged(
|
||||
viewModel.trackList.value!!.indexOf(
|
||||
track
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
showMessage("Processing!")
|
||||
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||
val urlList = arrayListOf<String>()
|
||||
this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
|
||||
//Appending Source
|
||||
urlList.add("spotify")
|
||||
sharedViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||
loadAllImages(
|
||||
requireActivity(),
|
||||
urlList
|
||||
viewModel.trackList.value?.map { it.albumArtURL },
|
||||
Source.Spotify
|
||||
)
|
||||
}
|
||||
this.viewModel.uiScope.launch {
|
||||
viewModelScope.launch {
|
||||
val finalList = viewModel.trackList.value
|
||||
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
|
||||
if (finalList.isNullOrEmpty()) showMessage("Not Downloading Any Song")
|
||||
DownloadHelper.downloadAllTracks(
|
||||
viewModel.folderType,
|
||||
viewModel.subFolder,
|
||||
@ -115,7 +134,7 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ -123,10 +142,10 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
|
||||
* Basic Initialization
|
||||
**/
|
||||
private fun initializeAll() {
|
||||
this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
|
||||
adapter = TrackListAdapter(this.viewModel)
|
||||
sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
|
||||
this.viewModel.spotifyService = it
|
||||
})
|
||||
viewModel.spotifyService = spotifyService //Temp Initialisation
|
||||
adapter = TrackListAdapter(this.viewModel)
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ package com.shabinder.spotiflyer.ui.spotify
|
||||
|
||||
import android.util.Log
|
||||
import androidx.hilt.lifecycle.ViewModelInject
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
@ -27,9 +28,10 @@ import com.shabinder.spotiflyer.models.spotify.Album
|
||||
import com.shabinder.spotiflyer.models.spotify.Image
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.models.spotify.Track
|
||||
import com.shabinder.spotiflyer.networking.GaanaInterface
|
||||
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
|
||||
import com.shabinder.spotiflyer.utils.Provider.imageDir
|
||||
import com.shabinder.spotiflyer.utils.TrackListViewModel
|
||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -38,6 +40,7 @@ import java.io.File
|
||||
|
||||
class SpotifyViewModel @ViewModelInject constructor(
|
||||
val databaseDAO: DatabaseDAO,
|
||||
val gaanaInterface : GaanaInterface
|
||||
) : TrackListViewModel(){
|
||||
|
||||
override var folderType:String = ""
|
||||
@ -45,8 +48,14 @@ class SpotifyViewModel @ViewModelInject constructor(
|
||||
|
||||
var spotifyService : SpotifyService? = null
|
||||
|
||||
fun resolveLink(url:String):String {
|
||||
val response = gaanaInterface.getResponse(url).execute().body()?.string().toString()
|
||||
val regex = """https://open\.spotify\.com.+\w""".toRegex()
|
||||
return regex.find(response)?.value.toString()
|
||||
}
|
||||
|
||||
fun spotifySearch(type:String,link: String){
|
||||
uiScope.launch {
|
||||
viewModelScope.launch {
|
||||
when (type) {
|
||||
"track" -> {
|
||||
spotifyService?.getTrack(link)?.value?.also {
|
||||
@ -130,6 +139,7 @@ class SpotifyViewModel @ViewModelInject constructor(
|
||||
}
|
||||
|
||||
"playlist" -> {
|
||||
Log.i("Spotify Service",spotifyService.toString())
|
||||
val playlistObject = spotifyService?.getPlaylist(link)?.value
|
||||
folderType = "Playlists"
|
||||
subFolder = playlistObject?.name.toString()
|
||||
|
@ -21,12 +21,14 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -36,9 +38,9 @@ private const val sampleDomain2 = "youtu.be"
|
||||
private const val sampleDomain1 = "youtube.com"
|
||||
|
||||
@AndroidEntryPoint
|
||||
class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>() {
|
||||
class YoutubeFragment : TrackListFragment<YoutubeViewModel, YoutubeFragmentArgs>() , YTDownloadHelper {
|
||||
|
||||
override lateinit var viewModel: YoutubeViewModel
|
||||
override val viewModel: YoutubeViewModel by viewModels()
|
||||
override lateinit var adapter : TrackListAdapter
|
||||
override var source: Source = Source.YouTube
|
||||
override val args: YoutubeFragmentArgs by navArgs()
|
||||
@ -48,8 +50,7 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
||||
adapter = TrackListAdapter(this.viewModel)
|
||||
adapter = TrackListAdapter(viewModel)
|
||||
|
||||
val args = YoutubeFragmentArgs.fromBundle(requireArguments())
|
||||
val link = args.link
|
||||
@ -62,7 +63,7 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
|
||||
if(link.contains("playlist",true) || link.contains("list",true)){
|
||||
// Given Link is of a Playlist
|
||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
|
||||
this.viewModel.getYTPlaylist(playlistId)
|
||||
viewModel.getYTPlaylist(playlistId)
|
||||
}else{//Given Link is of a Video
|
||||
var searchId = "error"
|
||||
if(link.contains(sampleDomain1,true) ){
|
||||
@ -84,31 +85,25 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
|
||||
showNoConnectionAlert()
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.btnDownloadAll.visibility = View.GONE
|
||||
binding.downloadingFab.visibility = View.VISIBLE
|
||||
|
||||
rotateAnim(binding.downloadingFab)
|
||||
binding.btnDownloadAll.gone()
|
||||
binding.downloadingFab.apply{
|
||||
visible()
|
||||
rotate()
|
||||
}
|
||||
|
||||
for (track in this.viewModel.trackList.value?: listOf()){
|
||||
if(track.downloaded != DownloadStatus.Downloaded){
|
||||
track.downloaded = DownloadStatus.Downloading
|
||||
adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
|
||||
track.downloaded = DownloadStatus.Queued
|
||||
//adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
showMessage("Processing!")
|
||||
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||
val urlList = arrayListOf<String>()
|
||||
viewModel.trackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
|
||||
.substringBeforeLast(".")}/hqdefault.jpg")}
|
||||
//Appending Source
|
||||
urlList.add("youtube")
|
||||
loadAllImages(
|
||||
requireActivity(),
|
||||
urlList
|
||||
)
|
||||
sharedViewModel.viewModelScope.launch(Dispatchers.Default){
|
||||
loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.YouTube)
|
||||
}
|
||||
viewModel.uiScope.launch {
|
||||
YTDownloadHelper.downloadYTTracks(
|
||||
viewModel.viewModelScope.launch {
|
||||
downloadYTTracks(
|
||||
type = viewModel.folderType,
|
||||
subFolder = viewModel.subFolder,
|
||||
tracks = viewModel.trackList.value ?: listOf()
|
||||
|
@ -20,14 +20,19 @@ package com.shabinder.spotiflyer.ui.youtube
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.hilt.lifecycle.ViewModelInject
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
|
||||
import com.shabinder.spotiflyer.utils.Provider.imageDir
|
||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||
import com.shabinder.spotiflyer.utils.isOnline
|
||||
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||
import com.shabinder.spotiflyer.utils.showMessage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -49,7 +54,7 @@ class YoutubeViewModel @ViewModelInject constructor(
|
||||
fun getYTPlaylist(searchId:String){
|
||||
if(!isOnline())return
|
||||
try{
|
||||
uiScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Log.i("YT Playlist",searchId)
|
||||
val playlist = ytDownloader.getPlaylist(searchId)
|
||||
val playlistDetails = playlist.details()
|
||||
@ -106,7 +111,7 @@ class YoutubeViewModel @ViewModelInject constructor(
|
||||
fun getYTTrack(searchId:String) {
|
||||
if(!isOnline())return
|
||||
try{
|
||||
uiScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Log.i("YT Video",searchId)
|
||||
val video = ytDownloader.getVideo(searchId)
|
||||
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg")
|
||||
|
@ -21,6 +21,9 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.animation.RotateAnimation
|
||||
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||
|
||||
fun View.openPlatformOnClick(packageName:String, websiteAddress:String){
|
||||
@ -43,3 +46,25 @@ fun View.openPlatformOnClick(websiteAddress:String){
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
this.setOnClickListener { mainActivity.startActivity(intent) }
|
||||
}
|
||||
|
||||
fun View.rotate(){
|
||||
val rotate = RotateAnimation(
|
||||
0F, 360F,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
|
||||
)
|
||||
rotate.duration = 2000
|
||||
rotate.repeatCount = Animation.INFINITE
|
||||
rotate.repeatMode = Animation.INFINITE
|
||||
rotate.interpolator = LinearInterpolator()
|
||||
this.animation = rotate
|
||||
}
|
||||
|
||||
fun View.visible(){
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
fun View.gone(){
|
||||
this.visibility = View.GONE
|
||||
}
|
||||
fun View.invisible(){
|
||||
this.visibility = View.INVISIBLE
|
||||
}
|
@ -49,10 +49,9 @@ import javax.inject.Singleton
|
||||
@Module
|
||||
object Provider {
|
||||
|
||||
|
||||
// mainActivity Instance to use whereEver Needed , as Its God Activity.
|
||||
// (i.e, Active Through out App' Lifecycle )
|
||||
val mainActivity: MainActivity = MainActivity.getInstance()
|
||||
// (i.e, Active Throughout App' Lifecycle )
|
||||
val mainActivity: MainActivity by lazy { MainActivity.getInstance() }
|
||||
|
||||
//Default Directory to save Media in their Own Categorized Folders
|
||||
@Suppress("DEPRECATION")// We Do Have Media Access (But Just Media in Media Directory,Not Anything Else)
|
||||
@ -61,12 +60,13 @@ object Provider {
|
||||
"SpotiFlyer"+ File.separator
|
||||
|
||||
//Default Cache Directory to save Album Art to use them for writing in Media Later
|
||||
val imageDir:String
|
||||
get() = mainActivity.externalCacheDir?.absolutePath + File.separator +
|
||||
".Images" + File.separator
|
||||
val imageDir:String by lazy { mainActivity
|
||||
.externalCacheDir?.absolutePath + File.separator +
|
||||
".Images" + File.separator }
|
||||
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
|
||||
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
|
||||
}
|
||||
|
@ -23,10 +23,6 @@ import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.animation.RotateAnimation
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
@ -50,9 +46,9 @@ import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
fun loadAllImages(context: Context?, images:ArrayList<String>? = null ) {
|
||||
fun loadAllImages(context: Context?, images:List<String>? = null,source:Source) {
|
||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||
images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) }
|
||||
images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList<String>) }
|
||||
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
|
||||
}
|
||||
|
||||
@ -114,19 +110,6 @@ fun showMessage(message: String, long: Boolean = false,isSuccess:Boolean = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun rotateAnim(view: View){
|
||||
val rotate = RotateAnimation(
|
||||
0F, 360F,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
|
||||
)
|
||||
rotate.duration = 2000
|
||||
rotate.repeatCount = Animation.INFINITE
|
||||
rotate.repeatMode = Animation.INFINITE
|
||||
rotate.interpolator = LinearInterpolator()
|
||||
view.animation = rotate
|
||||
}
|
||||
|
||||
fun showNoConnectionAlert(){
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
mainActivity.apply {
|
||||
|
@ -25,10 +25,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -48,10 +45,10 @@ import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||
import com.mpatric.mp3agic.ID3v1Tag
|
||||
import com.mpatric.mp3agic.ID3v24Tag
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.utils.Provider
|
||||
import com.shabinder.spotiflyer.utils.Provider.imageDir
|
||||
import com.shabinder.spotiflyer.utils.copyTo
|
||||
@ -80,45 +77,38 @@ class ForegroundService : Service(){
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var isServiceStarted = false
|
||||
var notificationLine = 0
|
||||
val messageList = mutableListOf("","","","")
|
||||
private var pendingIntent:PendingIntent? = null
|
||||
var messageList = mutableListOf("", "", "", "")
|
||||
private var cancelIntent:PendingIntent? = null
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
override fun onBind(intent: Intent): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||
pendingIntent = PendingIntent.getActivity(
|
||||
val intent = Intent(
|
||||
this,
|
||||
0, notificationIntent, 0
|
||||
)
|
||||
ForegroundService::class.java
|
||||
).apply{action = "kill"}
|
||||
cancelIntent = PendingIntent.getService (this, 0 , intent , PendingIntent.FLAG_CANCEL_CURRENT )
|
||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
ytDownloader = YoutubeDownloader()
|
||||
val fetchConfiguration =
|
||||
FetchConfiguration.Builder(this)
|
||||
.setDownloadConcurrentLimit(4)
|
||||
.build()
|
||||
|
||||
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||
|
||||
fetch = Fetch.getDefaultInstance()
|
||||
fetch.addListener(fetchListener)
|
||||
//clearing all not completed Downloads
|
||||
//Starting fresh
|
||||
fetch.removeAll()
|
||||
|
||||
initialiseFetch()
|
||||
startForeground()
|
||||
}
|
||||
|
||||
@SuppressLint("WakelockTimeout")
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
// Send a notification that service is started
|
||||
Log.i(tag,"Service Started.")
|
||||
Log.i(tag, "Service Started.")
|
||||
startForeground()
|
||||
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
|
||||
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList"))
|
||||
|
||||
if(intent.action == "kill") killService()
|
||||
|
||||
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList(
|
||||
"object"
|
||||
))
|
||||
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList(
|
||||
"imagesList"
|
||||
))
|
||||
|
||||
imagesList?.let{
|
||||
serviceScope.launch {
|
||||
@ -137,7 +127,7 @@ class ForegroundService : Service(){
|
||||
//Service Already Started
|
||||
START_STICKY
|
||||
} else{
|
||||
Log.i(tag,"Starting the foreground service task")
|
||||
Log.i(tag, "Starting the foreground service task")
|
||||
isServiceStarted = true
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
@ -171,20 +161,19 @@ class ForegroundService : Service(){
|
||||
format?.let {
|
||||
val url: String = format.url()
|
||||
Log.i("DHelper Link Found", url)
|
||||
serviceScope.launch {
|
||||
val request= Request(url, downloadObj.outputFile)
|
||||
request.priority = Priority.NORMAL
|
||||
request.networkType = NetworkType.ALL
|
||||
|
||||
val request= Request(url, downloadObj.outputFile).apply{
|
||||
priority = Priority.NORMAL
|
||||
networkType = NetworkType.ALL
|
||||
}
|
||||
fetch.enqueue(request,
|
||||
{
|
||||
requestMap[it] = downloadObj.trackDetails
|
||||
Log.i(tag, "Enqueuing Download")
|
||||
},
|
||||
{
|
||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
||||
)
|
||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
||||
Log.i("Service YT Error", e.message.toString())
|
||||
@ -196,25 +185,16 @@ class ForegroundService : Service(){
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if(converted == total){
|
||||
Handler().postDelayed({
|
||||
Log.i(tag,"Service destroyed.")
|
||||
cleanFiles(File(defaultDir))
|
||||
releaseWakeLock()
|
||||
stopForeground(true)
|
||||
},2000)
|
||||
Handler(Looper.myLooper()!!).postDelayed({
|
||||
killService()
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
if(converted == total ){
|
||||
Log.i(tag,"Service Removed.")
|
||||
cleanFiles(File(defaultDir))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stopForeground(true)
|
||||
} else {
|
||||
stopSelf()//System will automatically close it
|
||||
}
|
||||
killService()
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,7 +207,11 @@ class ForegroundService : Service(){
|
||||
download: Download,
|
||||
waitingOnNetwork: Boolean
|
||||
) {
|
||||
// TODO("Not yet implemented")
|
||||
//Notify Download Completed
|
||||
val intent = Intent()
|
||||
.setAction(Status.QUEUED.name)
|
||||
.putExtra("track", requestMap[download.request])
|
||||
this@ForegroundService.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
override fun onRemoved(download: Download) {
|
||||
@ -253,7 +237,7 @@ class ForegroundService : Service(){
|
||||
messageList[1] = "Downloading ${track?.title}"
|
||||
notificationLine = 2
|
||||
}
|
||||
2-> {
|
||||
2 -> {
|
||||
messageList[2] = "Downloading ${track?.title}"
|
||||
notificationLine = 3
|
||||
}
|
||||
@ -262,8 +246,12 @@ class ForegroundService : Service(){
|
||||
notificationLine = 0
|
||||
}
|
||||
}
|
||||
Log.i(tag,"${track?.title} Download Started")
|
||||
Log.i(tag, "${track?.title} Download Started")
|
||||
updateNotification()
|
||||
val intent = Intent()
|
||||
.setAction(Status.DOWNLOADING.name)
|
||||
.putExtra("track", requestMap[download.request])
|
||||
this@ForegroundService.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
override fun onWaitingNetwork(download: Download) {
|
||||
@ -290,12 +278,13 @@ class ForegroundService : Service(){
|
||||
serviceScope.launch {
|
||||
try{
|
||||
track?.let { convertToMp3(download.file, it) }
|
||||
Log.i(tag,"${track?.title} Download Completed")
|
||||
}catch (e:KotlinNullPointerException
|
||||
Log.i(tag, "${track?.title} Download Completed")
|
||||
}catch (
|
||||
e: KotlinNullPointerException
|
||||
){
|
||||
Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!")
|
||||
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||
Log.i(tag, "${track?.title} Download Failed! Error:Fetch!!!!")
|
||||
Log.i(tag, "${track?.title} Requesting Download thru Android DM")
|
||||
downloadUsingDM(download.request.url, download.request.file, track!!)
|
||||
downloaded++
|
||||
requestMap.remove(download.request)
|
||||
}
|
||||
@ -318,9 +307,9 @@ class ForegroundService : Service(){
|
||||
serviceScope.launch {
|
||||
val track = requestMap[download.request]
|
||||
downloaded++
|
||||
Log.i(tag,download.error.throwable.toString())
|
||||
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||
Log.i(tag, download.error.throwable.toString())
|
||||
Log.i(tag, "${track?.title} Requesting Download thru Android DM")
|
||||
downloadUsingDM(download.request.url, download.request.file, track!!)
|
||||
requestMap.remove(download.request)
|
||||
}
|
||||
updateNotification()
|
||||
@ -336,16 +325,20 @@ class ForegroundService : Service(){
|
||||
downloadedBytesPerSecond: Long
|
||||
) {
|
||||
val track = requestMap[download.request]
|
||||
Log.i(tag,"${track?.title} ETA: ${etaInMilliSeconds/1000} sec")
|
||||
Log.i(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec")
|
||||
val intent = Intent()
|
||||
.setAction("Progress")
|
||||
.putExtra("progress", download.progress)
|
||||
.putExtra("track", requestMap[download.request])
|
||||
this@ForegroundService.sendBroadcast(intent)
|
||||
// updateNotification()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If fetch Fails , Android Download Manager To RESCUE!!
|
||||
**/
|
||||
fun downloadUsingDM(url:String, outputDir:String, track: TrackDetails){
|
||||
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
|
||||
val uri = Uri.parse(url)
|
||||
val request = DownloadManager.Request(uri)
|
||||
.setAllowedNetworkTypes(
|
||||
@ -367,20 +360,25 @@ class ForegroundService : Service(){
|
||||
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) {
|
||||
convertToMp3(outputDir,track)
|
||||
convertToMp3(outputDir, track)
|
||||
converted++
|
||||
//Unregister this broadcast Receiver
|
||||
this@ForegroundService.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
registerReceiver(onDownloadComplete,IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||
registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||
}
|
||||
|
||||
/**
|
||||
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
|
||||
**/
|
||||
fun convertToMp3(filePath: String, track: TrackDetails){
|
||||
val intent = Intent()
|
||||
.setAction("Converting")
|
||||
.putExtra("track", track)
|
||||
this@ForegroundService.sendBroadcast(intent)
|
||||
|
||||
val m4aFile = File(filePath)
|
||||
|
||||
FFmpeg.executeAsync(
|
||||
@ -390,29 +388,34 @@ class ForegroundService : Service(){
|
||||
RETURN_CODE_SUCCESS -> {
|
||||
Log.i(Config.TAG, "Async command execution completed successfully.")
|
||||
m4aFile.delete()
|
||||
writeMp3Tags(filePath.substringBeforeLast('.')+".mp3",track)
|
||||
writeMp3Tags(filePath.substringBeforeLast('.') + ".mp3", track)
|
||||
//FFMPEG task Completed
|
||||
}
|
||||
RETURN_CODE_CANCEL -> {
|
||||
Log.i(Config.TAG, "Async command execution cancelled by user.")
|
||||
}
|
||||
else -> {
|
||||
Log.i(Config.TAG, String.format("Async command execution failed with rc=%d.", returnCode))
|
||||
Log.i(
|
||||
Config.TAG, String.format(
|
||||
"Async command execution failed with rc=%d.",
|
||||
returnCode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeMp3Tags(filePath:String, track: TrackDetails){
|
||||
private fun writeMp3Tags(filePath: String, track: TrackDetails){
|
||||
var mp3File = Mp3File(filePath)
|
||||
mp3File = removeAllTags(mp3File)
|
||||
mp3File = setId3v1Tags(mp3File,track)
|
||||
mp3File = setId3v2Tags(mp3File,track)
|
||||
Log.i("Mp3Tags","saving file")
|
||||
mp3File.save(filePath.substringBeforeLast('.')+".new.mp3")
|
||||
mp3File = setId3v1Tags(mp3File, track)
|
||||
mp3File = setId3v2Tags(mp3File, track)
|
||||
Log.i("Mp3Tags", "saving file")
|
||||
mp3File.save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||
val file = File(filePath)
|
||||
file.delete()
|
||||
val newFile = File((filePath.substringBeforeLast('.')+".new.mp3"))
|
||||
val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
|
||||
newFile.renameTo(file)
|
||||
converted++
|
||||
updateNotification()
|
||||
@ -420,7 +423,7 @@ class ForegroundService : Service(){
|
||||
//Notify Download Completed
|
||||
val intent = Intent()
|
||||
.setAction("track_download_completed")
|
||||
.putExtra("track",track)
|
||||
.putExtra("track", track)
|
||||
this@ForegroundService.sendBroadcast(intent)
|
||||
|
||||
//All tasks completed (REST IN PEACE)
|
||||
@ -439,13 +442,15 @@ class ForegroundService : Service(){
|
||||
.setSmallIcon(R.drawable.down_arrowbw)
|
||||
.setSubText("Total: $total Completed:$converted")
|
||||
.setNotificationSilent()
|
||||
.setStyle(NotificationCompat.InboxStyle()
|
||||
.setStyle(
|
||||
NotificationCompat.InboxStyle()
|
||||
// .setBigContentTitle("Speed: $speed KB/s")
|
||||
.addLine(messageList[0])
|
||||
.addLine(messageList[1])
|
||||
.addLine(messageList[2])
|
||||
.addLine(messageList[3]))
|
||||
.setContentIntent(pendingIntent)
|
||||
.addLine(messageList[3])
|
||||
)
|
||||
.addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
|
||||
.build()
|
||||
mNotificationManager.notify(notificationId, notification)
|
||||
}
|
||||
@ -479,9 +484,9 @@ class ForegroundService : Service(){
|
||||
val fis = FileInputStream(track.albumArt)
|
||||
fis.read(bytesArray) //read file into bytes[]
|
||||
fis.close()
|
||||
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
||||
}catch (e:java.io.FileNotFoundException){
|
||||
Log.i("Error","Couldn't Write Mp3 Album Art")
|
||||
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||
}catch (e: java.io.FileNotFoundException){
|
||||
Log.i("Error", "Couldn't Write Mp3 Album Art")
|
||||
}
|
||||
mp3file.id3v2Tag = id3v2Tag
|
||||
return mp3file
|
||||
@ -501,7 +506,7 @@ class ForegroundService : Service(){
|
||||
}
|
||||
|
||||
private fun releaseWakeLock() {
|
||||
Log.i(tag,"Releasing Wake Lock")
|
||||
Log.i(tag, "Releasing Wake Lock")
|
||||
try {
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
@ -509,7 +514,7 @@ class ForegroundService : Service(){
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.i(tag,"Service stopped without being started: ${e.message}")
|
||||
Log.i(tag, "Service stopped without being started: ${e.message}")
|
||||
}
|
||||
isServiceStarted = false
|
||||
}
|
||||
@ -531,13 +536,15 @@ class ForegroundService : Service(){
|
||||
.setSmallIcon(R.drawable.down_arrowbw)
|
||||
.setNotificationSilent()
|
||||
.setSubText("Total: $total Completed:$converted")
|
||||
.setStyle(NotificationCompat.InboxStyle()
|
||||
.setStyle(
|
||||
NotificationCompat.InboxStyle()
|
||||
// .setBigContentTitle("Speed: $speed KB/s")
|
||||
.addLine(messageList[0])
|
||||
.addLine(messageList[1])
|
||||
.addLine(messageList[2])
|
||||
.addLine(messageList[3]))
|
||||
.setContentIntent(pendingIntent)
|
||||
.addLine(messageList[3])
|
||||
)
|
||||
.addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
|
||||
.build()
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
@ -545,8 +552,10 @@ class ForegroundService : Service(){
|
||||
@Suppress("SameParameterValue")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(channelId: String, channelName: String): String{
|
||||
val chan = NotificationChannel(channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
val chan = NotificationChannel(
|
||||
channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
service.createNotificationChannel(chan)
|
||||
@ -556,8 +565,8 @@ class ForegroundService : Service(){
|
||||
/**
|
||||
* Cleaning All Residual Files except Mp3 Files
|
||||
**/
|
||||
private fun cleanFiles(dir:File) {
|
||||
Log.i(tag,"Starting Cleaning in ${dir.path} ")
|
||||
private fun cleanFiles(dir: File) {
|
||||
Log.i(tag, "Starting Cleaning in ${dir.path} ")
|
||||
val fList = dir.listFiles()
|
||||
fList?.let {
|
||||
for (file in fList) {
|
||||
@ -565,7 +574,7 @@ class ForegroundService : Service(){
|
||||
cleanFiles(file)
|
||||
} else if(file.isFile) {
|
||||
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
||||
Log.i(tag,"Cleaning ${file.path}")
|
||||
Log.i(tag, "Cleaning ${file.path}")
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
@ -581,20 +590,20 @@ class ForegroundService : Service(){
|
||||
* Last Element of this List defines Its Source
|
||||
* */
|
||||
val source = urlList.last()
|
||||
for (url in urlList.subList(0,urlList.size-2)) {
|
||||
for (url in urlList.subList(0, urlList.size - 2)) {
|
||||
val imgUri = url.toUri().buildUpon().scheme("https").build()
|
||||
Glide
|
||||
.with(this)
|
||||
.asFile()
|
||||
.load(imgUri)
|
||||
.listener(object: RequestListener<File> {
|
||||
.listener(object : RequestListener<File> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<File>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
Log.i("Glide","LoadFailed")
|
||||
Log.i("Glide", "LoadFailed")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -606,20 +615,35 @@ class ForegroundService : Service(){
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
serviceScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val file = when(source){
|
||||
"spotify" ->{
|
||||
val file = when (source) {
|
||||
Source.Spotify.name -> {
|
||||
File(imageDir, url.substringAfterLast('/') + ".jpeg")
|
||||
}
|
||||
"youtube" ->{
|
||||
File(imageDir, url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg")
|
||||
Source.YouTube.name -> {
|
||||
File(
|
||||
imageDir,
|
||||
url.substringBeforeLast('/', url)
|
||||
.substringAfterLast(
|
||||
'/',
|
||||
url
|
||||
) + ".jpeg"
|
||||
)
|
||||
}
|
||||
"gaana" -> {
|
||||
File(imageDir, (url.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
|
||||
Source.Gaana.name -> {
|
||||
File(
|
||||
imageDir,
|
||||
(url.substringBeforeLast('/').substringAfterLast(
|
||||
'/'
|
||||
)) + ".jpeg"
|
||||
)
|
||||
}
|
||||
|
||||
else -> File(imageDir, url.substringAfterLast('/') + ".jpeg")
|
||||
else -> File(
|
||||
imageDir,
|
||||
url.substringAfterLast('/') + ".jpeg"
|
||||
)
|
||||
}
|
||||
resource?.copyTo(file)
|
||||
} catch (e: IOException) {
|
||||
@ -633,4 +657,36 @@ class ForegroundService : Service(){
|
||||
}
|
||||
}
|
||||
|
||||
private fun killService() {
|
||||
serviceScope.launch{
|
||||
messageList = mutableListOf("Cleaning And Exiting","","","")
|
||||
fetch.cancelAll()
|
||||
fetch.removeAll()
|
||||
updateNotification()
|
||||
cleanFiles(File(defaultDir))
|
||||
messageList = mutableListOf("","","","")
|
||||
releaseWakeLock()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stopForeground(true)
|
||||
} else {
|
||||
stopSelf()//System will automatically close it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialiseFetch() {
|
||||
val fetchConfiguration =
|
||||
FetchConfiguration.Builder(this)
|
||||
.setNamespace(channelId)
|
||||
.setDownloadConcurrentLimit(4)
|
||||
.build()
|
||||
|
||||
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||
|
||||
fetch = Fetch.getDefaultInstance()
|
||||
fetch.addListener(fetchListener)
|
||||
//clearing all not completed Downloads
|
||||
//Starting fresh
|
||||
fetch.removeAll()
|
||||
}
|
||||
}
|
27
app/src/main/res/drawable/ic_baseline_cancel_24.xml
Normal file
27
app/src/main/res/drawable/ic_baseline_cancel_24.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<!--
|
||||
~ Copyright (C) 2020 Shabinder Singh
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
|
||||
</vector>
|
32
app/src/main/res/drawable/ic_error.xml
Normal file
32
app/src/main/res/drawable/ic_error.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<!--
|
||||
~ Copyright (C) 2020 Shabinder Singh
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="38dp" android:height="38dp"
|
||||
android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:pathData="m512,256c0,141.387 -114.613,256 -256,256s-256,-114.613 -256,-256 114.613,-256 256,-256 256,114.613 256,256zM512,256">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="512" android:endY="256"
|
||||
android:startX="0" android:startY="256" android:type="linear">
|
||||
<item android:color="#748AFF" android:offset="0"/>
|
||||
<item android:color="#FF3C64" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:fillColor="#000" android:pathData="m256,56c-110.281,0 -200,89.719 -200,200s89.719,200 200,200 200,-89.719 200,-200 -89.719,-200 -200,-200zM256,426c-93.738,0 -170,-76.262 -170,-170s76.262,-170 170,-170 170,76.262 170,170 -76.262,170 -170,170zM256,426"/>
|
||||
<path android:fillColor="#000" android:pathData="m324.18,187.82c-5.859,-5.855 -15.355,-5.855 -21.215,0l-46.965,46.965 -46.965,-46.965c-5.859,-5.855 -15.355,-5.855 -21.215,0 -5.855,5.859 -5.855,15.355 0,21.215l46.965,46.965 -46.965,46.965c-5.855,5.859 -5.855,15.355 0,21.215 2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.605,-4.395l46.969,-46.965 46.965,46.965c2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.609,-4.395 5.855,-5.859 5.855,-15.355 0,-21.215l-46.965,-46.965 46.965,-46.965c5.855,-5.859 5.855,-15.355 0,-21.215zM324.18,187.82"/>
|
||||
</vector>
|
61
app/src/main/res/drawable/progress_bar.xml
Normal file
61
app/src/main/res/drawable/progress_bar.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (C) 2020 Shabinder Singh
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="5dip" />
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:centerColor="#ff9d9e9d"
|
||||
android:centerY="0.75"
|
||||
android:endColor="#ff9d9e9d"
|
||||
android:startColor="#ff9d9e9d"
|
||||
/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/secondaryProgress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="5dip" />
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:centerColor="#80ffb600"
|
||||
android:centerY="0.75"
|
||||
android:endColor="#80ffb600"
|
||||
android:startColor="#80ffb600"
|
||||
/>
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
<item
|
||||
android:id="@android:id/progress"
|
||||
>
|
||||
<clip>
|
||||
<shape>
|
||||
<corners
|
||||
android:radius="5dip" />
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="#2196f3"
|
||||
android:startColor="#2196f3" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
133
app/src/main/res/layout/download_song_item.xml
Normal file
133
app/src/main/res/layout/download_song_item.xml
Normal file
@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (C) 2020 Shabinder Singh
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Title" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="5dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressDrawable="@drawable/progress_bar"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/actionButton"
|
||||
app:layout_constraintEnd_toStartOf="@+id/actionButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/actionButton" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionButton"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Retry"
|
||||
android:textColor="@color/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleTextView"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progress_TextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/status_TextView"
|
||||
app:layout_constraintEnd_toStartOf="@+id/actionButton"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/progressBar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/progressBar"
|
||||
tools:text="10%" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloadSpeedTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/status_TextView"
|
||||
app:layout_constraintEnd_toEndOf="@+id/progressBar"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/progress_TextView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/progressBar"
|
||||
tools:text="204 MB/s" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/remaining_TextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/status_TextView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/progress_TextView"
|
||||
tools:text="10s" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_TextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textStyle="italic|bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/remaining_TextView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/actionButton"
|
||||
tools:text="Status" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="@android:color/darker_gray"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -54,7 +54,7 @@
|
||||
app:layout_anchor="@+id/appbar"
|
||||
app:layout_anchorGravity="bottom|center"
|
||||
app:maxImageSize="38dp"
|
||||
app:rippleColor="@color/colorPrimaryDark"
|
||||
android:clickable="false"
|
||||
app:srcCompat="@drawable/ic_refresh"
|
||||
app:tint="@null" />
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="#000000">
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
|
||||
android:textColor="#9AB3FF"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_download_progress"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageUrl"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
@ -75,11 +75,11 @@
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="4 minutes, 20 sec"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/artist"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_download_progress"
|
||||
app:layout_constraintStart_toEndOf="@+id/artist"
|
||||
app:layout_constraintTop_toTopOf="@+id/artist" />
|
||||
|
||||
@ -87,6 +87,7 @@
|
||||
android:id="@+id/btn_download"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/circular_background"
|
||||
android:backgroundTint="@color/black"
|
||||
android:scaleType="centerInside"
|
||||
@ -95,5 +96,27 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_arrow" />
|
||||
|
||||
<com.github.lzyzsd.circleprogress.ArcProgress
|
||||
android:id="@+id/btn_download_progress"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:padding="1dp"
|
||||
android:visibility="invisible"
|
||||
app:arc_angle="260"
|
||||
app:arc_bottom_text="Waiting"
|
||||
app:arc_bottom_text_size="9sp"
|
||||
app:arc_finished_color="@color/colorPrimary"
|
||||
app:arc_progress="0"
|
||||
app:arc_stroke_width="2dp"
|
||||
app:arc_suffix_text_padding="0dp"
|
||||
app:arc_suffix_text_size="11sp"
|
||||
app:arc_text_color="@color/colorPrimary"
|
||||
app:arc_text_size="20sp"
|
||||
app:arc_unfinished_color="@color/colorAccent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -36,6 +36,9 @@ buildscript {
|
||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
||||
//Kotlinx-Serialization
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
//Crashlytics & Analytics
|
||||
classpath 'com.google.gms:google-services:4.3.4'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Shabinder Singh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
configurations.maybeCreate("default")
|
||||
artifacts.add("default", file('mobile-ffmpeg.aar'))
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user