mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
itch.io: browse the Store tab by sort and price, with paging and a disk cache (#1473)
## What this does The itch.io Store tab used to show only four fixed shelves. This is the first of a series of PRs that rework it. It adds real browsing: pick an order and a price filter, and the tab loads itch's own listing for that choice and keeps loading as you scroll. Login, the library, downloads and installs are unchanged. Platform and tag filters, the game page with the install picker, updates and collections are not in this PR. They come in the following ones. ## How it works itch serves every listing page as an RSS feed if you add `.xml` to the address. The app already reads those feeds for the shelves, so browsing reuses the same parser (`ItchShelves.parse`). Nothing here needs the API key. The address is built the way itch builds its own links: sort, then price, then `platform-<name>`, then `tag-<slug>`. | Choice | Address | |---|---| | Popular (default) | `https://itch.io/games` | | New & popular | `/games/new-and-popular` | | Top sellers | `/games/top-sellers` | | Top rated | `/games/top-rated` | | Most recent | `/games/newest` | | Free | `/games/free` | | On sale | `/games/on-sale` | | Paid | `/games/store` | | $5 or less | `/games/5-dollars-or-less` | | $15 or less | `/games/15-dollars-or-less` | Choices combine, for example `/games/top-rated/free`, then `.xml` for the feed. Page 2 and later add `?page=N`. ## Changes **`stores/itch/ItchBrowse.kt` (new)** - `BrowseQuery` holds sort, price, platforms and tags. The default query is the front page, which keeps the existing shelves. - `ItchBrowse.path` and `url` build the addresses above. Only the spellings listed above are used. Platforms and tags are already accepted by the builder, ready for the next PR. - `ItchBrowse.page` loads one page of results: - Disk cache under `cacheDir/itch-browse`, keyed by the SHA-1 of the address, fresh for 3 hours. - Requests are made one at a time, at least 1.2 s apart. - A 429 answer waits 8 s and tries once more. - Each request is written to the store log (address, status, number of games). - `ItchBrowseState` is the Compose state for the tab: the query, the games loaded so far, and the loading, ended and failed flags. A generation counter discards answers that arrive after the user has changed the query. Paging skips games it has already shown. **`ui/ItchBrowseBar.kt` (new)** - A sort chip row and a price chip row, plus a load-more trigger at the end of the grid. - Chips are selectable, work with the controller (`controllerConfirm`) and use the same glide border as the rest of the app. **`ui/StoresPage.kt`** - The storefront shows the chip bar for itch.io and, when a non-default choice is active, the browse results in the existing grid instead of the shelves. - Games you already own are marked as owned in the results. - Empty and failed states have their own messages. **`stores/itch/ItchShelves.kt`** - `open(url)` is now `internal` so browsing can reuse it. **`stores/StoresState.kt`** - `detailItem` also looks in the browse results for itch.io, so tapping a result opens its detail page. **Strings** - 13 new strings (sort names, price names, "Load more", "Couldn't load games"), translated in all 33 locales. **Docs and tests** - `docs/development/itch-browse.md` explains the address scheme, the cache and the paging. - `ItchBrowseTest` checks the addresses the code builds against the ones listed above. ## Testing I checked every address with `.xml` added, and all of them load without errors: - Every sort, every price filter and every platform on its own. - Combinations: `top-rated/free`, `top-rated/on-sale`, `top-rated/store`, `top-rated/5-dollars-or-less`, `top-rated/free/platform-windows` and `top-rated/free/platform-windows/tag-pixel-art`. - Paging: `?page=2` and `top-rated.xml?page=2`. The unit test locks those addresses in. ## Not in this PR - Platform and tag filters in the UI, and search. - The game page, the version picker and install for free games. - Updates for installed games and itch collections. - Android builds will link out to the itch.io page. Butler patching comes last, as an add-on pack.
This commit is contained in:
40 files changed
+840
-8
No files matched your search
@@ -126,7 +126,9 @@ object StoresState {
|
||||
fun installedGame(store: Store, id: String): InstalledStoreGame? = installed.firstOrNull { it.sidecar.store == store && it.sidecar.id == id }
|
||||
|
||||
fun detailItem(store: Store, id: String): CatalogItem? = detailCatalogItem(
|
||||
store, id, library[store].orEmpty(), found[store].orEmpty(), shelves[store],
|
||||
store, id, library[store].orEmpty(),
|
||||
found[store].orEmpty() + if (store == Store.ITCH) com.droiddeck.launcher.stores.itch.ItchBrowseState.items else emptyList(),
|
||||
shelves[store],
|
||||
)
|
||||
|
||||
fun open(context: Context, store: Store, force: Boolean = false) {
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.droiddeck.launcher.stores.itch
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.stores.CatalogItem
|
||||
import com.droiddeck.launcher.stores.StoresState
|
||||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
internal data class BrowseQuery(
|
||||
val sort: String = ItchBrowse.SORT_POPULAR,
|
||||
val price: String = ItchBrowse.PRICE_ANY,
|
||||
val platforms: Set<String> = emptySet(),
|
||||
val tags: List<String> = emptyList(),
|
||||
) {
|
||||
val isDefault: Boolean get() = this == BrowseQuery()
|
||||
}
|
||||
|
||||
internal class BrowsePage(val items: List<CatalogItem>, val code: Int, val cached: Boolean)
|
||||
|
||||
internal object ItchBrowse {
|
||||
const val SORT_POPULAR = "popular"
|
||||
const val SORT_NEW_POPULAR = "new-and-popular"
|
||||
const val SORT_TOP_SELLERS = "top-sellers"
|
||||
const val SORT_TOP_RATED = "top-rated"
|
||||
const val SORT_NEWEST = "newest"
|
||||
val sorts = listOf(SORT_POPULAR, SORT_NEW_POPULAR, SORT_TOP_SELLERS, SORT_TOP_RATED, SORT_NEWEST)
|
||||
|
||||
const val PRICE_ANY = "any"
|
||||
const val PRICE_FREE = "free"
|
||||
const val PRICE_SALE = "on-sale"
|
||||
const val PRICE_PAID = "store"
|
||||
const val PRICE_UNDER_5 = "5-dollars-or-less"
|
||||
const val PRICE_UNDER_15 = "15-dollars-or-less"
|
||||
val prices = listOf(PRICE_ANY, PRICE_FREE, PRICE_SALE, PRICE_PAID, PRICE_UNDER_5, PRICE_UNDER_15)
|
||||
|
||||
val platformNames = listOf("windows", "osx", "linux", "android", "ios", "web", "mobile-web")
|
||||
|
||||
private const val BASE = "https://itch.io/games"
|
||||
private const val FRESH_MS = 3 * 60 * 60 * 1000L
|
||||
private const val GAP_MS = 1_200L
|
||||
private const val RETRY_MS = 8_000L
|
||||
private const val FOLDER = "itch-browse"
|
||||
|
||||
@Volatile private var lastRequestAt = 0L
|
||||
private val lock = Any()
|
||||
|
||||
private fun sortSegment(sort: String): String? = sort.takeIf { it != SORT_POPULAR && it in sorts }
|
||||
|
||||
private fun priceSegment(price: String): String? = price.takeIf { it != PRICE_ANY && it in prices }
|
||||
|
||||
fun path(query: BrowseQuery): String {
|
||||
val segments = listOfNotNull(sortSegment(query.sort), priceSegment(query.price)) +
|
||||
query.platforms.sorted().map { "platform-$it" } +
|
||||
query.tags.map { "tag-" + tagSlug(it) }
|
||||
return if (segments.isEmpty()) BASE else BASE + "/" + segments.joinToString("/")
|
||||
}
|
||||
|
||||
fun url(query: BrowseQuery, page: Int): String = path(query) + ".xml" + if (page > 1) "?page=$page" else ""
|
||||
|
||||
fun tagSlug(tag: String): String = tag.trim().lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-')
|
||||
|
||||
fun page(context: Context, query: BrowseQuery, page: Int, force: Boolean = false): BrowsePage {
|
||||
val app = context.applicationContext
|
||||
val url = url(query, page)
|
||||
if (!force) read(app, url)?.let { return BrowsePage(it, 200, true) }
|
||||
val (status, body) = fetch(url)
|
||||
val items = if (status in 200..299 && body != null) ItchShelves.parse(body) else emptyList()
|
||||
StoresState.logLine("itch.io browse: $url answered $status with ${items.size} games")
|
||||
if (items.isNotEmpty()) write(app, url, items)
|
||||
return BrowsePage(items, status, false)
|
||||
}
|
||||
|
||||
private fun fetch(url: String): Pair<Int, String?> {
|
||||
synchronized(lock) {
|
||||
pause()
|
||||
var answer = ItchShelves.open(url)
|
||||
if (answer.first == 429) {
|
||||
StoresState.logLine("itch.io browse: 429, waiting before one more try")
|
||||
Thread.sleep(RETRY_MS)
|
||||
pause()
|
||||
answer = ItchShelves.open(url)
|
||||
}
|
||||
lastRequestAt = System.currentTimeMillis()
|
||||
return answer
|
||||
}
|
||||
}
|
||||
|
||||
private fun pause() {
|
||||
val gap = GAP_MS - (System.currentTimeMillis() - lastRequestAt)
|
||||
if (gap > 0) Thread.sleep(gap)
|
||||
}
|
||||
|
||||
private fun file(context: Context, url: String): File {
|
||||
val digest = MessageDigest.getInstance("SHA-1").digest(url.toByteArray()).joinToString("") { "%02x".format(it) }
|
||||
return File(File(context.cacheDir, FOLDER), "$digest.json")
|
||||
}
|
||||
|
||||
private fun read(context: Context, url: String): List<CatalogItem>? {
|
||||
val file = file(context, url)
|
||||
if (!file.isFile || System.currentTimeMillis() - file.lastModified() > FRESH_MS) return null
|
||||
return runCatching { ItchCatalog.list(JSONArray(file.readText())) }.getOrNull()?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun write(context: Context, url: String, items: List<CatalogItem>) {
|
||||
val file = file(context, url)
|
||||
runCatching {
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeText(ItchCatalog.array(items).toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal object ItchBrowseState {
|
||||
var query by mutableStateOf(BrowseQuery())
|
||||
private set
|
||||
var items by mutableStateOf<List<CatalogItem>>(emptyList())
|
||||
private set
|
||||
var loading by mutableStateOf(false)
|
||||
private set
|
||||
var ended by mutableStateOf(false)
|
||||
private set
|
||||
var failed by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private var next = 1
|
||||
private var generation = 0
|
||||
|
||||
val active: Boolean get() = !query.isDefault
|
||||
|
||||
fun choose(context: Context, wanted: BrowseQuery) {
|
||||
if (wanted == query) return
|
||||
generation++
|
||||
query = wanted
|
||||
items = emptyList()
|
||||
ended = false
|
||||
failed = false
|
||||
loading = false
|
||||
next = 1
|
||||
if (active) more(context)
|
||||
}
|
||||
|
||||
fun more(context: Context, retry: Boolean = false) {
|
||||
if (loading || ended || !active || (failed && !retry)) return
|
||||
loading = true
|
||||
failed = false
|
||||
val app = context.applicationContext
|
||||
val mine = generation
|
||||
val asked = query
|
||||
val page = next
|
||||
Thread({
|
||||
val answer = runCatching { ItchBrowse.page(app, asked, page) }.getOrNull()
|
||||
StoresState.post {
|
||||
if (mine != generation) return@post
|
||||
loading = false
|
||||
if (answer == null || answer.code !in 200..299) {
|
||||
failed = true
|
||||
return@post
|
||||
}
|
||||
if (answer.items.isEmpty()) {
|
||||
ended = true
|
||||
return@post
|
||||
}
|
||||
val known = items.mapTo(HashSet()) { it.id }
|
||||
val fresh = answer.items.filter { known.add(it.id) }
|
||||
if (fresh.isEmpty()) ended = true else {
|
||||
items = items + fresh
|
||||
next = page + 1
|
||||
}
|
||||
}
|
||||
}, "itch-browse").start()
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ internal object ItchShelves {
|
||||
return shelves.takeUnless { it.isEmpty }
|
||||
}
|
||||
|
||||
private fun open(url: String): Pair<Int, String?> {
|
||||
internal fun open(url: String): Pair<Int, String?> {
|
||||
var conn: HttpURLConnection? = null
|
||||
return try {
|
||||
conn = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.stores.itch.ItchBrowse
|
||||
import com.droiddeck.launcher.stores.itch.ItchBrowseState
|
||||
|
||||
@Composable
|
||||
internal fun ItchBrowseBar() {
|
||||
val ctx = LocalContext.current
|
||||
val query = ItchBrowseState.query
|
||||
Column(Modifier.fillMaxWidth().padding(top = 6.dp, bottom = 8.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
ChipRow {
|
||||
for (sort in ItchBrowse.sorts) BrowseChip(sortLabel(sort), query.sort == sort) { ItchBrowseState.choose(ctx, query.copy(sort = sort)) }
|
||||
}
|
||||
ChipRow {
|
||||
for (price in ItchBrowse.prices) BrowseChip(priceLabel(price), query.price == price) { ItchBrowseState.choose(ctx, query.copy(price = price)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ItchLoadMore() {
|
||||
val ctx = LocalContext.current
|
||||
val count = ItchBrowseState.items.size
|
||||
LaunchedEffect(Unit) { ItchBrowseState.more(ctx) }
|
||||
if (ItchBrowseState.loading) LinearProgressIndicator(Modifier.fillMaxWidth().padding(vertical = 12.dp))
|
||||
else if (ItchBrowseState.failed) Box(Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) {
|
||||
BrowseChip(stringResource(R.string.stores_dl_retry), false) { ItchBrowseState.more(ctx, retry = true) }
|
||||
}
|
||||
else if (!ItchBrowseState.ended && count > 0) Box(Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) {
|
||||
BrowseChip(stringResource(R.string.stores_itch_load_more), false) { ItchBrowseState.more(ctx) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChipRow(content: @Composable () -> Unit) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).selectableGroup(),
|
||||
) { content() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BrowseChip(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = rememberHot(src)
|
||||
Text(
|
||||
label, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1,
|
||||
color = if (selected) pal.onSignal else if (hot) colors.onBackground else colors.onSurfaceVariant,
|
||||
modifier = Modifier.clip(shape).background(if (selected) pal.signal else colors.surfaceVariant)
|
||||
.glideBorder(hot, shape, pal.signal)
|
||||
.hoverable(src).selectable(selected = selected, interactionSource = src, indication = LocalIndication.current, role = Role.Tab, onClick = onClick)
|
||||
.controllerConfirm(onClick = onClick)
|
||||
.padding(horizontal = 12.dp, vertical = 7.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun sortLabel(sort: String): String = stringResource(
|
||||
when (sort) {
|
||||
ItchBrowse.SORT_NEW_POPULAR -> R.string.stores_itch_sort_new_popular
|
||||
ItchBrowse.SORT_TOP_SELLERS -> R.string.stores_itch_sort_top_sellers
|
||||
ItchBrowse.SORT_TOP_RATED -> R.string.stores_itch_sort_top_rated
|
||||
ItchBrowse.SORT_NEWEST -> R.string.stores_itch_sort_new
|
||||
else -> R.string.stores_itch_sort_popular
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun priceLabel(price: String): String = stringResource(
|
||||
when (price) {
|
||||
ItchBrowse.PRICE_FREE -> R.string.stores_itch_price_free
|
||||
ItchBrowse.PRICE_SALE -> R.string.stores_itch_price_sale
|
||||
ItchBrowse.PRICE_PAID -> R.string.stores_itch_price_paid
|
||||
ItchBrowse.PRICE_UNDER_5 -> R.string.stores_itch_price_under_5
|
||||
ItchBrowse.PRICE_UNDER_15 -> R.string.stores_itch_price_under_15
|
||||
else -> R.string.stores_itch_price_any
|
||||
},
|
||||
)
|
||||
@@ -146,6 +146,7 @@ import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.stores.CatalogItem
|
||||
import com.droiddeck.launcher.stores.itch.ItchBrowseState
|
||||
import com.droiddeck.launcher.stores.Store
|
||||
import com.droiddeck.launcher.stores.StoreTab
|
||||
import com.droiddeck.launcher.stores.StoresState
|
||||
@@ -961,7 +962,8 @@ private fun Storefront(
|
||||
val everything = remember(library, shelves, found) { (found + library + (shelves?.all ?: emptyList())).distinctBy { it.id } }
|
||||
LaunchedEffect(store, q) { StoresState.search(ctx, store, q) }
|
||||
val loaded = StoresState.library.containsKey(store)
|
||||
val showGrid = tab != "store" || q.isNotEmpty()
|
||||
val browsing = store == Store.ITCH && tab == "store" && q.isEmpty() && ItchBrowseState.active
|
||||
val showGrid = tab != "store" || q.isNotEmpty() || browsing
|
||||
val sort = s.storesSort
|
||||
val lastPlayedFor = remember(s.steamGames) {
|
||||
val byKey = s.steamGames.associate { "${it.source}:${it.storeId}" to it.lastPlayed }
|
||||
@@ -974,8 +976,13 @@ private fun Storefront(
|
||||
lookup
|
||||
}
|
||||
val isSizeSort = tab == "library" && (sort == "sizeAsc" || sort == "sizeDesc")
|
||||
val candidates = remember(library, everything, tab, q, s.storesShowMature) {
|
||||
if (tab == "library") library.filter(::matches) else everything.filter { matches(it) && shown(it) }
|
||||
val browseItems = if (browsing) ItchBrowseState.items else emptyList()
|
||||
val candidates = remember(library, everything, tab, q, s.storesShowMature, browsing, browseItems) {
|
||||
if (tab == "library") library.filter(::matches)
|
||||
else if (browsing) {
|
||||
val owned = library.associateBy { it.id }
|
||||
browseItems.map { owned[it.id] ?: it }.filter(::shown)
|
||||
} else everything.filter { matches(it) && shown(it) }
|
||||
}
|
||||
val candidateKeys = remember(candidates) { candidates.map { it.key } }
|
||||
var resolvedSizeSortKey by remember(store, tab) { mutableStateOf<List<String>?>(null) }
|
||||
@@ -1013,6 +1020,8 @@ private fun Storefront(
|
||||
}
|
||||
val empty = stringResource(when {
|
||||
tab == "library" && library.isEmpty() && StoresState.status[store] != null -> R.string.stores_library_loading
|
||||
browsing && ItchBrowseState.loading -> R.string.stores_shelves_loading
|
||||
browsing && ItchBrowseState.failed -> R.string.stores_itch_browse_failed
|
||||
else -> R.string.stores_nothing_matches
|
||||
})
|
||||
var firstPlaced = false
|
||||
@@ -1023,8 +1032,9 @@ private fun Storefront(
|
||||
// A bar only where the tab has nothing else on it: shelves, the shelf of owned games, or a grid still loading.
|
||||
val gridEmpty = if (showGrid) visible.isEmpty() else (shelves == null || shelves.isEmpty) && library.isEmpty()
|
||||
val barLine = StoresState.status[store]?.takeIf { gridEmpty && syncCount(it) != null }
|
||||
val itchBar = store == Store.ITCH && tab == "store" && q.isEmpty()
|
||||
val headerItems = 1 + (if (sizeSortPending) 1 else 0) + (if (barLine != null) 1 else 0) +
|
||||
(if (StoresState.problems[store] != null) 1 else 0)
|
||||
(if (StoresState.problems[store] != null) 1 else 0) + (if (itchBar) 1 else 0)
|
||||
val listMode = s.storesView == "list" && tab == "library"
|
||||
val capsules = s.storesView == "capsules" && tab == "library"
|
||||
val tileWidth = if (capsules) CapsuleWidth else CardWidth
|
||||
@@ -1110,6 +1120,7 @@ private fun Storefront(
|
||||
}
|
||||
barLine?.let { line -> item(key = "sync", span = { GridItemSpan(maxLineSpan) }) { Rise(3) { SyncBar(line) } } }
|
||||
StoresState.problems[store]?.let { line -> item(key = "problem", span = { GridItemSpan(maxLineSpan) }) { Rise(3) { Box(Modifier.padding(bottom = 8.dp)) { Note(line) } } } }
|
||||
if (itchBar) item(key = "itch-browse-bar", span = { GridItemSpan(maxLineSpan) }) { ItchBrowseBar() }
|
||||
when (tab) {
|
||||
"library" -> if (!loaded) Unit
|
||||
else if (split == 0) storeGrid(visible, empty, card)
|
||||
@@ -1120,8 +1131,10 @@ private fun Storefront(
|
||||
storeGrid(visible.drop(split), empty, card)
|
||||
}
|
||||
else -> {
|
||||
if (q.isNotEmpty()) storeGrid(visible, empty, card)
|
||||
else {
|
||||
if (q.isNotEmpty() || browsing) {
|
||||
storeGrid(visible, empty, card)
|
||||
if (browsing) item(key = "itch-more", span = { GridItemSpan(maxLineSpan) }) { ItchLoadMore() }
|
||||
} else {
|
||||
item(key = "shelves", span = { GridItemSpan(maxLineSpan) }) {
|
||||
Column {
|
||||
Shelf(stringResource(R.string.stores_shelf_new), shelf(shelves?.whatsNew), card)
|
||||
|
||||
@@ -2088,6 +2088,19 @@
|
||||
<string name="stores_itch_no_exe">لم يتم العثور على لعبة Windows في التنزيل</string>
|
||||
<string name="stores_itch_extract_failed">تعذر استخراج التنزيل (رمز الخروج %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">لم يستجب itch.io</string>
|
||||
<string name="stores_itch_sort_popular">الأكثر شعبية</string>
|
||||
<string name="stores_itch_sort_new">الأحدث</string>
|
||||
<string name="stores_itch_sort_new_popular">جديد ورائج</string>
|
||||
<string name="stores_itch_price_paid">مدفوع</string>
|
||||
<string name="stores_itch_price_under_5">5 دولارات أو أقل</string>
|
||||
<string name="stores_itch_price_under_15">15 دولارًا أو أقل</string>
|
||||
<string name="stores_itch_sort_top_rated">الأعلى تقييمًا</string>
|
||||
<string name="stores_itch_sort_top_sellers">الأكثر مبيعًا</string>
|
||||
<string name="stores_itch_price_any">أي سعر</string>
|
||||
<string name="stores_itch_price_free">مجاني</string>
|
||||
<string name="stores_itch_price_sale">مخفّض</string>
|
||||
<string name="stores_itch_load_more">تحميل المزيد</string>
|
||||
<string name="stores_itch_browse_failed">تعذّر تحميل الألعاب من itch.io</string>
|
||||
<string name="stores_itch_archive">أرشيفات %1$s غير مدعومة</string>
|
||||
<string name="stores_itch_download_failed">استجاب itch.io بـ HTTP %1$d</string>
|
||||
<string name="stores_free">مجاني</string>
|
||||
|
||||
@@ -2772,6 +2772,19 @@
|
||||
<string name="stores_itch_no_exe">No se encontró un juego de Windows en la descarga</string>
|
||||
<string name="stores_itch_extract_failed">No se pudo extraer la descarga (salida %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io no respondió</string>
|
||||
<string name="stores_itch_sort_popular">Populares</string>
|
||||
<string name="stores_itch_sort_new">Más recientes</string>
|
||||
<string name="stores_itch_sort_new_popular">Nuevos y populares</string>
|
||||
<string name="stores_itch_price_paid">De pago</string>
|
||||
<string name="stores_itch_price_under_5">$5 o menos</string>
|
||||
<string name="stores_itch_price_under_15">$15 o menos</string>
|
||||
<string name="stores_itch_sort_top_rated">Mejor valorados</string>
|
||||
<string name="stores_itch_sort_top_sellers">Más vendidos</string>
|
||||
<string name="stores_itch_price_any">Cualquier precio</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">En oferta</string>
|
||||
<string name="stores_itch_load_more">Cargar más</string>
|
||||
<string name="stores_itch_browse_failed">No se pudieron cargar los juegos de itch.io</string>
|
||||
<string name="stores_itch_archive">Los archivos %1$s no son compatibles</string>
|
||||
<string name="stores_itch_download_failed">itch.io respondió HTTP %1$d</string>
|
||||
<string name="glasses_experimental">Cámaras Luma Ultra — Experimental</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">В изтеглянето не е намерена игра за Windows</string>
|
||||
<string name="stores_itch_extract_failed">Изтеглянето не можа да бъде разархивирано (код %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io не отговори</string>
|
||||
<string name="stores_itch_sort_popular">Популярни</string>
|
||||
<string name="stores_itch_sort_new">Най-нови</string>
|
||||
<string name="stores_itch_sort_new_popular">Нови и популярни</string>
|
||||
<string name="stores_itch_price_paid">Платени</string>
|
||||
<string name="stores_itch_price_under_5">До $5</string>
|
||||
<string name="stores_itch_price_under_15">До $15</string>
|
||||
<string name="stores_itch_sort_top_rated">Най-високо оценени</string>
|
||||
<string name="stores_itch_sort_top_sellers">Най-продавани</string>
|
||||
<string name="stores_itch_price_any">Всякаква цена</string>
|
||||
<string name="stores_itch_price_free">Безплатни</string>
|
||||
<string name="stores_itch_price_sale">С намаление</string>
|
||||
<string name="stores_itch_load_more">Зареди още</string>
|
||||
<string name="stores_itch_browse_failed">Игрите от itch.io не можаха да се заредят</string>
|
||||
<string name="stores_itch_archive">Архивите %1$s не се поддържат</string>
|
||||
<string name="stores_itch_download_failed">itch.io отговори HTTP %1$d</string>
|
||||
<string name="stores_free">Безплатна</string>
|
||||
|
||||
@@ -2046,6 +2046,19 @@
|
||||
<string name="stores_itch_no_exe">Ve stahování nebyla nalezena hra pro Windows</string>
|
||||
<string name="stores_itch_extract_failed">Stažený soubor nelze rozbalit (kód %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io neodpovědělo</string>
|
||||
<string name="stores_itch_sort_popular">Populární</string>
|
||||
<string name="stores_itch_sort_new">Nejnovější</string>
|
||||
<string name="stores_itch_sort_new_popular">Nové a populární</string>
|
||||
<string name="stores_itch_price_paid">Placené</string>
|
||||
<string name="stores_itch_price_under_5">Do $5</string>
|
||||
<string name="stores_itch_price_under_15">Do $15</string>
|
||||
<string name="stores_itch_sort_top_rated">Nejlépe hodnocené</string>
|
||||
<string name="stores_itch_sort_top_sellers">Nejprodávanější</string>
|
||||
<string name="stores_itch_price_any">Jakákoli cena</string>
|
||||
<string name="stores_itch_price_free">Zdarma</string>
|
||||
<string name="stores_itch_price_sale">Ve slevě</string>
|
||||
<string name="stores_itch_load_more">Načíst další</string>
|
||||
<string name="stores_itch_browse_failed">Hry z itch.io se nepodařilo načíst</string>
|
||||
<string name="stores_itch_archive">Archivy %1$s nejsou podporovány</string>
|
||||
<string name="stores_itch_download_failed">itch.io odpovědělo HTTP %1$d</string>
|
||||
<string name="stores_free">Zdarma</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">Der blev ikke fundet et Windows-spil i downloaden</string>
|
||||
<string name="stores_itch_extract_failed">Downloaden kunne ikke udpakkes (kode %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io svarede ikke</string>
|
||||
<string name="stores_itch_sort_popular">Populære</string>
|
||||
<string name="stores_itch_sort_new">Seneste</string>
|
||||
<string name="stores_itch_sort_new_popular">Nye og populære</string>
|
||||
<string name="stores_itch_price_paid">Betalt</string>
|
||||
<string name="stores_itch_price_under_5">$5 eller mindre</string>
|
||||
<string name="stores_itch_price_under_15">$15 eller mindre</string>
|
||||
<string name="stores_itch_sort_top_rated">Bedst bedømte</string>
|
||||
<string name="stores_itch_sort_top_sellers">Mest solgte</string>
|
||||
<string name="stores_itch_price_any">Alle priser</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">På tilbud</string>
|
||||
<string name="stores_itch_load_more">Indlæs flere</string>
|
||||
<string name="stores_itch_browse_failed">Kunne ikke hente spil fra itch.io</string>
|
||||
<string name="stores_itch_archive">%1$s-arkiver understøttes ikke</string>
|
||||
<string name="stores_itch_download_failed">itch.io svarede HTTP %1$d</string>
|
||||
<string name="stores_free">Gratis</string>
|
||||
|
||||
@@ -1996,6 +1996,19 @@
|
||||
<string name="stores_itch_no_exe">Im Download wurde kein Windows-Spiel gefunden</string>
|
||||
<string name="stores_itch_extract_failed">Download konnte nicht entpackt werden (Code %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io hat nicht geantwortet</string>
|
||||
<string name="stores_itch_sort_popular">Beliebt</string>
|
||||
<string name="stores_itch_sort_new">Neueste</string>
|
||||
<string name="stores_itch_sort_new_popular">Neu und beliebt</string>
|
||||
<string name="stores_itch_price_paid">Kostenpflichtig</string>
|
||||
<string name="stores_itch_price_under_5">5 $ oder weniger</string>
|
||||
<string name="stores_itch_price_under_15">15 $ oder weniger</string>
|
||||
<string name="stores_itch_sort_top_rated">Bestbewertet</string>
|
||||
<string name="stores_itch_sort_top_sellers">Meistverkauft</string>
|
||||
<string name="stores_itch_price_any">Beliebiger Preis</string>
|
||||
<string name="stores_itch_price_free">Kostenlos</string>
|
||||
<string name="stores_itch_price_sale">Im Angebot</string>
|
||||
<string name="stores_itch_load_more">Mehr laden</string>
|
||||
<string name="stores_itch_browse_failed">Spiele von itch.io konnten nicht geladen werden</string>
|
||||
<string name="stores_itch_archive">%1$s-Archive werden nicht unterstützt</string>
|
||||
<string name="stores_itch_download_failed">itch.io antwortete mit HTTP %1$d</string>
|
||||
<string name="stores_free">Kostenlos</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">Δεν βρέθηκε παιχνίδι Windows στη λήψη</string>
|
||||
<string name="stores_itch_extract_failed">Δεν ήταν δυνατή η αποσυμπίεση της λήψης (κωδικός %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">Το itch.io δεν απάντησε</string>
|
||||
<string name="stores_itch_sort_popular">Δημοφιλή</string>
|
||||
<string name="stores_itch_sort_new">Πιο πρόσφατα</string>
|
||||
<string name="stores_itch_sort_new_popular">Νέα και δημοφιλή</string>
|
||||
<string name="stores_itch_price_paid">Επί πληρωμή</string>
|
||||
<string name="stores_itch_price_under_5">$5 ή λιγότερο</string>
|
||||
<string name="stores_itch_price_under_15">$15 ή λιγότερο</string>
|
||||
<string name="stores_itch_sort_top_rated">Κορυφαία βαθμολογία</string>
|
||||
<string name="stores_itch_sort_top_sellers">Μπεστ σέλερ</string>
|
||||
<string name="stores_itch_price_any">Οποιαδήποτε τιμή</string>
|
||||
<string name="stores_itch_price_free">Δωρεάν</string>
|
||||
<string name="stores_itch_price_sale">Σε προσφορά</string>
|
||||
<string name="stores_itch_load_more">Φόρτωση περισσότερων</string>
|
||||
<string name="stores_itch_browse_failed">Δεν ήταν δυνατή η φόρτωση παιχνιδιών από το itch.io</string>
|
||||
<string name="stores_itch_archive">Τα αρχεία %1$s δεν υποστηρίζονται</string>
|
||||
<string name="stores_itch_download_failed">Το itch.io απάντησε HTTP %1$d</string>
|
||||
<string name="stores_free">Δωρεάν</string>
|
||||
|
||||
@@ -2027,6 +2027,19 @@
|
||||
<string name="stores_itch_no_exe">No se ha encontrado un juego de Windows en la descarga</string>
|
||||
<string name="stores_itch_extract_failed">No se ha podido extraer la descarga (código %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io no respondió</string>
|
||||
<string name="stores_itch_sort_popular">Populares</string>
|
||||
<string name="stores_itch_sort_new">Más recientes</string>
|
||||
<string name="stores_itch_sort_new_popular">Nuevos y populares</string>
|
||||
<string name="stores_itch_price_paid">De pago</string>
|
||||
<string name="stores_itch_price_under_5">5 $ o menos</string>
|
||||
<string name="stores_itch_price_under_15">15 $ o menos</string>
|
||||
<string name="stores_itch_sort_top_rated">Mejor valorados</string>
|
||||
<string name="stores_itch_sort_top_sellers">Más vendidos</string>
|
||||
<string name="stores_itch_price_any">Cualquier precio</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">En oferta</string>
|
||||
<string name="stores_itch_load_more">Cargar más</string>
|
||||
<string name="stores_itch_browse_failed">No se pudieron cargar los juegos de itch.io</string>
|
||||
<string name="stores_itch_archive">Los archivos %1$s no son compatibles</string>
|
||||
<string name="stores_itch_download_failed">itch.io respondió HTTP %1$d</string>
|
||||
<string name="stores_free">Gratis</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">Latauksesta ei löytynyt Windows-peliä</string>
|
||||
<string name="stores_itch_extract_failed">Latausta ei voitu purkaa (koodi %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io ei vastannut</string>
|
||||
<string name="stores_itch_sort_popular">Suositut</string>
|
||||
<string name="stores_itch_sort_new">Uusimmat</string>
|
||||
<string name="stores_itch_sort_new_popular">Uudet ja suositut</string>
|
||||
<string name="stores_itch_price_paid">Maksulliset</string>
|
||||
<string name="stores_itch_price_under_5">Enintään 5 $</string>
|
||||
<string name="stores_itch_price_under_15">Enintään 15 $</string>
|
||||
<string name="stores_itch_sort_top_rated">Parhaiten arvioidut</string>
|
||||
<string name="stores_itch_sort_top_sellers">Myydyimmät</string>
|
||||
<string name="stores_itch_price_any">Mikä tahansa hinta</string>
|
||||
<string name="stores_itch_price_free">Ilmaiset</string>
|
||||
<string name="stores_itch_price_sale">Alennuksessa</string>
|
||||
<string name="stores_itch_load_more">Lataa lisää</string>
|
||||
<string name="stores_itch_browse_failed">Pelien lataaminen itch.io:sta epäonnistui</string>
|
||||
<string name="stores_itch_archive">%1$s-arkistot eivät ole tuettuja</string>
|
||||
<string name="stores_itch_download_failed">itch.io vastasi HTTP %1$d</string>
|
||||
<string name="stores_free">Ilmainen</string>
|
||||
|
||||
@@ -2027,6 +2027,19 @@
|
||||
<string name="stores_itch_no_exe">Aucun jeu Windows trouvé dans le téléchargement</string>
|
||||
<string name="stores_itch_extract_failed">Impossible d\'extraire le téléchargement (code %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io n\'a pas répondu</string>
|
||||
<string name="stores_itch_sort_popular">Populaires</string>
|
||||
<string name="stores_itch_sort_new">Plus récents</string>
|
||||
<string name="stores_itch_sort_new_popular">Nouveautés populaires</string>
|
||||
<string name="stores_itch_price_paid">Payants</string>
|
||||
<string name="stores_itch_price_under_5">5 $ ou moins</string>
|
||||
<string name="stores_itch_price_under_15">15 $ ou moins</string>
|
||||
<string name="stores_itch_sort_top_rated">Les mieux notés</string>
|
||||
<string name="stores_itch_sort_top_sellers">Meilleures ventes</string>
|
||||
<string name="stores_itch_price_any">Tous les prix</string>
|
||||
<string name="stores_itch_price_free">Gratuits</string>
|
||||
<string name="stores_itch_price_sale">En promotion</string>
|
||||
<string name="stores_itch_load_more">Charger plus</string>
|
||||
<string name="stores_itch_browse_failed">Impossible de charger les jeux d\'itch.io</string>
|
||||
<string name="stores_itch_archive">Les archives %1$s ne sont pas prises en charge</string>
|
||||
<string name="stores_itch_download_failed">itch.io a répondu HTTP %1$d</string>
|
||||
<string name="stores_free">Gratuit</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">डाउनलोड में कोई Windows गेम नहीं मिला</string>
|
||||
<string name="stores_itch_extract_failed">डाउनलोड एक्सट्रैक्ट नहीं हो सका (कोड %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io ने जवाब नहीं दिया</string>
|
||||
<string name="stores_itch_sort_popular">लोकप्रिय</string>
|
||||
<string name="stores_itch_sort_new">सबसे हाल के</string>
|
||||
<string name="stores_itch_sort_new_popular">नए और लोकप्रिय</string>
|
||||
<string name="stores_itch_price_paid">सशुल्क</string>
|
||||
<string name="stores_itch_price_under_5">$5 या कम</string>
|
||||
<string name="stores_itch_price_under_15">$15 या कम</string>
|
||||
<string name="stores_itch_sort_top_rated">सबसे ऊँची रेटिंग</string>
|
||||
<string name="stores_itch_sort_top_sellers">सबसे ज़्यादा बिकने वाले</string>
|
||||
<string name="stores_itch_price_any">कोई भी कीमत</string>
|
||||
<string name="stores_itch_price_free">मुफ़्त</string>
|
||||
<string name="stores_itch_price_sale">सेल में</string>
|
||||
<string name="stores_itch_load_more">और लोड करें</string>
|
||||
<string name="stores_itch_browse_failed">itch.io से गेम लोड नहीं हो सके</string>
|
||||
<string name="stores_itch_archive">%1$s आर्काइव समर्थित नहीं हैं</string>
|
||||
<string name="stores_itch_download_failed">itch.io ने HTTP %1$d लौटाया</string>
|
||||
<string name="stores_free">फ़्री</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">A letöltésben nem található Windows-játék</string>
|
||||
<string name="stores_itch_extract_failed">A letöltés nem bontható ki (kód: %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">Az itch.io nem válaszolt</string>
|
||||
<string name="stores_itch_sort_popular">Népszerű</string>
|
||||
<string name="stores_itch_sort_new">Legfrissebb</string>
|
||||
<string name="stores_itch_sort_new_popular">Új és népszerű</string>
|
||||
<string name="stores_itch_price_paid">Fizetős</string>
|
||||
<string name="stores_itch_price_under_5">Legfeljebb 5 $</string>
|
||||
<string name="stores_itch_price_under_15">Legfeljebb 15 $</string>
|
||||
<string name="stores_itch_sort_top_rated">Legjobbra értékelt</string>
|
||||
<string name="stores_itch_sort_top_sellers">Legkelendőbb</string>
|
||||
<string name="stores_itch_price_any">Bármilyen ár</string>
|
||||
<string name="stores_itch_price_free">Ingyenes</string>
|
||||
<string name="stores_itch_price_sale">Akciós</string>
|
||||
<string name="stores_itch_load_more">Továbbiak betöltése</string>
|
||||
<string name="stores_itch_browse_failed">Nem sikerült betölteni a játékokat az itch.io-ról</string>
|
||||
<string name="stores_itch_archive">A(z) %1$s archívumok nem támogatottak</string>
|
||||
<string name="stores_itch_download_failed">Az itch.io HTTP %1$d választ adott</string>
|
||||
<string name="stores_free">Ingyenes</string>
|
||||
|
||||
@@ -1983,6 +1983,19 @@
|
||||
<string name="stores_itch_no_exe">Tidak ada game Windows di unduhan</string>
|
||||
<string name="stores_itch_extract_failed">Tidak dapat mengekstrak unduhan (kode %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io tidak menjawab</string>
|
||||
<string name="stores_itch_sort_popular">Populer</string>
|
||||
<string name="stores_itch_sort_new">Terbaru</string>
|
||||
<string name="stores_itch_sort_new_popular">Baru dan populer</string>
|
||||
<string name="stores_itch_price_paid">Berbayar</string>
|
||||
<string name="stores_itch_price_under_5">$5 atau kurang</string>
|
||||
<string name="stores_itch_price_under_15">$15 atau kurang</string>
|
||||
<string name="stores_itch_sort_top_rated">Rating tertinggi</string>
|
||||
<string name="stores_itch_sort_top_sellers">Terlaris</string>
|
||||
<string name="stores_itch_price_any">Harga apa saja</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">Diskon</string>
|
||||
<string name="stores_itch_load_more">Muat lebih banyak</string>
|
||||
<string name="stores_itch_browse_failed">Tidak dapat memuat game dari itch.io</string>
|
||||
<string name="stores_itch_archive">Arsip %1$s tidak didukung</string>
|
||||
<string name="stores_itch_download_failed">itch.io menjawab HTTP %1$d</string>
|
||||
<string name="stores_free">Gratis</string>
|
||||
|
||||
@@ -2017,6 +2017,19 @@
|
||||
<string name="stores_itch_no_exe">Nessun gioco Windows trovato nel download</string>
|
||||
<string name="stores_itch_extract_failed">Impossibile estrarre il download (codice %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io non ha risposto</string>
|
||||
<string name="stores_itch_sort_popular">Popolari</string>
|
||||
<string name="stores_itch_sort_new">Più recenti</string>
|
||||
<string name="stores_itch_sort_new_popular">Nuovi e popolari</string>
|
||||
<string name="stores_itch_price_paid">A pagamento</string>
|
||||
<string name="stores_itch_price_under_5">5 $ o meno</string>
|
||||
<string name="stores_itch_price_under_15">15 $ o meno</string>
|
||||
<string name="stores_itch_sort_top_rated">I più votati</string>
|
||||
<string name="stores_itch_sort_top_sellers">I più venduti</string>
|
||||
<string name="stores_itch_price_any">Qualsiasi prezzo</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">In offerta</string>
|
||||
<string name="stores_itch_load_more">Carica altri</string>
|
||||
<string name="stores_itch_browse_failed">Impossibile caricare i giochi da itch.io</string>
|
||||
<string name="stores_itch_archive">Gli archivi %1$s non sono supportati</string>
|
||||
<string name="stores_itch_download_failed">itch.io ha risposto HTTP %1$d</string>
|
||||
<string name="stores_free">Gratis</string>
|
||||
|
||||
@@ -1985,6 +1985,19 @@
|
||||
<string name="stores_itch_no_exe">ダウンロードに Windows 版のゲームが見つかりません</string>
|
||||
<string name="stores_itch_extract_failed">ダウンロードを展開できませんでした (終了コード %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io から応答がありません</string>
|
||||
<string name="stores_itch_sort_popular">人気</string>
|
||||
<string name="stores_itch_sort_new">新着順</string>
|
||||
<string name="stores_itch_sort_new_popular">新着&人気</string>
|
||||
<string name="stores_itch_price_paid">有料</string>
|
||||
<string name="stores_itch_price_under_5">5ドル以下</string>
|
||||
<string name="stores_itch_price_under_15">15ドル以下</string>
|
||||
<string name="stores_itch_sort_top_rated">高評価</string>
|
||||
<string name="stores_itch_sort_top_sellers">売れ筋</string>
|
||||
<string name="stores_itch_price_any">すべての価格</string>
|
||||
<string name="stores_itch_price_free">無料</string>
|
||||
<string name="stores_itch_price_sale">セール中</string>
|
||||
<string name="stores_itch_load_more">さらに読み込む</string>
|
||||
<string name="stores_itch_browse_failed">itch.io のゲームを読み込めませんでした</string>
|
||||
<string name="stores_itch_archive">%1$s 形式のアーカイブには対応していません</string>
|
||||
<string name="stores_itch_download_failed">itch.io が HTTP %1$d を返しました</string>
|
||||
<string name="stores_free">無料</string>
|
||||
|
||||
@@ -1985,6 +1985,19 @@
|
||||
<string name="stores_itch_no_exe">다운로드에서 Windows 게임을 찾을 수 없습니다</string>
|
||||
<string name="stores_itch_extract_failed">다운로드한 파일의 압축을 풀 수 없습니다(코드 %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io가 응답하지 않았습니다</string>
|
||||
<string name="stores_itch_sort_popular">인기</string>
|
||||
<string name="stores_itch_sort_new">최신순</string>
|
||||
<string name="stores_itch_sort_new_popular">신규 및 인기</string>
|
||||
<string name="stores_itch_price_paid">유료</string>
|
||||
<string name="stores_itch_price_under_5">$5 이하</string>
|
||||
<string name="stores_itch_price_under_15">$15 이하</string>
|
||||
<string name="stores_itch_sort_top_rated">평점 높은 순</string>
|
||||
<string name="stores_itch_sort_top_sellers">베스트셀러</string>
|
||||
<string name="stores_itch_price_any">모든 가격</string>
|
||||
<string name="stores_itch_price_free">무료</string>
|
||||
<string name="stores_itch_price_sale">할인 중</string>
|
||||
<string name="stores_itch_load_more">더 불러오기</string>
|
||||
<string name="stores_itch_browse_failed">itch.io에서 게임을 불러오지 못했습니다</string>
|
||||
<string name="stores_itch_archive">%1$s 압축 파일은 지원하지 않습니다</string>
|
||||
<string name="stores_itch_download_failed">itch.io가 HTTP %1$d을(를) 반환했습니다</string>
|
||||
<string name="stores_free">무료</string>
|
||||
|
||||
@@ -1983,6 +1983,19 @@
|
||||
<string name="stores_itch_no_exe">Tiada permainan Windows ditemui dalam muat turun</string>
|
||||
<string name="stores_itch_extract_failed">Tidak dapat mengekstrak muat turun (kod %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io tidak menjawab</string>
|
||||
<string name="stores_itch_sort_popular">Popular</string>
|
||||
<string name="stores_itch_sort_new">Terkini</string>
|
||||
<string name="stores_itch_sort_new_popular">Baharu dan popular</string>
|
||||
<string name="stores_itch_price_paid">Berbayar</string>
|
||||
<string name="stores_itch_price_under_5">$5 atau kurang</string>
|
||||
<string name="stores_itch_price_under_15">$15 atau kurang</string>
|
||||
<string name="stores_itch_sort_top_rated">Penilaian tertinggi</string>
|
||||
<string name="stores_itch_sort_top_sellers">Terlaris</string>
|
||||
<string name="stores_itch_price_any">Sebarang harga</string>
|
||||
<string name="stores_itch_price_free">Percuma</string>
|
||||
<string name="stores_itch_price_sale">Diskaun</string>
|
||||
<string name="stores_itch_load_more">Muat lagi</string>
|
||||
<string name="stores_itch_browse_failed">Tidak dapat memuatkan permainan daripada itch.io</string>
|
||||
<string name="stores_itch_archive">Arkib %1$s tidak disokong</string>
|
||||
<string name="stores_itch_download_failed">itch.io menjawab HTTP %1$d</string>
|
||||
<string name="stores_free">Percuma</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">Fant ikke noe Windows-spill i nedlastingen</string>
|
||||
<string name="stores_itch_extract_failed">Kunne ikke pakke ut nedlastingen (kode %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io svarte ikke</string>
|
||||
<string name="stores_itch_sort_popular">Populære</string>
|
||||
<string name="stores_itch_sort_new">Nyeste</string>
|
||||
<string name="stores_itch_sort_new_popular">Nye og populære</string>
|
||||
<string name="stores_itch_price_paid">Betalt</string>
|
||||
<string name="stores_itch_price_under_5">$5 eller mindre</string>
|
||||
<string name="stores_itch_price_under_15">$15 eller mindre</string>
|
||||
<string name="stores_itch_sort_top_rated">Best vurdert</string>
|
||||
<string name="stores_itch_sort_top_sellers">Mest solgt</string>
|
||||
<string name="stores_itch_price_any">Alle priser</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">På salg</string>
|
||||
<string name="stores_itch_load_more">Last inn flere</string>
|
||||
<string name="stores_itch_browse_failed">Kunne ikke laste spill fra itch.io</string>
|
||||
<string name="stores_itch_archive">%1$s-arkiver støttes ikke</string>
|
||||
<string name="stores_itch_download_failed">itch.io svarte HTTP %1$d</string>
|
||||
<string name="stores_free">Gratis</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">Geen Windows-game gevonden in de download</string>
|
||||
<string name="stores_itch_extract_failed">Download kan niet worden uitgepakt (code %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io gaf geen antwoord</string>
|
||||
<string name="stores_itch_sort_popular">Populair</string>
|
||||
<string name="stores_itch_sort_new">Meest recent</string>
|
||||
<string name="stores_itch_sort_new_popular">Nieuw en populair</string>
|
||||
<string name="stores_itch_price_paid">Betaald</string>
|
||||
<string name="stores_itch_price_under_5">$5 of minder</string>
|
||||
<string name="stores_itch_price_under_15">$15 of minder</string>
|
||||
<string name="stores_itch_sort_top_rated">Hoogst beoordeeld</string>
|
||||
<string name="stores_itch_sort_top_sellers">Bestverkocht</string>
|
||||
<string name="stores_itch_price_any">Elke prijs</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">In de aanbieding</string>
|
||||
<string name="stores_itch_load_more">Meer laden</string>
|
||||
<string name="stores_itch_browse_failed">Games van itch.io konden niet worden geladen</string>
|
||||
<string name="stores_itch_archive">%1$s-archieven worden niet ondersteund</string>
|
||||
<string name="stores_itch_download_failed">itch.io antwoordde HTTP %1$d</string>
|
||||
<string name="stores_free">Gratis</string>
|
||||
|
||||
@@ -2046,6 +2046,19 @@
|
||||
<string name="stores_itch_no_exe">W pobranych plikach nie znaleziono gry na Windows</string>
|
||||
<string name="stores_itch_extract_failed">Nie można rozpakować pobranego pliku (kod %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io nie odpowiedziało</string>
|
||||
<string name="stores_itch_sort_popular">Popularne</string>
|
||||
<string name="stores_itch_sort_new">Najnowsze</string>
|
||||
<string name="stores_itch_sort_new_popular">Nowe i popularne</string>
|
||||
<string name="stores_itch_price_paid">Płatne</string>
|
||||
<string name="stores_itch_price_under_5">Do 5 $</string>
|
||||
<string name="stores_itch_price_under_15">Do 15 $</string>
|
||||
<string name="stores_itch_sort_top_rated">Najwyżej oceniane</string>
|
||||
<string name="stores_itch_sort_top_sellers">Najlepiej sprzedające się</string>
|
||||
<string name="stores_itch_price_any">Dowolna cena</string>
|
||||
<string name="stores_itch_price_free">Za darmo</string>
|
||||
<string name="stores_itch_price_sale">Na wyprzedaży</string>
|
||||
<string name="stores_itch_load_more">Wczytaj więcej</string>
|
||||
<string name="stores_itch_browse_failed">Nie udało się wczytać gier z itch.io</string>
|
||||
<string name="stores_itch_archive">Archiwa %1$s nie są obsługiwane</string>
|
||||
<string name="stores_itch_download_failed">itch.io odpowiedziało HTTP %1$d</string>
|
||||
<string name="stores_free">Bezpłatna</string>
|
||||
|
||||
@@ -2017,6 +2017,19 @@
|
||||
<string name="stores_itch_no_exe">Não foi encontrado nenhum jogo para Windows na transferência</string>
|
||||
<string name="stores_itch_extract_failed">Não foi possível extrair a transferência (código %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">O itch.io não respondeu</string>
|
||||
<string name="stores_itch_sort_popular">Populares</string>
|
||||
<string name="stores_itch_sort_new">Mais recentes</string>
|
||||
<string name="stores_itch_sort_new_popular">Novos e populares</string>
|
||||
<string name="stores_itch_price_paid">Pagos</string>
|
||||
<string name="stores_itch_price_under_5">5 $ ou menos</string>
|
||||
<string name="stores_itch_price_under_15">15 $ ou menos</string>
|
||||
<string name="stores_itch_sort_top_rated">Mais bem avaliados</string>
|
||||
<string name="stores_itch_sort_top_sellers">Mais vendidos</string>
|
||||
<string name="stores_itch_price_any">Qualquer preço</string>
|
||||
<string name="stores_itch_price_free">Grátis</string>
|
||||
<string name="stores_itch_price_sale">Em promoção</string>
|
||||
<string name="stores_itch_load_more">Carregar mais</string>
|
||||
<string name="stores_itch_browse_failed">Não foi possível carregar os jogos do itch.io</string>
|
||||
<string name="stores_itch_archive">Ficheiros %1$s não são suportados</string>
|
||||
<string name="stores_itch_download_failed">O itch.io respondeu HTTP %1$d</string>
|
||||
<string name="stores_free">Grátis</string>
|
||||
|
||||
@@ -2017,6 +2017,19 @@
|
||||
<string name="stores_itch_no_exe">Nenhum jogo para Windows encontrado no download</string>
|
||||
<string name="stores_itch_extract_failed">Não foi possível extrair o download (código %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">O itch.io não respondeu</string>
|
||||
<string name="stores_itch_sort_popular">Populares</string>
|
||||
<string name="stores_itch_sort_new">Mais recentes</string>
|
||||
<string name="stores_itch_sort_new_popular">Novos e populares</string>
|
||||
<string name="stores_itch_price_paid">Pagos</string>
|
||||
<string name="stores_itch_price_under_5">US$ 5 ou menos</string>
|
||||
<string name="stores_itch_price_under_15">US$ 15 ou menos</string>
|
||||
<string name="stores_itch_sort_top_rated">Mais bem avaliados</string>
|
||||
<string name="stores_itch_sort_top_sellers">Mais vendidos</string>
|
||||
<string name="stores_itch_price_any">Qualquer preço</string>
|
||||
<string name="stores_itch_price_free">Grátis</string>
|
||||
<string name="stores_itch_price_sale">Em promoção</string>
|
||||
<string name="stores_itch_load_more">Carregar mais</string>
|
||||
<string name="stores_itch_browse_failed">Não foi possível carregar os jogos do itch.io</string>
|
||||
<string name="stores_itch_archive">Arquivos %1$s não são suportados</string>
|
||||
<string name="stores_itch_download_failed">O itch.io respondeu HTTP %1$d</string>
|
||||
<string name="stores_free">Grátis</string>
|
||||
|
||||
@@ -2025,6 +2025,19 @@
|
||||
<string name="stores_itch_no_exe">Nu s-a găsit niciun joc Windows în descărcare</string>
|
||||
<string name="stores_itch_extract_failed">Descărcarea nu a putut fi extrasă (cod %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io nu a răspuns</string>
|
||||
<string name="stores_itch_sort_popular">Populare</string>
|
||||
<string name="stores_itch_sort_new">Cele mai recente</string>
|
||||
<string name="stores_itch_sort_new_popular">Noi și populare</string>
|
||||
<string name="stores_itch_price_paid">Cu plată</string>
|
||||
<string name="stores_itch_price_under_5">5 $ sau mai puțin</string>
|
||||
<string name="stores_itch_price_under_15">15 $ sau mai puțin</string>
|
||||
<string name="stores_itch_sort_top_rated">Cele mai bine cotate</string>
|
||||
<string name="stores_itch_sort_top_sellers">Cele mai vândute</string>
|
||||
<string name="stores_itch_price_any">Orice preț</string>
|
||||
<string name="stores_itch_price_free">Gratuite</string>
|
||||
<string name="stores_itch_price_sale">La reducere</string>
|
||||
<string name="stores_itch_load_more">Încarcă mai multe</string>
|
||||
<string name="stores_itch_browse_failed">Nu s-au putut încărca jocurile de pe itch.io</string>
|
||||
<string name="stores_itch_archive">Arhivele %1$s nu sunt acceptate</string>
|
||||
<string name="stores_itch_download_failed">itch.io a răspuns HTTP %1$d</string>
|
||||
<string name="stores_free">Gratuit</string>
|
||||
|
||||
@@ -2048,6 +2048,19 @@
|
||||
<string name="stores_itch_no_exe">В загрузке не найдена игра для Windows</string>
|
||||
<string name="stores_itch_extract_failed">Не удалось распаковать загрузку (код %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io не ответил</string>
|
||||
<string name="stores_itch_sort_popular">Популярные</string>
|
||||
<string name="stores_itch_sort_new">Самые новые</string>
|
||||
<string name="stores_itch_sort_new_popular">Новые и популярные</string>
|
||||
<string name="stores_itch_price_paid">Платные</string>
|
||||
<string name="stores_itch_price_under_5">До $5</string>
|
||||
<string name="stores_itch_price_under_15">До $15</string>
|
||||
<string name="stores_itch_sort_top_rated">С лучшими оценками</string>
|
||||
<string name="stores_itch_sort_top_sellers">Бестселлеры</string>
|
||||
<string name="stores_itch_price_any">Любая цена</string>
|
||||
<string name="stores_itch_price_free">Бесплатные</string>
|
||||
<string name="stores_itch_price_sale">Со скидкой</string>
|
||||
<string name="stores_itch_load_more">Загрузить ещё</string>
|
||||
<string name="stores_itch_browse_failed">Не удалось загрузить игры с itch.io</string>
|
||||
<string name="stores_itch_archive">Архивы %1$s не поддерживаются</string>
|
||||
<string name="stores_itch_download_failed">itch.io ответил HTTP %1$d</string>
|
||||
<string name="stores_free">Бесплатно</string>
|
||||
|
||||
@@ -2004,6 +2004,19 @@
|
||||
<string name="stores_itch_no_exe">Inget Windows-spel hittades i nedladdningen</string>
|
||||
<string name="stores_itch_extract_failed">Det gick inte att packa upp nedladdningen (kod %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io svarade inte</string>
|
||||
<string name="stores_itch_sort_popular">Populära</string>
|
||||
<string name="stores_itch_sort_new">Senaste</string>
|
||||
<string name="stores_itch_sort_new_popular">Nya och populära</string>
|
||||
<string name="stores_itch_price_paid">Betalda</string>
|
||||
<string name="stores_itch_price_under_5">$5 eller mindre</string>
|
||||
<string name="stores_itch_price_under_15">$15 eller mindre</string>
|
||||
<string name="stores_itch_sort_top_rated">Högst betyg</string>
|
||||
<string name="stores_itch_sort_top_sellers">Mest sålda</string>
|
||||
<string name="stores_itch_price_any">Alla priser</string>
|
||||
<string name="stores_itch_price_free">Gratis</string>
|
||||
<string name="stores_itch_price_sale">På rea</string>
|
||||
<string name="stores_itch_load_more">Ladda fler</string>
|
||||
<string name="stores_itch_browse_failed">Det gick inte att läsa in spel från itch.io</string>
|
||||
<string name="stores_itch_archive">%1$s-arkiv stöds inte</string>
|
||||
<string name="stores_itch_download_failed">itch.io svarade HTTP %1$d</string>
|
||||
<string name="stores_free">Gratis</string>
|
||||
|
||||
@@ -1983,6 +1983,19 @@
|
||||
<string name="stores_itch_no_exe">ไม่พบเกม Windows ในไฟล์ที่ดาวน์โหลด</string>
|
||||
<string name="stores_itch_extract_failed">แตกไฟล์ที่ดาวน์โหลดไม่ได้ (รหัส %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io ไม่ตอบสนอง</string>
|
||||
<string name="stores_itch_sort_popular">ยอดนิยม</string>
|
||||
<string name="stores_itch_sort_new">ล่าสุด</string>
|
||||
<string name="stores_itch_sort_new_popular">ใหม่และยอดนิยม</string>
|
||||
<string name="stores_itch_price_paid">เสียเงิน</string>
|
||||
<string name="stores_itch_price_under_5">ไม่เกิน $5</string>
|
||||
<string name="stores_itch_price_under_15">ไม่เกิน $15</string>
|
||||
<string name="stores_itch_sort_top_rated">คะแนนสูงสุด</string>
|
||||
<string name="stores_itch_sort_top_sellers">ขายดี</string>
|
||||
<string name="stores_itch_price_any">ทุกราคา</string>
|
||||
<string name="stores_itch_price_free">ฟรี</string>
|
||||
<string name="stores_itch_price_sale">ลดราคา</string>
|
||||
<string name="stores_itch_load_more">โหลดเพิ่มเติม</string>
|
||||
<string name="stores_itch_browse_failed">โหลดเกมจาก itch.io ไม่ได้</string>
|
||||
<string name="stores_itch_archive">ไม่รองรับไฟล์ %1$s</string>
|
||||
<string name="stores_itch_download_failed">itch.io ตอบกลับ HTTP %1$d</string>
|
||||
<string name="stores_free">ฟรี</string>
|
||||
|
||||
@@ -1998,6 +1998,19 @@
|
||||
<string name="stores_itch_no_exe">İndirmede Windows oyunu bulunamadı</string>
|
||||
<string name="stores_itch_extract_failed">İndirme ayıklanamadı (kod %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io yanıt vermedi</string>
|
||||
<string name="stores_itch_sort_popular">Popüler</string>
|
||||
<string name="stores_itch_sort_new">En yeniler</string>
|
||||
<string name="stores_itch_sort_new_popular">Yeni ve popüler</string>
|
||||
<string name="stores_itch_price_paid">Ücretli</string>
|
||||
<string name="stores_itch_price_under_5">$5 veya altı</string>
|
||||
<string name="stores_itch_price_under_15">$15 veya altı</string>
|
||||
<string name="stores_itch_sort_top_rated">En yüksek puanlı</string>
|
||||
<string name="stores_itch_sort_top_sellers">En çok satanlar</string>
|
||||
<string name="stores_itch_price_any">Herhangi bir fiyat</string>
|
||||
<string name="stores_itch_price_free">Ücretsiz</string>
|
||||
<string name="stores_itch_price_sale">İndirimde</string>
|
||||
<string name="stores_itch_load_more">Daha fazla yükle</string>
|
||||
<string name="stores_itch_browse_failed">itch.io oyunları yüklenemedi</string>
|
||||
<string name="stores_itch_archive">%1$s arşivleri desteklenmiyor</string>
|
||||
<string name="stores_itch_download_failed">itch.io HTTP %1$d yanıtı verdi</string>
|
||||
<string name="stores_free">Ücretsiz</string>
|
||||
|
||||
@@ -2046,6 +2046,19 @@
|
||||
<string name="stores_itch_no_exe">У завантаженні не знайдено гри для Windows</string>
|
||||
<string name="stores_itch_extract_failed">Не вдалося розпакувати завантаження (код %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io не відповів</string>
|
||||
<string name="stores_itch_sort_popular">Популярні</string>
|
||||
<string name="stores_itch_sort_new">Найновіші</string>
|
||||
<string name="stores_itch_sort_new_popular">Нові й популярні</string>
|
||||
<string name="stores_itch_price_paid">Платні</string>
|
||||
<string name="stores_itch_price_under_5">До $5</string>
|
||||
<string name="stores_itch_price_under_15">До $15</string>
|
||||
<string name="stores_itch_sort_top_rated">З найвищими оцінками</string>
|
||||
<string name="stores_itch_sort_top_sellers">Бестселери</string>
|
||||
<string name="stores_itch_price_any">Будь-яка ціна</string>
|
||||
<string name="stores_itch_price_free">Безкоштовні</string>
|
||||
<string name="stores_itch_price_sale">Зі знижкою</string>
|
||||
<string name="stores_itch_load_more">Завантажити ще</string>
|
||||
<string name="stores_itch_browse_failed">Не вдалося завантажити ігри з itch.io</string>
|
||||
<string name="stores_itch_archive">Архіви %1$s не підтримуються</string>
|
||||
<string name="stores_itch_download_failed">itch.io відповів HTTP %1$d</string>
|
||||
<string name="stores_free">Безкоштовно</string>
|
||||
|
||||
@@ -1983,6 +1983,19 @@
|
||||
<string name="stores_itch_no_exe">Không tìm thấy trò chơi Windows trong bản tải xuống</string>
|
||||
<string name="stores_itch_extract_failed">Không thể giải nén bản tải xuống (mã %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io không phản hồi</string>
|
||||
<string name="stores_itch_sort_popular">Phổ biến</string>
|
||||
<string name="stores_itch_sort_new">Mới nhất</string>
|
||||
<string name="stores_itch_sort_new_popular">Mới và phổ biến</string>
|
||||
<string name="stores_itch_price_paid">Trả phí</string>
|
||||
<string name="stores_itch_price_under_5">Từ $5 trở xuống</string>
|
||||
<string name="stores_itch_price_under_15">Từ $15 trở xuống</string>
|
||||
<string name="stores_itch_sort_top_rated">Đánh giá cao nhất</string>
|
||||
<string name="stores_itch_sort_top_sellers">Bán chạy nhất</string>
|
||||
<string name="stores_itch_price_any">Mọi mức giá</string>
|
||||
<string name="stores_itch_price_free">Miễn phí</string>
|
||||
<string name="stores_itch_price_sale">Đang giảm giá</string>
|
||||
<string name="stores_itch_load_more">Tải thêm</string>
|
||||
<string name="stores_itch_browse_failed">Không tải được game từ itch.io</string>
|
||||
<string name="stores_itch_archive">Không hỗ trợ tệp %1$s</string>
|
||||
<string name="stores_itch_download_failed">itch.io trả về HTTP %1$d</string>
|
||||
<string name="stores_free">Miễn phí</string>
|
||||
|
||||
@@ -2062,6 +2062,19 @@
|
||||
<string name="stores_itch_no_exe">下载内容中未找到 Windows 游戏</string>
|
||||
<string name="stores_itch_extract_failed">无法解压下载内容(代码 %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io 没有响应</string>
|
||||
<string name="stores_itch_sort_popular">热门</string>
|
||||
<string name="stores_itch_sort_new">最新发布</string>
|
||||
<string name="stores_itch_sort_new_popular">新品与热门</string>
|
||||
<string name="stores_itch_price_paid">付费</string>
|
||||
<string name="stores_itch_price_under_5">5 美元以下</string>
|
||||
<string name="stores_itch_price_under_15">15 美元以下</string>
|
||||
<string name="stores_itch_sort_top_rated">评分最高</string>
|
||||
<string name="stores_itch_sort_top_sellers">热销</string>
|
||||
<string name="stores_itch_price_any">任意价格</string>
|
||||
<string name="stores_itch_price_free">免费</string>
|
||||
<string name="stores_itch_price_sale">特价</string>
|
||||
<string name="stores_itch_load_more">加载更多</string>
|
||||
<string name="stores_itch_browse_failed">无法从 itch.io 加载游戏</string>
|
||||
<string name="stores_itch_archive">不支持 %1$s 压缩包</string>
|
||||
<string name="stores_itch_download_failed">itch.io 返回 HTTP %1$d</string>
|
||||
<string name="stores_free">免费</string>
|
||||
|
||||
@@ -1986,6 +1986,19 @@
|
||||
<string name="stores_itch_no_exe">下載內容中找不到 Windows 遊戲</string>
|
||||
<string name="stores_itch_extract_failed">無法解壓下載內容(代碼 %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io 沒有回應</string>
|
||||
<string name="stores_itch_sort_popular">熱門</string>
|
||||
<string name="stores_itch_sort_new">最新發布</string>
|
||||
<string name="stores_itch_sort_new_popular">新品與熱門</string>
|
||||
<string name="stores_itch_price_paid">付費</string>
|
||||
<string name="stores_itch_price_under_5">5 美元以下</string>
|
||||
<string name="stores_itch_price_under_15">15 美元以下</string>
|
||||
<string name="stores_itch_sort_top_rated">評分最高</string>
|
||||
<string name="stores_itch_sort_top_sellers">熱賣</string>
|
||||
<string name="stores_itch_price_any">任何價格</string>
|
||||
<string name="stores_itch_price_free">免費</string>
|
||||
<string name="stores_itch_price_sale">特價</string>
|
||||
<string name="stores_itch_load_more">載入更多</string>
|
||||
<string name="stores_itch_browse_failed">無法從 itch.io 載入遊戲</string>
|
||||
<string name="stores_itch_archive">不支援 %1$s 壓縮檔</string>
|
||||
<string name="stores_itch_download_failed">itch.io 回應 HTTP %1$d</string>
|
||||
<string name="stores_free">免費</string>
|
||||
|
||||
@@ -1986,6 +1986,19 @@
|
||||
<string name="stores_itch_no_exe">下載內容中找不到 Windows 遊戲</string>
|
||||
<string name="stores_itch_extract_failed">無法解壓縮下載內容(代碼 %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io 沒有回應</string>
|
||||
<string name="stores_itch_sort_popular">熱門</string>
|
||||
<string name="stores_itch_sort_new">最新發布</string>
|
||||
<string name="stores_itch_sort_new_popular">新品與熱門</string>
|
||||
<string name="stores_itch_price_paid">付費</string>
|
||||
<string name="stores_itch_price_under_5">5 美元以下</string>
|
||||
<string name="stores_itch_price_under_15">15 美元以下</string>
|
||||
<string name="stores_itch_sort_top_rated">評分最高</string>
|
||||
<string name="stores_itch_sort_top_sellers">熱銷</string>
|
||||
<string name="stores_itch_price_any">任何價格</string>
|
||||
<string name="stores_itch_price_free">免費</string>
|
||||
<string name="stores_itch_price_sale">特價</string>
|
||||
<string name="stores_itch_load_more">載入更多</string>
|
||||
<string name="stores_itch_browse_failed">無法從 itch.io 載入遊戲</string>
|
||||
<string name="stores_itch_archive">不支援 %1$s 壓縮檔</string>
|
||||
<string name="stores_itch_download_failed">itch.io 回應 HTTP %1$d</string>
|
||||
<string name="stores_free">免費</string>
|
||||
|
||||
@@ -2081,6 +2081,19 @@
|
||||
<string name="stores_itch_no_exe">No Windows game found in the download</string>
|
||||
<string name="stores_itch_extract_failed">Could not extract the download (exit %1$d)</string>
|
||||
<string name="stores_itch_no_catalog">itch.io did not answer</string>
|
||||
<string name="stores_itch_sort_popular">Popular</string>
|
||||
<string name="stores_itch_sort_new">Most recent</string>
|
||||
<string name="stores_itch_sort_new_popular">New & popular</string>
|
||||
<string name="stores_itch_price_paid">Paid</string>
|
||||
<string name="stores_itch_price_under_5">$5 or less</string>
|
||||
<string name="stores_itch_price_under_15">$15 or less</string>
|
||||
<string name="stores_itch_sort_top_rated">Top rated</string>
|
||||
<string name="stores_itch_sort_top_sellers">Top sellers</string>
|
||||
<string name="stores_itch_price_any">Any price</string>
|
||||
<string name="stores_itch_price_free">Free</string>
|
||||
<string name="stores_itch_price_sale">On sale</string>
|
||||
<string name="stores_itch_load_more">Load more</string>
|
||||
<string name="stores_itch_browse_failed">Could not load games from itch.io</string>
|
||||
<string name="stores_itch_archive">%1$s archives are not supported</string>
|
||||
<string name="stores_itch_download_failed">itch.io answered HTTP %1$d</string>
|
||||
<string name="stores_free">Free</string>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.droiddeck.launcher.stores.itch
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ItchBrowseTest {
|
||||
private fun path(sort: String = ItchBrowse.SORT_POPULAR, price: String = ItchBrowse.PRICE_ANY, platforms: Set<String> = emptySet(), tags: List<String> = emptyList()) =
|
||||
ItchBrowse.path(BrowseQuery(sort, price, platforms, tags))
|
||||
|
||||
@Test fun theDefaultQueryIsTheFrontPage() {
|
||||
assertTrue(BrowseQuery().isDefault)
|
||||
assertFalse(BrowseQuery(sort = ItchBrowse.SORT_NEWEST).isDefault)
|
||||
assertFalse(BrowseQuery(price = ItchBrowse.PRICE_FREE).isDefault)
|
||||
}
|
||||
|
||||
@Test fun sortsUseItchsOwnPaths() {
|
||||
assertEquals("https://itch.io/games", path())
|
||||
assertEquals("https://itch.io/games/new-and-popular", path(sort = ItchBrowse.SORT_NEW_POPULAR))
|
||||
assertEquals("https://itch.io/games/top-sellers", path(sort = ItchBrowse.SORT_TOP_SELLERS))
|
||||
assertEquals("https://itch.io/games/top-rated", path(sort = ItchBrowse.SORT_TOP_RATED))
|
||||
assertEquals("https://itch.io/games/newest", path(sort = ItchBrowse.SORT_NEWEST))
|
||||
}
|
||||
|
||||
@Test fun pricesUseItchsOwnPaths() {
|
||||
assertEquals("https://itch.io/games/free", path(price = ItchBrowse.PRICE_FREE))
|
||||
assertEquals("https://itch.io/games/on-sale", path(price = ItchBrowse.PRICE_SALE))
|
||||
assertEquals("https://itch.io/games/store", path(price = ItchBrowse.PRICE_PAID))
|
||||
assertEquals("https://itch.io/games/5-dollars-or-less", path(price = ItchBrowse.PRICE_UNDER_5))
|
||||
assertEquals("https://itch.io/games/15-dollars-or-less", path(price = ItchBrowse.PRICE_UNDER_15))
|
||||
}
|
||||
|
||||
@Test fun platformsUseItchsOwnNames() {
|
||||
val expected = mapOf(
|
||||
"windows" to "platform-windows", "osx" to "platform-osx", "linux" to "platform-linux", "android" to "platform-android",
|
||||
"ios" to "platform-ios", "web" to "platform-web", "mobile-web" to "platform-mobile-web",
|
||||
)
|
||||
assertEquals(expected.keys.toList(), ItchBrowse.platformNames)
|
||||
for ((name, segment) in expected) assertEquals("https://itch.io/games/$segment", path(platforms = setOf(name)))
|
||||
}
|
||||
|
||||
@Test fun aSortAPriceAndAPlatformCombineInThatOrder() {
|
||||
assertEquals(
|
||||
"https://itch.io/games/top-rated/free/platform-windows",
|
||||
path(ItchBrowse.SORT_TOP_RATED, ItchBrowse.PRICE_FREE, setOf("windows")),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun aTagIsItsLowercaseNameWithHyphens() {
|
||||
assertEquals("https://itch.io/games/tag-pixel-art", path(tags = listOf("Pixel Art")))
|
||||
assertEquals("pixel-art", ItchBrowse.tagSlug(" Pixel Art "))
|
||||
}
|
||||
|
||||
@Test fun theSamePlatformsInAnyOrderGiveTheSameAddress() {
|
||||
assertEquals(path(platforms = setOf("web", "linux")), path(platforms = setOf("linux", "web")))
|
||||
}
|
||||
|
||||
@Test fun laterPagesAskForThePageNumberAndTheFirstDoesNot() {
|
||||
val query = BrowseQuery(sort = ItchBrowse.SORT_TOP_RATED)
|
||||
assertEquals("https://itch.io/games/top-rated.xml", ItchBrowse.url(query, 1))
|
||||
assertEquals("https://itch.io/games/top-rated.xml?page=2", ItchBrowse.url(query, 2))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# itch.io browsing
|
||||
|
||||
The itch Store tab opens on the front page shelves. A row of chips above them (`ui/ItchBrowseBar.kt`)
|
||||
sets an order (Popular, New, Top rated, Top sellers) and a price (Any, Free, On sale). Leave both at
|
||||
their defaults and the shelves stay; pick anything else and the tab shows that query's games as a grid
|
||||
that loads the next page as it scrolls into view.
|
||||
|
||||
## Data
|
||||
|
||||
`ItchBrowse` (`stores/itch/ItchBrowse.kt`) turns a `BrowseQuery` into itch's listing feed:
|
||||
|
||||
```
|
||||
https://itch.io/games[/<sort>][/<price>][/platform-<name>...][/tag-<slug>...].xml[?page=N]
|
||||
```
|
||||
|
||||
`BrowseQuery` already carries platforms and tags, so the URL builder is complete; the controls for them
|
||||
come in a later change. The path pieces are itch's own, copied from its links: sorts `new-and-popular`, `top-sellers`, `top-rated`,
|
||||
`newest` (Popular has none); prices `free`, `on-sale`, `store` (Paid), `5-dollars-or-less`,
|
||||
`15-dollars-or-less`; platforms `platform-windows`, `-osx`, `-linux`, `-android`, `-ios`, `-web`,
|
||||
`-mobile-web`; a tag is `tag-` and its lowercase name with hyphens. `ItchBrowse.path()` builds the page
|
||||
address, `url()` adds `.xml` and `?page=N`. Every request is logged
|
||||
(`itch.io browse: <url> answered <code> with <n> games`).
|
||||
|
||||
Requests go out one at a time, at least 1.2 s apart. A 429 waits 8 s and tries once more; if that fails
|
||||
the games already on screen stay. Each page is kept under the app cache for three hours, so going back to
|
||||
a query does not ask itch again. Results are parsed by `ItchShelves.parse`, the same reader the shelves
|
||||
use, so a game looks the same wherever it appears.
|
||||
|
||||
`ItchBrowseState` holds the query, the games loaded so far and whether more can be asked for. A newer
|
||||
query bumps a generation counter, so a slow answer for an older one is dropped. A game already in the
|
||||
library replaces its browse copy on the grid, so Install and the owned markers are right; the detail page
|
||||
looks browse results up through `StoresState.detailItem`.
|
||||
|
||||
## When a page fails
|
||||
|
||||
A fetch that throws or answers with a non-2xx status marks the list as failed but leaves it open. The end of the list then shows a Retry button that asks for the same page again. Scrolling does not retry on its own, so a failing page cannot loop. Only a successful page with no new games ends the list.
|
||||
Reference in new issue
Block a user