mirror of
https://github.com/Kurocon/projectivy-booru-wallpaper-provider.git
synced 2025-06-04 23:20:20 +00: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
42
README.md
42
README.md
|
@ -5,7 +5,12 @@ This is a [Projectivy](https://xdaforums.com/t/app-android-tv-projectivy-launche
|
|||
## Repository layout
|
||||
- /booru : code for the Booru plugin service and its setting activity
|
||||
- /api : api used to communicate with Projectivy through AIDL
|
||||
|
||||
|
||||
# Screenshots
|
||||

|
||||
|
||||

|
||||
|
||||
# Usage
|
||||
- Download the `booru-release.apk` from the releases tab and install it on your TV
|
||||
- Navigate to Projectivy -> Launcher settings -> Appearance -> Wallpaper -> Launcher wallpaper
|
||||
|
@ -16,10 +21,39 @@ This is a [Projectivy](https://xdaforums.com/t/app-android-tv-projectivy-launche
|
|||
- Search query: The search query from which images are pulled. This is the same as you would fill in on the imageboard website's search box. Check the help pages of your booru for guidance. (i.e. the [cheatsheet](https://danbooru.donmai.us/wiki_pages/help%3Acheatsheet) for Danbooru) All tag types should work normally (including ordering, ratio, and other metatags).
|
||||
- Username and API Key: These are optional. Some boorus have limitations on how many tags can be searched anonymously or by standard users. For example, Danbooru allows 2 tags for logged out and normal users, but 6 for premium accounts. Fill in these fields to authenticate.
|
||||
|
||||
# Screenshots
|
||||

|
||||
## Settings for common boorus
|
||||
| Booru | Type | URL | Query guide | Username | API Key |
|
||||
|-----------------------------------------------------------------------------------|-----------|----------------------------------------------------------------------------|---------------------------------------------------------------------------|--------------------|----------------------------------------------------------------------------------------|
|
||||
| [Danbooru](https://danbooru.donmai.us) <br>Anime, NSFW and SFW variants | Danbooru | `https://danbooru.donmai.us` (NSFW)<br>`https://safebooru.donmai.us` (SFW) | [cheatsheet](https://danbooru.donmai.us/wiki_pages/help%3Acheatsheet) | Optional, username | Optional, API key. Found on [profile](https://danbooru.donmai.us/profile) |
|
||||
| [Konachan](https://konachan.com/) <br>Anime wallpapers, NSFW and SFW variants | Moebooru | `https://konachan.com` (NSFW)<br>`https://konachan.net` (SFW) | [cheatsheet](https://konachan.net/help/cheatsheet) | Optional, username | Optional, hashed password. See [api docs, `Logging in`](https://konachan.net/help/api) |
|
||||
| [Yande.re](https://yande.re) <br>Anime, contains NSFW | Moebooru | `https://yande.re` | [cheatsheet](https://yande.re/wiki/show?title=cheat_sheet_extended) | Optional, username | Optional, hashed password. See [api docs, `Logging in`](https://yande.re/help/api) |
|
||||
| [Gelbooru](https://gelbooru.com) <br>Anime, contains NSFW | Gelbooru | `https://gelbooru.com` | [cheatsheet](https://gelbooru.com/index.php?page=wiki&s=&s=view&id=26263) | Optional, username | Optional, API key. Found in account options |
|
||||
| [Safebooru](https://safebooru.org) <br>Anime, SFW | Gelbooru | `https://safebooru.org` | See `Gelbooru` | Optional, username | Optional, API key. Found in account options |
|
||||
| [Zerochan](https://www.zerochan.net) (*) <br>Anime, SFW | Zerochan | `https://www.zerochan.net` | Tag names, comma separated. I.e. `Genshin Impact,Lumine` | Optional, username | *empty* |
|
||||
| [Asiachan](https://kpop.asiachan.com) (*) <br>IRL, K-Pop, SFW | Zerochan | `https://kpop.asiachan.com` | Tag names, comma separated. I.e. `BTS,Park Jimin` | Optional, username | *empty* |
|
||||
| [Wallhaven](https://wallhaven.cc) <br>General wallpapers, SFW (NSFW with API key) | Wallhaven | `https://wallhaven.cc` | [API docs](https://wallhaven.cc/help/api#search) | *empty* | Optional, found in account settings |
|
||||
|
||||

|
||||
**(*)** Support for Zerochan-type boards is pretty unstable due to how these sites handle requests. Don't expect these to be very stable/usable.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Other known boorus by type</summary>
|
||||
|
||||
### Other known boorus by type
|
||||
These are some other boorus listed by their software, sorted into 'SFW' and 'NSFW'.
|
||||
Take the 'SFW' tag with a grain of salt, you know how the internet works...
|
||||
|
||||
| Content rating | Type | Known boorus |
|
||||
|----------------|--------------------------|----------------------------------------------------------------------------------------|
|
||||
| SFW | Danbooru | `yukkuri.shiteitte.net`, `e926.net` |
|
||||
| | Moebooru | `sakugabooru.com`, `img.genshiken-itb.org` |
|
||||
| | Gelbooru | `*.booru.org (some)` |
|
||||
| NSFW | Danbooru | `yukkuri.shiteitte.net`, `booru.allthefallen.moe`, `e621.net` |
|
||||
| | Gelbooru | `*.booru.org`, `xbooru.com`, `rule34.xxx`, `tbib.org`, `hypnohub.net`, `realbooru.com` |
|
||||
| | Shimmie2 *(unsupported)* | `rule34.paheal.net`, `rule34hentai.net`, `fanservice.fan` |
|
||||
| | Sankaku *(unsupported)* | `chan.sankakucomplex.com`, `idol.sankakucomplex.com` |
|
||||
|
||||
</details>
|
||||
|
||||
# Note
|
||||
This plugin is provided as an open-source project and is distributed "as is." While the author may offer voluntary support, there is no guarantee of availability or resolution. The author is not responsible for any damages, data loss, or issues arising from the use of this plugin. Use at your own risk.
|
||||
|
|
|
@ -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…
Reference in a new issue