diff --git a/res/drawable/arrow_drop_down_24px.xml b/res/drawable/arrow_drop_down_24px.xml
new file mode 100644
index 000000000..ea75299b3
--- /dev/null
+++ b/res/drawable/arrow_drop_down_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/res/drawable/osudirect.png b/res/drawable/osudirect.png
new file mode 100644
index 000000000..3f6a60b42
Binary files /dev/null and b/res/drawable/osudirect.png differ
diff --git a/res/drawable/powered_by_osudirect.png b/res/drawable/powered_by_osudirect.png
deleted file mode 100644
index e3d843001..000000000
Binary files a/res/drawable/powered_by_osudirect.png and /dev/null differ
diff --git a/res/layout/beatmap_downloader_fragment.xml b/res/layout/beatmap_downloader_fragment.xml
index c4aa876f3..5e4aae8f6 100644
--- a/res/layout/beatmap_downloader_fragment.xml
+++ b/res/layout/beatmap_downloader_fragment.xml
@@ -26,11 +26,13 @@
-
+ android:layout_toLeftOf="@id/logo"
+ android:drawableLeft="@drawable/osudirect"
+ android:drawablePadding="8dp"
+ android:paddingVertical="16dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="2dp"
+ android:text="osu.direct"
+ android:textColor="#FFF" />
+ Option(
+ text = buildSpannedString {
+ append(mirror.description)
+ appendLine()
+ color(0xBFFFFFFF.toInt()) { append(mirror.homeUrl) }
+ },
+ value = mirror.ordinal,
+ icon = requireContext().getDrawable(mirror.logoResource)
+ )
+ })
+ .setSelected(mirror.ordinal)
+ .setOnSelectListener { value ->
+ value as Int
+ if (value != mirror.ordinal) {
+ Config.setInt("beatmapMirror", value)
+ mirror = BeatmapMirror.entries[Config.getInt("beatmapMirror", 0)]
+ search(false)
+ }
+ }
+ .setTitle("Select a beatmap mirror")
+ .show()
}
findViewById(R.id.close)!!.setOnClickListener {
@@ -195,21 +213,17 @@ class BeatmapListing : BaseFragment(),
ensureActive()
- JsonArrayRequest(mirror.search.endpoint).use { request ->
-
- request.buildUrl {
-
- addQueryParameter("mode", "0")
- addQueryParameter("query", searchBox.text.toString())
- addQueryParameter("offset", offset.toString())
- }
+ JsonArrayRequest(
+ mirror.search.request(
+ query = searchBox.text.toString(),
+ offset = offset
+ )
+ ).use { request ->
request.buildRequest { header("User-Agent", "Chrome/Android") }
-
ensureActive()
- val beatmapSets = mirror.search.mapResponse(request.execute().json)
-
+ val beatmapSets = mirror.search.response(request.execute().json)
ensureActive()
adapter.data.addAll(beatmapSets)
@@ -286,7 +300,7 @@ class BeatmapListing : BaseFragment(),
/**
* The current selected beatmap mirror.
*/
- var mirror = BeatmapMirror.OSU_DIRECT
+ var mirror = BeatmapMirror.entries[Config.getInt("beatmapMirror", 0)]
/**
* Whether is a beatmap preview music playing or not.
@@ -393,8 +407,10 @@ class BeatmapSetDetails(val beatmapSet: BeatmapSetModel, val holder: BeatmapSetV
}
downloadButton.setOnClickListener {
- val url = BeatmapListing.mirror.downloadEndpoint(beatmapSet.id)
- BeatmapDownloader.download(url, "${beatmapSet.id} ${beatmapSet.artist} - ${beatmapSet.title}")
+ BeatmapDownloader.download(
+ url = BeatmapListing.mirror.download.request(beatmapSet.id).toString(),
+ suggestedFilename = "${beatmapSet.id} ${beatmapSet.artist} - ${beatmapSet.title}"
+ )
}
cover.setImageDrawable(holder.cover.drawable)
@@ -598,8 +614,10 @@ class BeatmapSetViewHolder(itemView: View, private val mediaScope: CoroutineScop
}
downloadButton.setOnClickListener {
- val url = BeatmapListing.mirror.downloadEndpoint(beatmapSet.id)
- BeatmapDownloader.download(url, "${beatmapSet.id} ${beatmapSet.artist} - ${beatmapSet.title}")
+ BeatmapDownloader.download(
+ url = BeatmapListing.mirror.download.request(beatmapSet.id).toString(),
+ suggestedFilename = "${beatmapSet.id} ${beatmapSet.artist} - ${beatmapSet.title}"
+ )
}
@@ -621,7 +639,7 @@ class BeatmapSetViewHolder(itemView: View, private val mediaScope: CoroutineScop
previewJob = mediaScope.launch {
try {
- previewStream = URLBassStream(BeatmapListing.mirror.previewEndpoint(beatmapSet.beatmaps[0].id)) {
+ previewStream = URLBassStream(BeatmapListing.mirror.preview.request(beatmapSet.beatmaps[0].id).toString()) {
stopPreview(true)
if (BeatmapListing.isPlayingMusic) {
diff --git a/src/com/reco1l/osu/beatmaplisting/BeatmapModel.kt b/src/com/reco1l/osu/beatmaplisting/BeatmapListingModels.kt
similarity index 97%
rename from src/com/reco1l/osu/beatmaplisting/BeatmapModel.kt
rename to src/com/reco1l/osu/beatmaplisting/BeatmapListingModels.kt
index 67a4c9ac8..0ebe77a03 100644
--- a/src/com/reco1l/osu/beatmaplisting/BeatmapModel.kt
+++ b/src/com/reco1l/osu/beatmaplisting/BeatmapListingModels.kt
@@ -6,54 +6,31 @@ import ru.nsu.ccfit.zuev.osu.RankedStatus
* Beatmap set response model for beatmaps mirrors.
*/
data class BeatmapSetModel(
-
val id: Long,
-
val title: String,
-
val titleUnicode: String,
-
val artist: String,
-
val artistUnicode: String,
-
val status: RankedStatus,
-
val creator: String,
-
val thumbnail: String?,
-
val beatmaps: List
-
)
/**
* Beatmap response model for beatmaps mirrors.
*/
data class BeatmapModel(
-
val id: Long,
-
val version: String,
-
val starRating: Double,
-
val ar: Double,
-
val cs: Double,
-
val hp: Double,
-
val od: Double,
-
val bpm: Double,
-
val lengthSec: Long,
-
val circleCount: Int,
-
val sliderCount:Int,
-
val spinnerCount: Int
-
)
\ No newline at end of file
diff --git a/src/com/reco1l/osu/beatmaplisting/BeatmapMirror.kt b/src/com/reco1l/osu/beatmaplisting/BeatmapMirror.kt
index ee199e221..3485e3b65 100644
--- a/src/com/reco1l/osu/beatmaplisting/BeatmapMirror.kt
+++ b/src/com/reco1l/osu/beatmaplisting/BeatmapMirror.kt
@@ -1,42 +1,53 @@
package com.reco1l.osu.beatmaplisting
-import org.json.JSONArray
-import ru.nsu.ccfit.zuev.osu.RankedStatus
-
+import androidx.annotation.DrawableRes
+import com.reco1l.osu.beatmaplisting.mirrors.OsuDirectDownloadRequestModel
+import com.reco1l.osu.beatmaplisting.mirrors.OsuDirectPreviewRequestModel
+import com.reco1l.osu.beatmaplisting.mirrors.OsuDirectSearchRequestModel
+import com.reco1l.osu.beatmaplisting.mirrors.OsuDirectSearchResponseModel
+import ru.nsu.ccfit.zuev.osuplus.R
/**
- * Defines an action to be performed on a mirror API.
+ * Defines a beatmap mirror API and its actions.
*/
-data class MirrorAction(
+enum class BeatmapMirror(
/**
- * The action API endpoint.
+ * The home URL of the beatmap mirror where the user will be redirected to when
+ * clicking on the logo.
*/
- // TODO replace with a request creation function, some APIs have different query arguments.
- val endpoint: String,
+ val homeUrl: String,
/**
- * A function to map the response into a model.
+ * The description of the beatmap mirror.
*/
- val mapResponse: (R) -> M
+ val description: String,
-)
+ /**
+ * The resource ID of the logo image to be displayed in the UI.
+ */
+ @DrawableRes
+ val logoResource: Int,
-/**
- * Defines a beatmap mirror API and its actions.
- */
-enum class BeatmapMirror(
+
+ // Actions / Endpoints
/**
* The search query action.
*/
- val search: MirrorAction>,
+ val search: BeatmapMirrorActionWithResponse,
- val downloadEndpoint: (Long) -> String,
+ /**
+ * The download action.
+ */
+ val download: BeatmapMirrorAction,
- val previewEndpoint: (Long) -> String,
+ /**
+ * The music preview action.
+ */
+ val preview: BeatmapMirrorAction,
- ) {
+) {
/**
* osu.direct beatmap mirror.
@@ -44,54 +55,17 @@ enum class BeatmapMirror(
* [See documentation](https://osu.direct/api/docs)
*/
OSU_DIRECT(
- search = MirrorAction(
- endpoint = "https://osu.direct/api/v2/search",
- mapResponse = { array ->
-
- MutableList(array.length()) { index ->
-
- val json = array.getJSONObject(index)
-
- BeatmapSetModel(
- id = json.getLong("id"),
- title = json.getString("title"),
- titleUnicode = json.getString("title_unicode"),
- artist = json.getString("artist"),
- artistUnicode = json.getString("artist_unicode"),
- status = RankedStatus.valueOf(json.getInt("ranked")),
- creator = json.getString("creator"),
- thumbnail = json.optJSONObject("covers")?.optString("card"),
- beatmaps = json.getJSONArray("beatmaps").let {
-
- MutableList(it.length()) { i ->
-
- val obj = it.getJSONObject(i)
-
- BeatmapModel(
- id = obj.getLong("id"),
- version = obj.getString("version"),
- starRating = obj.getDouble("difficulty_rating"),
- ar = obj.getDouble("ar"),
- cs = obj.getDouble("cs"),
- hp = obj.getDouble("drain"),
- od = obj.getDouble("accuracy"),
- bpm = obj.getDouble("bpm"),
- lengthSec = obj.getLong("hit_length"),
- circleCount = obj.getInt("count_circles"),
- sliderCount = obj.getInt("count_sliders"),
- spinnerCount = obj.getInt("count_spinners")
- )
-
- }.sortedBy(BeatmapModel::starRating)
- }
- )
- }
+ homeUrl = "https://osu.direct",
+ description = "osu.direct",
+ logoResource = R.drawable.osudirect,
- }
+ search = BeatmapMirrorActionWithResponse(
+ request = OsuDirectSearchRequestModel(),
+ response = OsuDirectSearchResponseModel(),
),
- downloadEndpoint = { "https://osu.direct/api/d/$it" },
- previewEndpoint = { "https://osu.direct/api/media/preview/$it" },
- );
+ download = BeatmapMirrorAction(OsuDirectDownloadRequestModel()),
+ preview = BeatmapMirrorAction(OsuDirectPreviewRequestModel()),
+ )
}
diff --git a/src/com/reco1l/osu/beatmaplisting/BeatmapMirrorAction.kt b/src/com/reco1l/osu/beatmaplisting/BeatmapMirrorAction.kt
new file mode 100644
index 000000000..f97a90b33
--- /dev/null
+++ b/src/com/reco1l/osu/beatmaplisting/BeatmapMirrorAction.kt
@@ -0,0 +1,24 @@
+package com.reco1l.osu.beatmaplisting
+
+/**
+ * Defines an action to be performed on a mirror API.
+ */
+open class BeatmapMirrorAction(
+
+ /**
+ * The action API endpoint.
+ */
+ val request: RequestModel
+
+)
+
+class BeatmapMirrorActionWithResponse(
+
+ request: RequestModel,
+
+ /**
+ * The action response mapping.
+ */
+ val response: ResponseModel
+
+) : BeatmapMirrorAction(request)
diff --git a/src/com/reco1l/osu/beatmaplisting/BeatmapMirrorModels.kt b/src/com/reco1l/osu/beatmaplisting/BeatmapMirrorModels.kt
new file mode 100644
index 000000000..9101f66ea
--- /dev/null
+++ b/src/com/reco1l/osu/beatmaplisting/BeatmapMirrorModels.kt
@@ -0,0 +1,38 @@
+package com.reco1l.osu.beatmaplisting
+
+import okhttp3.HttpUrl
+
+
+// Request
+
+fun interface BeatmapMirrorSearchRequestModel {
+ /**
+ * @param query The search query.
+ * @param offset The search result offset.
+ */
+ operator fun invoke(query: String, offset: Int): HttpUrl
+}
+
+fun interface BeatmapMirrorDownloadRequestModel {
+ /**
+ * @param beatmapSetId The beatmap set ID.
+ */
+ operator fun invoke(beatmapSetId: Long): HttpUrl
+}
+
+fun interface BeatmapMirrorPreviewRequestModel {
+ /**
+ * @param beatmapSetId The beatmap set ID.
+ */
+ operator fun invoke(beatmapSetId: Long): HttpUrl
+}
+
+
+// Response
+
+fun interface BeatmapMirrorSearchResponseModel {
+ /**
+ * @return The list of beatmap sets.
+ */
+ operator fun invoke(response: Any): MutableList
+}
\ No newline at end of file
diff --git a/src/com/reco1l/osu/beatmaplisting/mirrors/OsuDirect.kt b/src/com/reco1l/osu/beatmaplisting/mirrors/OsuDirect.kt
new file mode 100644
index 000000000..ab5a55b25
--- /dev/null
+++ b/src/com/reco1l/osu/beatmaplisting/mirrors/OsuDirect.kt
@@ -0,0 +1,80 @@
+package com.reco1l.osu.beatmaplisting.mirrors
+
+import com.reco1l.osu.beatmaplisting.BeatmapMirrorDownloadRequestModel
+import com.reco1l.osu.beatmaplisting.BeatmapMirrorPreviewRequestModel
+import com.reco1l.osu.beatmaplisting.BeatmapMirrorSearchRequestModel
+import com.reco1l.osu.beatmaplisting.BeatmapMirrorSearchResponseModel
+import com.reco1l.osu.beatmaplisting.BeatmapModel
+import com.reco1l.osu.beatmaplisting.BeatmapSetModel
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import org.json.JSONArray
+import ru.nsu.ccfit.zuev.osu.RankedStatus
+
+
+class OsuDirectSearchRequestModel : BeatmapMirrorSearchRequestModel {
+ override fun invoke(query: String, offset: Int): HttpUrl {
+ return "https://osu.direct/api/v2/search".toHttpUrl()
+ .newBuilder()
+ .addQueryParameter("mode", "0")
+ .addQueryParameter("query", query)
+ .addQueryParameter("offset", offset.toString())
+ .build()
+ }
+}
+
+class OsuDirectSearchResponseModel : BeatmapMirrorSearchResponseModel {
+ override fun invoke(response: Any): MutableList {
+ response as JSONArray
+
+ return MutableList(response.length()) { index ->
+ val json = response.getJSONObject(index)
+
+ BeatmapSetModel(
+ id = json.getLong("id"),
+ title = json.getString("title"),
+ titleUnicode = json.getString("title_unicode"),
+ artist = json.getString("artist"),
+ artistUnicode = json.getString("artist_unicode"),
+ status = RankedStatus.valueOf(json.getInt("ranked")),
+ creator = json.getString("creator"),
+ thumbnail = json.optJSONObject("covers")?.optString("card"),
+ beatmaps = json.getJSONArray("beatmaps").let {
+
+ MutableList(it.length()) { i ->
+
+ val obj = it.getJSONObject(i)
+
+ BeatmapModel(
+ id = obj.getLong("id"),
+ version = obj.getString("version"),
+ starRating = obj.getDouble("difficulty_rating"),
+ ar = obj.getDouble("ar"),
+ cs = obj.getDouble("cs"),
+ hp = obj.getDouble("drain"),
+ od = obj.getDouble("accuracy"),
+ bpm = obj.getDouble("bpm"),
+ lengthSec = obj.getLong("hit_length"),
+ circleCount = obj.getInt("count_circles"),
+ sliderCount = obj.getInt("count_sliders"),
+ spinnerCount = obj.getInt("count_spinners")
+ )
+
+ }.sortedBy(BeatmapModel::starRating)
+ }
+ )
+ }
+ }
+}
+
+class OsuDirectDownloadRequestModel : BeatmapMirrorDownloadRequestModel {
+ override fun invoke(beatmapSetId: Long): HttpUrl {
+ return "https://osu.direct/api/d/$beatmapSetId".toHttpUrl()
+ }
+}
+
+class OsuDirectPreviewRequestModel : BeatmapMirrorPreviewRequestModel {
+ override fun invoke(beatmapSetId: Long): HttpUrl {
+ return "https://osu.direct/api/media/preview/$beatmapSetId".toHttpUrl()
+ }
+}
\ No newline at end of file
diff --git a/src/com/reco1l/osu/ui/ListDialog.kt b/src/com/reco1l/osu/ui/ListDialog.kt
index 40547753e..75ffe9cd2 100644
--- a/src/com/reco1l/osu/ui/ListDialog.kt
+++ b/src/com/reco1l/osu/ui/ListDialog.kt
@@ -1,14 +1,17 @@
package com.reco1l.osu.ui
import android.graphics.Color
+import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
+import androidx.annotation.DrawableRes
import androidx.core.view.forEach
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.reco1l.toolkt.android.cornerRadius
import com.reco1l.toolkt.android.dp
+import com.reco1l.toolkt.android.drawableLeft
import com.reco1l.toolkt.android.drawableRight
import ru.nsu.ccfit.zuev.osuplus.R
@@ -21,12 +24,17 @@ data class Option(
/**
* The text to be displayed in the option.
*/
- val text: String,
+ val text: CharSequence,
/**
* The value to be returned when the option is selected.
*/
- val value: Any
+ val value: Any,
+
+ /**
+ * The icon to be displayed in the option.
+ */
+ val icon: Drawable? = null
)
@@ -71,8 +79,8 @@ open class SelectDialog : MessageDialog() {
}
- fun setOptions(options: MutableList