mirror of
				https://github.com/Kurocon/projectivy-booru-wallpaper-provider.git
				synced 2025-11-03 20:31:14 +01:00 
			
		
		
		
	Add support for Moebooru-, Gelbooru- and Zerochan-based imageboards, and Wallhaven. Restructure how board type preference is saved. Restructure README.
This commit is contained in:
		
							parent
							
								
									291307b121
								
							
						
					
					
						commit
						799151a720
					
				
					 6 changed files with 412 additions and 39 deletions
				
			
		| 
						 | 
				
			
			@ -13,8 +13,8 @@ android {
 | 
			
		|||
        applicationId = "nl.kurocon.plugin.wallpaperprovider.booru"
 | 
			
		||||
        minSdk = 23
 | 
			
		||||
        targetSdk = 35
 | 
			
		||||
        versionCode = 1
 | 
			
		||||
        versionName = "1.0"
 | 
			
		||||
        versionCode = 2
 | 
			
		||||
        versionName = "1.1"
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,39 +1,63 @@
 | 
			
		|||
package nl.kurocon.plugin.wallpaperprovider.booru
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.DANBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.GELBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.MOEBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.WALLHAVEN
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.ZEROCHAN
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import org.json.JSONArray
 | 
			
		||||
import org.json.JSONException
 | 
			
		||||
import org.json.JSONObject
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.net.URLEncoder
 | 
			
		||||
 | 
			
		||||
class BooruAPI(context: Context) {
 | 
			
		||||
    private val client = OkHttpClient()
 | 
			
		||||
    private var booruPluginUserAgent = "ProjectivyBooruWallpaperPlugin"
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        PreferencesManager.init(context)
 | 
			
		||||
        val pluginName = context.getString(R.string.plugin_name)
 | 
			
		||||
        val pluginVersion = context.getString(R.string.plugin_version)
 | 
			
		||||
        booruPluginUserAgent = "$pluginName/$pluginVersion"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Throws(IOException::class)
 | 
			
		||||
    fun getImageUrls(limit: Int = 20): List<BooruImage> {
 | 
			
		||||
        return when (PreferencesManager.booruType) {
 | 
			
		||||
            "danbooru" -> getDanbooruImageUrls(limit = limit)
 | 
			
		||||
            DANBOORU -> getDanbooruImageUrls(limit = limit)
 | 
			
		||||
            GELBOORU -> getGelbooruImageUrls(limit = limit)
 | 
			
		||||
            MOEBOORU -> getMoebooruImageUrls(limit = limit)
 | 
			
		||||
            ZEROCHAN -> getZerochanImageUrls(limit = limit)
 | 
			
		||||
            WALLHAVEN -> getWallhavenImageUrls(limit = limit)
 | 
			
		||||
            else -> getDanbooruImageUrls(limit = limit)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getRequestBuilder(url: String, addAuth: Boolean = false): Request.Builder {
 | 
			
		||||
        // Add default headers (User-Agent) and set URL
 | 
			
		||||
        var builder = Request.Builder()
 | 
			
		||||
            .url(url)
 | 
			
		||||
            .addHeader("User-Agent", booruPluginUserAgent)
 | 
			
		||||
        if (addAuth && (PreferencesManager.booruUserId.isNotEmpty() && PreferencesManager.booruApiKey.isNotEmpty())) {
 | 
			
		||||
            builder = builder.addHeader(
 | 
			
		||||
                "Authorization",
 | 
			
		||||
                okhttp3.Credentials.basic(PreferencesManager.booruUserId, PreferencesManager.booruApiKey)
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        return builder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Throws(IOException::class)
 | 
			
		||||
    fun getDanbooruImageUrls(limit: Int = 20): List<BooruImage> {
 | 
			
		||||
        val tags = PreferencesManager.booruTagSearch
 | 
			
		||||
        val encodedTags = URLEncoder.encode(tags, "UTF-8")
 | 
			
		||||
        val url = "${PreferencesManager.booruUrl}/posts.json?tags=$encodedTags&limit=$limit"
 | 
			
		||||
        var requestBuilder = Request.Builder()
 | 
			
		||||
            .url(url)
 | 
			
		||||
        if (PreferencesManager.booruUserId.isNotEmpty() && PreferencesManager.booruApiKey.isNotEmpty()) {
 | 
			
		||||
            requestBuilder = requestBuilder.addHeader("Authorization", okhttp3.Credentials.basic(PreferencesManager.booruUserId, PreferencesManager.booruApiKey))
 | 
			
		||||
        }
 | 
			
		||||
        val request = requestBuilder.build()
 | 
			
		||||
        val request = getRequestBuilder(url, addAuth=true).build()
 | 
			
		||||
 | 
			
		||||
        client.newCall(request).execute().use { response ->
 | 
			
		||||
            if (!response.isSuccessful) {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +68,7 @@ class BooruAPI(context: Context) {
 | 
			
		|||
                if (errorMessage.isNotEmpty()) {
 | 
			
		||||
                    throw IOException("Error ${response.code}. $errorMessage")
 | 
			
		||||
                }
 | 
			
		||||
                throw IOException("Unexpected code ${response.code} - ${response.body}")
 | 
			
		||||
                throw IOException("Error ${response.code} - ${response.body}")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val responseBody = response.body?.string() ?: throw IOException("Response body is null")
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +89,222 @@ class BooruAPI(context: Context) {
 | 
			
		|||
            return images
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Throws(IOException::class)
 | 
			
		||||
    fun getMoebooruImageUrls(limit: Int = 20): List<BooruImage> {
 | 
			
		||||
        val tags = PreferencesManager.booruTagSearch
 | 
			
		||||
        val encodedTags = URLEncoder.encode(tags, "UTF-8")
 | 
			
		||||
        var url = "${PreferencesManager.booruUrl}/post.json?tags=$encodedTags&limit=$limit"
 | 
			
		||||
        if (PreferencesManager.booruUserId.isNotEmpty() && PreferencesManager.booruApiKey.isNotEmpty()) {
 | 
			
		||||
            val encUsername = URLEncoder.encode(PreferencesManager.booruUserId, "UTF-8")
 | 
			
		||||
            val encApiKey = URLEncoder.encode(PreferencesManager.booruApiKey, "UTF-8")
 | 
			
		||||
            url = "$url&login=$encUsername&password_hash=$encApiKey"
 | 
			
		||||
        }
 | 
			
		||||
        val request = getRequestBuilder(url).build()
 | 
			
		||||
 | 
			
		||||
        client.newCall(request).execute().use { response ->
 | 
			
		||||
            if (!response.isSuccessful) {
 | 
			
		||||
                // Try to parse error message from body
 | 
			
		||||
                val responseBody = response.body?.string() ?: throw IOException("Unexpected code ${response.code}, and response body is empty")
 | 
			
		||||
                val jsonBody = JSONObject(responseBody)
 | 
			
		||||
                val errorMessage = jsonBody.optString("message")
 | 
			
		||||
                if (errorMessage.isNotEmpty()) {
 | 
			
		||||
                    throw IOException("Error ${response.code}. $errorMessage")
 | 
			
		||||
                }
 | 
			
		||||
                throw IOException("Error ${response.code} - ${response.body}")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val responseBody = response.body?.string() ?: throw IOException("Response body is null")
 | 
			
		||||
            val jsonArray = JSONArray(responseBody)
 | 
			
		||||
 | 
			
		||||
            // Parse JSON response into BooruImage instances
 | 
			
		||||
            val images = mutableListOf<BooruImage>()
 | 
			
		||||
            for (i in 0 until jsonArray.length()) {
 | 
			
		||||
                val jsonObject = jsonArray.getJSONObject(i)
 | 
			
		||||
                val imageUrl = jsonObject.optString("file_url")
 | 
			
		||||
                val title = jsonObject.optString("tag_string")
 | 
			
		||||
                val author = jsonObject.optString("tag_string_artist")
 | 
			
		||||
                val sourceUri = jsonObject.optString("source")
 | 
			
		||||
                if (imageUrl.isNotEmpty()) {
 | 
			
		||||
                    images.add(BooruImage(imageUrl, title, author, sourceUri))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return images
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Throws(IOException::class)
 | 
			
		||||
    fun getGelbooruImageUrls(limit: Int = 20): List<BooruImage> {
 | 
			
		||||
        val tags = PreferencesManager.booruTagSearch
 | 
			
		||||
        val encodedTags = URLEncoder.encode(tags, "UTF-8")
 | 
			
		||||
        var url = "${PreferencesManager.booruUrl}/index.php?page=dapi&s=post&q=index&tags=$encodedTags&limit=$limit&json=1"
 | 
			
		||||
        if (PreferencesManager.booruUserId.isNotEmpty() && PreferencesManager.booruApiKey.isNotEmpty()) {
 | 
			
		||||
            val encUsername = URLEncoder.encode(PreferencesManager.booruUserId, "UTF-8")
 | 
			
		||||
            val encApiKey = URLEncoder.encode(PreferencesManager.booruApiKey, "UTF-8")
 | 
			
		||||
            url = "$url&api_key=$encApiKey&user_id=$encUsername"
 | 
			
		||||
        }
 | 
			
		||||
        val request = getRequestBuilder(url).build()
 | 
			
		||||
 | 
			
		||||
        client.newCall(request).execute().use { response ->
 | 
			
		||||
            if (!response.isSuccessful) {
 | 
			
		||||
                // Try to parse error message from body
 | 
			
		||||
                val responseBody = response.body?.string() ?: throw IOException("Unexpected code ${response.code}, and response body is empty")
 | 
			
		||||
                val jsonBody = JSONObject(responseBody)
 | 
			
		||||
                val errorMessage = jsonBody.optString("message")
 | 
			
		||||
                if (errorMessage.isNotEmpty()) {
 | 
			
		||||
                    throw IOException("Error ${response.code}. $errorMessage")
 | 
			
		||||
                }
 | 
			
		||||
                throw IOException("Error ${response.code} - ${response.body}")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val responseBody = response.body?.string() ?: throw IOException("Response body is null")
 | 
			
		||||
            val jsonBody: JSONObject
 | 
			
		||||
            try {
 | 
			
		||||
                jsonBody = JSONObject(responseBody)
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                // gelbooru returns xml response if request was denied for some reason
 | 
			
		||||
                // i.e. user hit a rate limit because he didn't include api key
 | 
			
		||||
                throw IOException("Error. Unexpected response. You might be rate limited.")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Parse JSON response into BooruImage instances
 | 
			
		||||
            val images = mutableListOf<BooruImage>()
 | 
			
		||||
            val jsonArray = jsonBody.getJSONArray("post")
 | 
			
		||||
            for (i in 0 until jsonArray.length()) {
 | 
			
		||||
                val jsonObject = jsonArray.getJSONObject(i)
 | 
			
		||||
                val imageUrl = jsonObject.optString("file_url")
 | 
			
		||||
                val title = jsonObject.optString("tags")
 | 
			
		||||
                // Gelbooru does not have an easily accessible Artist tag. Link to the source in a best-effort way.
 | 
			
		||||
                val author = jsonObject.optString("source")
 | 
			
		||||
                val sourceUri = jsonObject.optString("source")
 | 
			
		||||
                if (imageUrl.isNotEmpty()) {
 | 
			
		||||
                    images.add(BooruImage(imageUrl, title, author, sourceUri))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return images
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Throws(IOException::class)
 | 
			
		||||
    fun getZerochanImageUrls(limit: Int = 20): List<BooruImage> {
 | 
			
		||||
        val tags = PreferencesManager.booruTagSearch
 | 
			
		||||
        val encodedTags = URLEncoder.encode(tags, "UTF-8")
 | 
			
		||||
        val url = "${PreferencesManager.booruUrl}/$encodedTags?json&l=$limit"
 | 
			
		||||
        var requestBuilder = getRequestBuilder(url)
 | 
			
		||||
        // Zerochan has no authentication, but asks to put the username in the User-Agent string.
 | 
			
		||||
        // This header is already added by the `getRequestBuilder`, so need to override it.
 | 
			
		||||
        if (PreferencesManager.booruUserId.isNotEmpty()) {
 | 
			
		||||
             requestBuilder = requestBuilder.header(
 | 
			
		||||
                 "User-Agent", "$booruPluginUserAgent - ${PreferencesManager.booruUserId}"
 | 
			
		||||
             )
 | 
			
		||||
        }
 | 
			
		||||
        val request = requestBuilder.build()
 | 
			
		||||
 | 
			
		||||
        client.newCall(request).execute().use { response ->
 | 
			
		||||
            if (!response.isSuccessful) {
 | 
			
		||||
                // Try to parse error message from body
 | 
			
		||||
                val responseBody = response.body?.string() ?: throw IOException("Unexpected code ${response.code}, and response body is empty")
 | 
			
		||||
                val jsonBody = JSONObject(responseBody)
 | 
			
		||||
                val errorMessage = jsonBody.optString("message")
 | 
			
		||||
                if (errorMessage.isNotEmpty()) {
 | 
			
		||||
                    throw IOException("Error ${response.code}. $errorMessage")
 | 
			
		||||
                }
 | 
			
		||||
                throw IOException("Error ${response.code} - ${response.body}")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val responseBody = response.body?.string() ?: throw IOException("Response body is null")
 | 
			
		||||
            val jsonArray = JSONObject(responseBody).getJSONArray("items")
 | 
			
		||||
 | 
			
		||||
            // Parse JSON response into BooruImage instances
 | 
			
		||||
            val images = mutableListOf<BooruImage>()
 | 
			
		||||
            for (i in 0 until jsonArray.length()) {
 | 
			
		||||
                val jsonObject = jsonArray.getJSONObject(i)
 | 
			
		||||
                val imageId = jsonObject.optInt("id")
 | 
			
		||||
                if (imageId != 0) {
 | 
			
		||||
                    // Do a second request for the complete image information, because
 | 
			
		||||
                    // zerochan does not include the full image link in the list results...
 | 
			
		||||
                    var requestBuilder2 =
 | 
			
		||||
                        getRequestBuilder("${PreferencesManager.booruUrl}/$imageId?json")
 | 
			
		||||
                    if (PreferencesManager.booruUserId.isNotEmpty()) {
 | 
			
		||||
                        requestBuilder2 = requestBuilder2.header(
 | 
			
		||||
                            "User-Agent",
 | 
			
		||||
                            "$booruPluginUserAgent - ${PreferencesManager.booruUserId}"
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    val request2 = requestBuilder2.build()
 | 
			
		||||
                    client.newCall(request2).execute().use { response2 ->
 | 
			
		||||
                        if (response2.isSuccessful) {
 | 
			
		||||
                            val responseBody2 = response2.body?.string()
 | 
			
		||||
                                ?: throw IOException("Response body is null")
 | 
			
		||||
                            val jsonBody: JSONObject
 | 
			
		||||
                            try {
 | 
			
		||||
                                jsonBody = JSONObject(responseBody2)
 | 
			
		||||
                                val imageUrl = jsonBody.optString("full")
 | 
			
		||||
                                val title1 = jsonBody.optString("primary")
 | 
			
		||||
                                val title2 = jsonBody.optJSONArray("tags")?.join(" ")
 | 
			
		||||
                                val title = "$title1 - $title2"
 | 
			
		||||
                                // Zerochan does not have an easily accessible Artist tag. Link to the source in a best-effort way.
 | 
			
		||||
                                val author = jsonBody.optString("source")
 | 
			
		||||
                                val sourceUri = jsonBody.optString("source")
 | 
			
		||||
                                if (imageUrl.isNotEmpty()) {
 | 
			
		||||
                                    images.add(BooruImage(imageUrl, title, author, sourceUri))
 | 
			
		||||
                                }
 | 
			
		||||
                            } catch (e: JSONException) {
 | 
			
		||||
                                // Invalid response, do nothing (probably rate limited, HTML output)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return images
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Throws(IOException::class)
 | 
			
		||||
    fun getWallhavenImageUrls(limit: Int = 20): List<BooruImage> {
 | 
			
		||||
        val tags = PreferencesManager.booruTagSearch
 | 
			
		||||
        val encodedTags = URLEncoder.encode(tags, "UTF-8")
 | 
			
		||||
        var url = "${PreferencesManager.booruUrl}/api/v1/search?q=$encodedTags&sorting=random"
 | 
			
		||||
        val requestBuilder = getRequestBuilder(url)
 | 
			
		||||
        if (PreferencesManager.booruApiKey.isNotEmpty()) {
 | 
			
		||||
            // Users can authenticate by including their API key either in a request URL by appending
 | 
			
		||||
            // ?apikey=<API KEY>, or by including the X-API-Key: <API KEY> header with the request.
 | 
			
		||||
            // API key grants access to NSFW images (but the purity=111 flag is not set by this plugin, default is SFW, 100).
 | 
			
		||||
            requestBuilder.addHeader("X-API-Key", PreferencesManager.booruApiKey)
 | 
			
		||||
        }
 | 
			
		||||
        val request = requestBuilder.build()
 | 
			
		||||
 | 
			
		||||
        client.newCall(request).execute().use { response ->
 | 
			
		||||
            if (!response.isSuccessful) {
 | 
			
		||||
                // Try to parse error message from body
 | 
			
		||||
                val responseBody = response.body?.string() ?: throw IOException("Unexpected code ${response.code}, and response body is empty")
 | 
			
		||||
                val jsonBody = JSONObject(responseBody)
 | 
			
		||||
                val errorMessage = jsonBody.optString("message")
 | 
			
		||||
                if (errorMessage.isNotEmpty()) {
 | 
			
		||||
                    throw IOException("Error ${response.code}. $errorMessage")
 | 
			
		||||
                }
 | 
			
		||||
                throw IOException("Error ${response.code} - ${response.body}")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val responseBody = response.body?.string() ?: throw IOException("Response body is null")
 | 
			
		||||
            val jsonBody = JSONObject(responseBody)
 | 
			
		||||
            val jsonArray = jsonBody.getJSONArray("data")
 | 
			
		||||
 | 
			
		||||
            // Parse JSON response into BooruImage instances
 | 
			
		||||
            val images = mutableListOf<BooruImage>()
 | 
			
		||||
            for (i in 0 until jsonArray.length()) {
 | 
			
		||||
                val jsonObject = jsonArray.getJSONObject(i)
 | 
			
		||||
                val imageUrl = jsonObject.optString("path")
 | 
			
		||||
                val title = jsonObject.optString("id")
 | 
			
		||||
                val author = jsonObject.optString("source")
 | 
			
		||||
                val sourceUri = jsonObject.optString("source")
 | 
			
		||||
                if (imageUrl.isNotEmpty()) {
 | 
			
		||||
                    images.add(BooruImage(imageUrl, title, author, sourceUri))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return images
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class BooruImage(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,13 +2,40 @@ package nl.kurocon.plugin.wallpaperprovider.booru
 | 
			
		|||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import androidx.annotation.IntDef
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import com.google.gson.Gson
 | 
			
		||||
import com.google.gson.GsonBuilder
 | 
			
		||||
import com.google.gson.ToNumberPolicy
 | 
			
		||||
import com.google.gson.reflect.TypeToken
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.DANBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.MOEBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.GELBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.ZEROCHAN
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.WALLHAVEN
 | 
			
		||||
 | 
			
		||||
object PreferencesManager {
 | 
			
		||||
 | 
			
		||||
    @Target(AnnotationTarget.TYPE)
 | 
			
		||||
    @Retention(AnnotationRetention.SOURCE)
 | 
			
		||||
    @IntDef(DANBOORU, MOEBOORU, GELBOORU, ZEROCHAN, WALLHAVEN)
 | 
			
		||||
    annotation class BooruType {
 | 
			
		||||
        companion object {
 | 
			
		||||
            const val DANBOORU = 1
 | 
			
		||||
            const val MOEBOORU = 2
 | 
			
		||||
            const val GELBOORU = 3
 | 
			
		||||
            const val ZEROCHAN = 4
 | 
			
		||||
            const val WALLHAVEN = 5
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private val BOORU_TYPE_NAMES = mapOf(
 | 
			
		||||
        DANBOORU to "Danbooru",
 | 
			
		||||
        MOEBOORU to "Moebooru",
 | 
			
		||||
        GELBOORU to "Gelbooru",
 | 
			
		||||
        ZEROCHAN to "Zerochan",
 | 
			
		||||
        WALLHAVEN to "Wallhaven",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private const val BOORU_URL_KEY = "booru_url_key"
 | 
			
		||||
    private const val BOORU_TYPE_KEY = "booru_type_key"
 | 
			
		||||
    private const val BOORU_TAG_SEARCH_KEY = "booru_tag_search_key"
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +54,10 @@ object PreferencesManager {
 | 
			
		|||
        editor.apply()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getBooruName(type: @BooruType Int): String {
 | 
			
		||||
        return BOORU_TYPE_NAMES[type]!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    operator fun set(key: String, value: Any?) =
 | 
			
		||||
        when (value) {
 | 
			
		||||
            is String? -> preferences.edit { it.putString(key, value) }
 | 
			
		||||
| 
						 | 
				
			
			@ -40,21 +71,30 @@ object PreferencesManager {
 | 
			
		|||
    inline operator fun <reified T : Any> get(
 | 
			
		||||
        key: String,
 | 
			
		||||
        defaultValue: T? = null
 | 
			
		||||
    ): T =
 | 
			
		||||
        when (T::class) {
 | 
			
		||||
            String::class -> preferences.getString(key, defaultValue as String? ?: "") as T
 | 
			
		||||
            Int::class -> preferences.getInt(key, defaultValue as? Int ?: -1) as T
 | 
			
		||||
            Boolean::class -> preferences.getBoolean(key, defaultValue as? Boolean ?: false) as T
 | 
			
		||||
            Float::class -> preferences.getFloat(key, defaultValue as? Float ?: -1f) as T
 | 
			
		||||
            Long::class -> preferences.getLong(key, defaultValue as? Long ?: -1) as T
 | 
			
		||||
            else -> throw UnsupportedOperationException("Not yet implemented")
 | 
			
		||||
    ): T {
 | 
			
		||||
        try {
 | 
			
		||||
            when (T::class) {
 | 
			
		||||
                String::class -> return preferences.getString(key, defaultValue as String? ?: "") as T
 | 
			
		||||
                Int::class -> return preferences.getInt(key, defaultValue as? Int ?: -1) as T
 | 
			
		||||
                Boolean::class -> return preferences.getBoolean(
 | 
			
		||||
                    key,
 | 
			
		||||
                    defaultValue as? Boolean ?: false
 | 
			
		||||
                ) as T
 | 
			
		||||
 | 
			
		||||
                Float::class -> return preferences.getFloat(key, defaultValue as? Float ?: -1f) as T
 | 
			
		||||
                Long::class -> return preferences.getLong(key, defaultValue as? Long ?: -1) as T
 | 
			
		||||
                else -> throw UnsupportedOperationException("Not yet implemented")
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: ClassCastException) {
 | 
			
		||||
            return defaultValue as T
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var booruUrl: String
 | 
			
		||||
        get () = PreferencesManager[BOORU_URL_KEY, "https://danbooru.donmai.us"]
 | 
			
		||||
        set(value) { PreferencesManager[BOORU_URL_KEY] = value }
 | 
			
		||||
    var booruType: String
 | 
			
		||||
        get () = PreferencesManager[BOORU_TYPE_KEY, "danbooru"]
 | 
			
		||||
    var booruType: @BooruType Int
 | 
			
		||||
        get () = PreferencesManager[BOORU_TYPE_KEY, DANBOORU]
 | 
			
		||||
        set(value) { PreferencesManager[BOORU_TYPE_KEY] = value }
 | 
			
		||||
    var booruTagSearch: String
 | 
			
		||||
        get () = PreferencesManager[BOORU_TAG_SEARCH_KEY, "ratio:16:9 rating:general order:random"]
 | 
			
		||||
| 
						 | 
				
			
			@ -99,4 +139,4 @@ object PreferencesManager {
 | 
			
		|||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,11 @@ import androidx.appcompat.content.res.AppCompatResources
 | 
			
		|||
import androidx.leanback.app.GuidedStepSupportFragment
 | 
			
		||||
import androidx.leanback.widget.GuidanceStylist.Guidance
 | 
			
		||||
import androidx.leanback.widget.GuidedAction
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.DANBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.GELBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.MOEBOORU
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.ZEROCHAN
 | 
			
		||||
import nl.kurocon.plugin.wallpaperprovider.booru.PreferencesManager.BooruType.Companion.WALLHAVEN
 | 
			
		||||
import kotlin.CharSequence
 | 
			
		||||
 | 
			
		||||
class SettingsFragment : GuidedStepSupportFragment() {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,20 +37,50 @@ class SettingsFragment : GuidedStepSupportFragment() {
 | 
			
		|||
            .build()
 | 
			
		||||
        actions.add(actionBooruUrl)
 | 
			
		||||
 | 
			
		||||
        // Booru Type setting (choice menu with only Danbooru currently)
 | 
			
		||||
        val booruTypeSubActions: MutableList<GuidedAction> = mutableListOf();
 | 
			
		||||
        val typeDanbooruSubAction = GuidedAction.Builder(context)
 | 
			
		||||
            .id(SUBACTION_ID_BOORU_TYPE_DANBOORU)
 | 
			
		||||
            .title(R.string.setting_booru_type_subtype_danbooru_title)
 | 
			
		||||
            .description(R.string.setting_booru_type_subtype_danbooru_description)
 | 
			
		||||
            .build()
 | 
			
		||||
        booruTypeSubActions.add(typeDanbooruSubAction)
 | 
			
		||||
        // Booru Type subaction list (choice menu)
 | 
			
		||||
        val booruTypeSubActions: MutableList<GuidedAction> = mutableListOf(
 | 
			
		||||
            // Danbooru
 | 
			
		||||
            GuidedAction.Builder(context)
 | 
			
		||||
                .id(SUBACTION_ID_BOORU_TYPE_DANBOORU)
 | 
			
		||||
                .title(R.string.setting_booru_type_subtype_danbooru_title)
 | 
			
		||||
                .description(R.string.setting_booru_type_subtype_danbooru_description)
 | 
			
		||||
                .build(),
 | 
			
		||||
 | 
			
		||||
            // Moebooru
 | 
			
		||||
            GuidedAction.Builder(context)
 | 
			
		||||
                .id(SUBACTION_ID_BOORU_TYPE_MOEBOORU)
 | 
			
		||||
                .title(R.string.setting_booru_type_subtype_moebooru_title)
 | 
			
		||||
                .description(R.string.setting_booru_type_subtype_moebooru_description)
 | 
			
		||||
                .build(),
 | 
			
		||||
 | 
			
		||||
            // Gelbooru
 | 
			
		||||
            GuidedAction.Builder(context)
 | 
			
		||||
                .id(SUBACTION_ID_BOORU_TYPE_GELBOORU)
 | 
			
		||||
                .title(R.string.setting_booru_type_subtype_gelbooru_title)
 | 
			
		||||
                .description(R.string.setting_booru_type_subtype_gelbooru_description)
 | 
			
		||||
                .build(),
 | 
			
		||||
 | 
			
		||||
            // Zerochan
 | 
			
		||||
            GuidedAction.Builder(context)
 | 
			
		||||
                .id(SUBACTION_ID_BOORU_TYPE_ZEROCHAN)
 | 
			
		||||
                .title(R.string.setting_booru_type_subtype_zerochan_title)
 | 
			
		||||
                .description(R.string.setting_booru_type_subtype_zerochan_description)
 | 
			
		||||
                .build(),
 | 
			
		||||
 | 
			
		||||
            // Wallhaven.cc
 | 
			
		||||
            GuidedAction.Builder(context)
 | 
			
		||||
                .id(SUBACTION_ID_BOORU_TYPE_WALLHAVEN)
 | 
			
		||||
                .title(R.string.setting_booru_type_subtype_wallhaven_title)
 | 
			
		||||
                .description(R.string.setting_booru_type_subtype_wallhaven_description)
 | 
			
		||||
                .build(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Actual Booru Type choice button with subactions
 | 
			
		||||
        val currentBooruType = PreferencesManager.booruType
 | 
			
		||||
        val actionBooruType = GuidedAction.Builder(context)
 | 
			
		||||
            .id(ACTION_ID_BOORU_TYPE)
 | 
			
		||||
            .title(R.string.setting_booru_type_title)
 | 
			
		||||
            .description(currentBooruType)
 | 
			
		||||
            .description(PreferencesManager.getBooruName(currentBooruType))
 | 
			
		||||
            .subActions(booruTypeSubActions)
 | 
			
		||||
            .build()
 | 
			
		||||
        actions.add(actionBooruType)
 | 
			
		||||
| 
						 | 
				
			
			@ -104,10 +139,13 @@ class SettingsFragment : GuidedStepSupportFragment() {
 | 
			
		|||
 | 
			
		||||
    override fun onSubGuidedActionClicked(action: GuidedAction): Boolean {
 | 
			
		||||
        when (action.id) {
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_DANBOORU -> {
 | 
			
		||||
                findActionById(ACTION_ID_BOORU_TYPE)?.description = "danbooru"
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_DANBOORU, SUBACTION_ID_BOORU_TYPE_MOEBOORU,
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_GELBOORU, SUBACTION_ID_BOORU_TYPE_ZEROCHAN,
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_WALLHAVEN -> {
 | 
			
		||||
                val bType = BOORU_TYPE_ACTION_MAP[action.id]!!
 | 
			
		||||
                findActionById(ACTION_ID_BOORU_TYPE)?.description = PreferencesManager.getBooruName(bType)
 | 
			
		||||
                notifyActionChanged(findActionPositionById(ACTION_ID_BOORU_TYPE))
 | 
			
		||||
                PreferencesManager.booruType = "danbooru"
 | 
			
		||||
                PreferencesManager.booruType = bType
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
| 
						 | 
				
			
			@ -151,5 +189,17 @@ class SettingsFragment : GuidedStepSupportFragment() {
 | 
			
		|||
        private const val ACTION_ID_BOORU_API_KEY = 6L
 | 
			
		||||
 | 
			
		||||
        private const val SUBACTION_ID_BOORU_TYPE_DANBOORU = 7L
 | 
			
		||||
        private const val SUBACTION_ID_BOORU_TYPE_MOEBOORU = 8L
 | 
			
		||||
        private const val SUBACTION_ID_BOORU_TYPE_GELBOORU = 9L
 | 
			
		||||
        private const val SUBACTION_ID_BOORU_TYPE_ZEROCHAN = 10L
 | 
			
		||||
        private const val SUBACTION_ID_BOORU_TYPE_WALLHAVEN = 11L
 | 
			
		||||
 | 
			
		||||
        private val BOORU_TYPE_ACTION_MAP = mapOf(
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_DANBOORU to DANBOORU,
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_MOEBOORU to MOEBOORU,
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_GELBOORU to GELBOORU,
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_ZEROCHAN to ZEROCHAN,
 | 
			
		||||
            SUBACTION_ID_BOORU_TYPE_WALLHAVEN to WALLHAVEN,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
<resources>
 | 
			
		||||
    <string name="plugin_name">Booru Wallpaper Provider</string>
 | 
			
		||||
    <string name="plugin_version">1.1</string>
 | 
			
		||||
    <string name="plugin_description">
 | 
			
		||||
        A wallpaper provider that can talk to Booru sites running Danbooru software.
 | 
			
		||||
    </string>
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +9,7 @@
 | 
			
		|||
    <!-- Setting parameters -->
 | 
			
		||||
    <string name="settings">Settings</string>
 | 
			
		||||
    <string name="setting_image_url_title">Image URL</string>
 | 
			
		||||
    <string name="setting_booru_url_title">Booru URL - e.g. \'https://danbooru.donmai.us/\'</string>
 | 
			
		||||
    <string name="setting_booru_url_title">Booru URL - e.g. \'https://danbooru.donmai.us\'</string>
 | 
			
		||||
    <string name="setting_booru_type_title">Booru Type</string>
 | 
			
		||||
    <string name="setting_booru_tag_search_title">Search query - e.g. \'ratio:16:9 rating:general\'</string>
 | 
			
		||||
    <string name="setting_booru_user_id_title">Booru Username </string>
 | 
			
		||||
| 
						 | 
				
			
			@ -20,5 +21,13 @@
 | 
			
		|||
    </string>
 | 
			
		||||
 | 
			
		||||
    <string name="setting_booru_type_subtype_danbooru_title">Danbooru</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_danbooru_description">Danbooru-based (danbooru.donmai.us) - Currently the only supported type</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_danbooru_description">Danbooru-based (danbooru.donmai.us)</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_moebooru_title">Moebooru (Danbooru v1)</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_moebooru_description">Moebooru-based (konachan.com, yande.re)</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_gelbooru_title">Gelbooru</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_gelbooru_description">Gelbooru-based (gelbooru.com)</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_zerochan_title">Zerochan</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_zerochan_description">Zerochan-based (zerochan.net, asiachan.com)</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_wallhaven_title">Wallhaven</string>
 | 
			
		||||
    <string name="setting_booru_type_subtype_wallhaven_description">Wallhaven-based (wallhaven.cc)</string>
 | 
			
		||||
</resources>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue