Skip to content

Commit cccf47d

Browse files
committed
Melotune v1.0.18
* Introduced **Snappy Lists** across the UI — list items now smoothly snap into the viewport for a more polished browsing experience. * Added the new **Queue System**: * **Add to Queue** support. * **Play Next** option for better playback control. * Added **Import** and **Export** buttons for Saved Libraries, making backups and migration easier. * Removed duplicate entries from search results for cleaner and more accurate searches. There are two APK variants available: * **Melotune-X.X.X-nofirebaseRelease.apk** * Contains no Firebase, Google services, analytics, or tracking libraries. * Suitable for users who prefer a completely telemetry-free experience. * **Melotune-X.X.X-prodRelease.apk** * Includes Firebase Analytics solely for anonymous usage statistics and app analytics. * Helps improve the app by providing aggregate usage insights. > Both APKs are functionally identical. The only difference is the inclusion of analytics in the `prodRelease` build. You can install either version with confidence. Thank you for using Melotune and helping make it better with your feedback and support!
1 parent 7479ca3 commit cccf47d

18 files changed

Lines changed: 407 additions & 29 deletions

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ android {
1313
applicationId "com.harsh.shah.saavnmp3"
1414
minSdk 23
1515
targetSdk 36
16-
versionCode 11
17-
versionName '1.0.17'
16+
versionCode 12
17+
versionName '1.0.18'
1818

1919
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2020
//Universal APK Supporting All ABIs

app/src/main/java/com/harsh/shah/saavnmp3/activities/ArtistProfileActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.harsh.shah.saavnmp3.records.SongResponse.Lyrics
2727
import com.harsh.shah.saavnmp3.records.SongResponse.Song
2828
import com.harsh.shah.saavnmp3.utils.MiniPlayerHelper
2929
import com.harsh.shah.saavnmp3.utils.SharedPreferenceManager
30+
import com.harsh.shah.saavnmp3.utils.attachSnapHelper
3031
import com.squareup.picasso.Picasso
3132

3233
class ArtistProfileActivity : AppCompatActivity() {
@@ -58,6 +59,9 @@ class ArtistProfileActivity : AppCompatActivity() {
5859
binding!!.topSongsRecyclerview.setLayoutManager(LinearLayoutManager(this))
5960
binding!!.topAlbumsRecyclerview.setLayoutManager(LinearLayoutManager(this))
6061
binding!!.topSinglesRecyclerview.setLayoutManager(LinearLayoutManager(this))
62+
binding!!.topSongsRecyclerview.attachSnapHelper()
63+
binding!!.topAlbumsRecyclerview.attachSnapHelper()
64+
binding!!.topSinglesRecyclerview.attachSnapHelper()
6165

6266
binding!!.topSongsSeeMore.setOnClickListener { v: View? ->
6367
startActivity(

app/src/main/java/com/harsh/shah/saavnmp3/activities/DownloadManagerActivity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.harsh.shah.saavnmp3.activities
1+
package com.harsh.shah.saavnmp3.activities
22

33
import android.os.Bundle
44
import android.view.View
@@ -7,6 +7,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
77
import com.harsh.shah.saavnmp3.adapters.ActivityDownloadManagerListAdapter
88
import com.harsh.shah.saavnmp3.databinding.ActivityDownloadManagerBinding
99
import com.harsh.shah.saavnmp3.utils.TrackDownloader
10+
import com.harsh.shah.saavnmp3.utils.attachSnapHelper
1011

1112
class DownloadManagerActivity : AppCompatActivity() {
1213
private var binding: ActivityDownloadManagerBinding? = null
@@ -17,6 +18,7 @@ class DownloadManagerActivity : AppCompatActivity() {
1718
setContentView(binding!!.getRoot())
1819

1920
binding!!.recyclerView.setLayoutManager(LinearLayoutManager(this))
21+
binding!!.recyclerView.attachSnapHelper()
2022
val tracks = TrackDownloader.getDownloadedTracks(this)
2123
binding!!.recyclerView.setAdapter(
2224
ActivityDownloadManagerListAdapter(

app/src/main/java/com/harsh/shah/saavnmp3/activities/ListActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.harsh.shah.saavnmp3.records.sharedpref.SavedLibraries.Library
3232
import com.harsh.shah.saavnmp3.utils.MiniPlayerHelper
3333
import com.harsh.shah.saavnmp3.utils.MusicPlayerManager
3434
import com.harsh.shah.saavnmp3.utils.SharedPreferenceManager
35+
import com.harsh.shah.saavnmp3.utils.attachSnapHelper
3536
import com.harsh.shah.saavnmp3.utils.customview.BottomSheetItemView
3637
import com.squareup.picasso.Picasso
3738

@@ -47,6 +48,7 @@ class ListActivity : AppCompatActivity() {
4748
MiniPlayerHelper.initMiniPlayer(this)
4849

4950
binding!!.recyclerView.setLayoutManager(LinearLayoutManager(this))
51+
binding!!.recyclerView.attachSnapHelper()
5052
binding!!.addMoreSongs.visibility = View.GONE
5153

5254
Log.i("ListActivity", "onCreate: reached ListActivity")

app/src/main/java/com/harsh/shah/saavnmp3/activities/MainActivity.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import java.util.Calendar
5151
import androidx.core.net.toUri
5252
import com.harsh.shah.saavnmp3.utils.MiniPlayerHelper
5353
import com.harsh.shah.saavnmp3.utils.UpdateUtil
54+
import com.harsh.shah.saavnmp3.utils.attachSnapHelper
5455

5556
class MainActivity : AppCompatActivity() {
5657
private var requestStoragePermission: ActivityResultLauncher<Array<String>>? = null
@@ -135,6 +136,12 @@ class MainActivity : AppCompatActivity() {
135136
)
136137
)
137138

139+
// Snap helpers — smooth magnetic scroll on all horizontal lists
140+
binding!!.popularSongsRecyclerView.attachSnapHelper()
141+
binding!!.popularArtistsRecyclerView.attachSnapHelper()
142+
binding!!.popularAlbumsRecyclerView.attachSnapHelper()
143+
binding!!.savedRecyclerView.attachSnapHelper()
144+
138145
OverScrollDecoratorHelper.setUpOverScroll(
139146
binding!!.popularSongsRecyclerView,
140147
OverScrollDecoratorHelper.ORIENTATION_HORIZONTAL

app/src/main/java/com/harsh/shah/saavnmp3/activities/SavedLibrariesActivity.kt

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
44
import android.os.Bundle
55
import android.util.Log
66
import android.view.View
7+
import androidx.activity.result.contract.ActivityResultContracts
78
import androidx.appcompat.app.AppCompatActivity
89
import androidx.recyclerview.widget.LinearLayoutManager
910
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -17,20 +18,107 @@ import com.harsh.shah.saavnmp3.records.sharedpref.SavedLibraries.Library
1718
import com.harsh.shah.saavnmp3.utils.SharedPreferenceManager
1819
import me.everything.android.ui.overscroll.OverScrollDecoratorHelper
1920
import com.harsh.shah.saavnmp3.utils.MiniPlayerHelper
21+
import com.harsh.shah.saavnmp3.utils.attachSnapHelper
22+
import java.io.BufferedReader
23+
import java.io.InputStreamReader
2024
import java.text.SimpleDateFormat
2125
import java.util.Date
2226

2327
class SavedLibrariesActivity : AppCompatActivity() {
2428
var binding: ActivitySavedLibrariesBinding? = null
2529
var savedLibraries: SavedLibraries? = null
2630

31+
private val exportLauncher = registerForActivityResult(
32+
ActivityResultContracts.CreateDocument("application/json")
33+
) { uri ->
34+
if (uri != null) {
35+
try {
36+
val sharedPreferenceManager = SharedPreferenceManager.getInstance(this)
37+
val savedLibrariesObj = sharedPreferenceManager.savedLibrariesData
38+
val exportMap = HashMap<String, Any?>()
39+
exportMap["saved_libraries"] = savedLibrariesObj
40+
41+
val detailsMap = HashMap<String, String>()
42+
savedLibrariesObj?.lists?.forEach { lib ->
43+
if (lib != null && lib.id != null) {
44+
val detailJson = sharedPreferenceManager.getJson(lib.id)
45+
if (!detailJson.isNullOrEmpty()) {
46+
detailsMap[lib.id] = detailJson
47+
}
48+
}
49+
}
50+
exportMap["libraries_details"] = detailsMap
51+
52+
val exportJson = com.google.gson.Gson().toJson(exportMap)
53+
contentResolver.openOutputStream(uri)?.use { outputStream ->
54+
outputStream.write(exportJson.toByteArray())
55+
}
56+
Snackbar.make(binding!!.getRoot(), "Library exported successfully", Snackbar.LENGTH_SHORT).show()
57+
} catch (e: Exception) {
58+
Log.e("SavedLibrariesActivity", "Export failed", e)
59+
Snackbar.make(binding!!.getRoot(), "Export failed: ${e.message}", Snackbar.LENGTH_SHORT).show()
60+
}
61+
}
62+
}
63+
64+
private val importLauncher = registerForActivityResult(
65+
ActivityResultContracts.OpenDocument()
66+
) { uri ->
67+
if (uri != null) {
68+
try {
69+
contentResolver.openInputStream(uri)?.use { inputStream ->
70+
val reader = BufferedReader(InputStreamReader(inputStream))
71+
val stringBuilder = StringBuilder()
72+
var line: String?
73+
while (reader.readLine().also { line = it } != null) {
74+
stringBuilder.append(line)
75+
}
76+
val importJson = stringBuilder.toString()
77+
val gson = com.google.gson.Gson()
78+
val type = object : com.google.gson.reflect.TypeToken<HashMap<String, Any?>>() {}.type
79+
val importMap = gson.fromJson<HashMap<String, Any?>>(importJson, type)
80+
81+
val savedLibrariesJson = gson.toJson(importMap["saved_libraries"])
82+
val importedSavedLibraries = gson.fromJson(savedLibrariesJson, SavedLibraries::class.java)
83+
84+
val detailsJson = gson.toJson(importMap["libraries_details"])
85+
val importedDetails = gson.fromJson<HashMap<String, String>>(detailsJson, object : com.google.gson.reflect.TypeToken<HashMap<String, String>>() {}.type)
86+
87+
val sharedPreferenceManager = SharedPreferenceManager.getInstance(this)
88+
val currentSavedLibraries = sharedPreferenceManager.savedLibrariesData ?: SavedLibraries(ArrayList())
89+
val currentLists = currentSavedLibraries.lists ?: ArrayList()
90+
91+
importedSavedLibraries?.lists?.forEach { importedLib ->
92+
if (importedLib != null && importedLib.id != null) {
93+
if (currentLists.none { it?.id == importedLib.id }) {
94+
currentLists.add(importedLib)
95+
}
96+
val detailJson = importedDetails?.get(importedLib.id)
97+
if (!detailJson.isNullOrEmpty()) {
98+
sharedPreferenceManager.putJson(importedLib.id, detailJson)
99+
}
100+
}
101+
}
102+
103+
sharedPreferenceManager.savedLibrariesData = SavedLibraries(currentLists)
104+
showData(sharedPreferenceManager)
105+
Snackbar.make(binding!!.getRoot(), "Library imported successfully", Snackbar.LENGTH_SHORT).show()
106+
}
107+
} catch (e: Exception) {
108+
Log.e("SavedLibrariesActivity", "Import failed", e)
109+
Snackbar.make(binding!!.getRoot(), "Import failed: ${e.message}", Snackbar.LENGTH_SHORT).show()
110+
}
111+
}
112+
}
113+
27114
override fun onCreate(savedInstanceState: Bundle?) {
28115
super.onCreate(savedInstanceState)
29116
binding = ActivitySavedLibrariesBinding.inflate(layoutInflater)
30117
setContentView(binding!!.getRoot())
31118
MiniPlayerHelper.initMiniPlayer(this)
32119

33120
binding!!.recyclerView.setLayoutManager(LinearLayoutManager(this))
121+
binding!!.recyclerView.attachSnapHelper()
34122
OverScrollDecoratorHelper.setUpOverScroll(
35123
binding!!.recyclerView,
36124
OverScrollDecoratorHelper.ORIENTATION_VERTICAL
@@ -81,6 +169,14 @@ class SavedLibrariesActivity : AppCompatActivity() {
81169
bottomSheetDialog.show()
82170
})
83171

172+
binding?.importLibrary?.setOnClickListener {
173+
importLauncher.launch(arrayOf("application/json"))
174+
}
175+
176+
binding?.exportLibrary?.setOnClickListener {
177+
exportLauncher.launch("saavn_library.json")
178+
}
179+
84180
showData()
85181
}
86182

app/src/main/java/com/harsh/shah/saavnmp3/activities/SearchActivity.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.harsh.shah.saavnmp3.records.GlobalSearch
2727
import com.harsh.shah.saavnmp3.records.GlobalSearch.Data.TopQuery
2828
import com.harsh.shah.saavnmp3.utils.MiniPlayerHelper
2929
import com.harsh.shah.saavnmp3.utils.SharedPreferenceManager
30+
import com.harsh.shah.saavnmp3.utils.attachSnapHelper
3031
import me.everything.android.ui.overscroll.OverScrollDecoratorHelper
3132
import java.util.Locale
3233
import androidx.core.net.toUri
@@ -47,6 +48,7 @@ class SearchActivity : AppCompatActivity() {
4748

4849
OverScrollDecoratorHelper.setUpOverScroll(binding!!.hscrollview)
4950
binding!!.recyclerView.setLayoutManager(LinearLayoutManager(this))
51+
binding!!.recyclerView.attachSnapHelper()
5052

5153
binding!!.edittext.requestFocus()
5254

@@ -205,7 +207,10 @@ class SearchActivity : AppCompatActivity() {
205207
} else {
206208
throw IllegalStateException("Unexpected value: " + binding!!.chipGroup.checkedChipId)
207209
}
208-
if (!data.isEmpty()) binding!!.recyclerView.setAdapter(ActivitySearchListItemAdapter(data.filterNotNull().toMutableList()))
210+
if (!data.isEmpty()) {
211+
val uniqueData = data.filterNotNull().distinctBy { it.id }.toMutableList()
212+
binding!!.recyclerView.setAdapter(ActivitySearchListItemAdapter(uniqueData))
213+
}
209214
}
210215

211216
private fun addSongsData(data: MutableList<SearchListItem?>) {

app/src/main/java/com/harsh/shah/saavnmp3/adapters/ActivitySearchListItemAdapter.kt

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,60 @@ class ActivitySearchListItemAdapter(private val data: MutableList<SearchListItem
4747
val item = data[position]
4848

4949
(holder.itemView.findViewById<View?>(R.id.title) as TextView).text = item.title()
50-
(holder.itemView.findViewById<View?>(R.id.artist) as TextView).text = item.subtitle()
50+
51+
val typePrefix = when (item.type) {
52+
SearchListItem.Type.SONG -> "Song"
53+
SearchListItem.Type.ALBUM -> "Album"
54+
SearchListItem.Type.PLAYLIST -> "Playlist"
55+
SearchListItem.Type.ARTIST -> "Artist"
56+
else -> ""
57+
}
58+
val formattedSubtitle = if (typePrefix.isNotEmpty() && !item.subtitle().isNullOrEmpty()) {
59+
"$typePrefix${item.subtitle()}"
60+
} else if (typePrefix.isNotEmpty()) {
61+
typePrefix
62+
} else {
63+
item.subtitle()
64+
}
65+
(holder.itemView.findViewById<View?>(R.id.artist) as TextView).text = formattedSubtitle
66+
67+
val coverCard = holder.itemView.findViewById<com.google.android.material.card.MaterialCardView>(R.id.coverCard)
68+
if (coverCard != null) {
69+
val radiusRes = if (item.type == SearchListItem.Type.ARTIST) {
70+
com.intuit.sdp.R.dimen._20sdp
71+
} else {
72+
com.intuit.sdp.R.dimen._4sdp
73+
}
74+
coverCard.radius = coverCard.context.resources.getDimension(radiusRes)
75+
}
76+
77+
val moreIcon = holder.itemView.findViewById<ImageView>(R.id.more)
78+
if (item.type == SearchListItem.Type.SONG) {
79+
moreIcon.visibility = View.VISIBLE
80+
moreIcon.setOnClickListener { v ->
81+
val popup = androidx.appcompat.widget.PopupMenu(v.context, v)
82+
popup.menu.add("Play Next")
83+
popup.menu.add("Add to Queue")
84+
popup.setOnMenuItemClickListener { menuItem ->
85+
when (menuItem.title) {
86+
"Play Next" -> {
87+
MusicPlayerManager.playNext(item.id)
88+
android.widget.Toast.makeText(v.context, "Song will play next", android.widget.Toast.LENGTH_SHORT).show()
89+
true
90+
}
91+
"Add to Queue" -> {
92+
MusicPlayerManager.addToQueue(item.id)
93+
android.widget.Toast.makeText(v.context, "Song added to queue", android.widget.Toast.LENGTH_SHORT).show()
94+
true
95+
}
96+
else -> false
97+
}
98+
}
99+
popup.show()
100+
}
101+
} else {
102+
moreIcon.visibility = View.GONE
103+
}
51104

52105
Picasso.get().load(item.coverImage?.toUri())
53106
.into((holder.itemView.findViewById<View?>(R.id.coverImage) as ImageView?))

0 commit comments

Comments
 (0)