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:
MaxsTechReview authored and GitHub committed 2026-10-24 11:33:47 -04:00
commit 77e23cb397
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)
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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>
+13
View File
@@ -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 &amp; 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))
}
}
+36
View File
@@ -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.