Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38996943b5 | ||
|
|
b6f103dc73 | ||
|
|
5a338d0e0a | ||
|
|
f49d9f080f | ||
|
|
e0537e3455 | ||
|
|
6590a6b21a | ||
|
|
66d8b82648 | ||
|
|
c801d88ef3 |
No files matched your search
@@ -13,6 +13,15 @@ An Android reader for [Ganjoor](https://ganjoor.net), the open archive of Persia
|
||||
240 poets and ~135,000 poems, laid out for comfortable long-form reading in Persian, Urdu
|
||||
and Arabic script, online or fully offline.
|
||||
|
||||
### ⬇ [Download ganjoor-0.4.1.apk](releases/ganjoor-0.4.1.apk)
|
||||
|
||||
Android 7.0 and up · 31.7 MB · [older releases](releases/) · `sha256 6c85da41…ce357615daa`
|
||||
|
||||
Android will call the developer unknown and offer to install anyway — it says that about every
|
||||
app installed outside a store. Allow installs from your browser once and it will go through.
|
||||
Every release is signed with the same key, so a new one upgrades the last without losing your
|
||||
bookmarks. [Build it yourself](#build) instead if you would rather.
|
||||
|
||||
Jetpack Compose, Material 3, `minSdk 24`. No account, no tracking, no server of its own.
|
||||
|
||||
## Reading
|
||||
|
||||
+12
-3
@@ -1,3 +1,4 @@
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
@@ -16,7 +17,7 @@ val keystoreProperties = Properties().apply {
|
||||
|
||||
// A delivered file should say what it is without being opened.
|
||||
base {
|
||||
archivesName = "ganjoor-0.4.0"
|
||||
archivesName = "ganjoor-0.4.1"
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -32,8 +33,8 @@ android {
|
||||
// Pre-1.0 while the app is still being shaped. versionCode only ever climbs: F-Droid
|
||||
// refuses an update that does not, and one changelog file per code lives in
|
||||
// fastlane/metadata/android/*/changelogs/.
|
||||
versionCode = 6
|
||||
versionName = "0.4.0"
|
||||
versionCode = 7
|
||||
versionName = "0.4.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -75,6 +76,14 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// ReleaseDocsTest reads these, so a change to either has to re-run the tests. Without this
|
||||
// Gradle sees only Kotlin sources, calls the task up to date, and a README that has fallen
|
||||
// behind the build sails through green.
|
||||
tasks.withType<Test>().configureEach {
|
||||
inputs.file(rootProject.file("README.md")).withPathSensitivity(PathSensitivity.RELATIVE)
|
||||
inputs.file(rootProject.file("releases/README.md")).withPathSensitivity(PathSensitivity.RELATIVE)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.ganjoor.android.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.PlatformTextStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontVariation
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
@@ -15,24 +15,40 @@ import com.ganjoor.android.R
|
||||
import com.ganjoor.android.ui.ReadingFont
|
||||
|
||||
/**
|
||||
* Both files are variable fonts with a `wght` axis, registered at four weights so a reader can
|
||||
* thicken the text — thin naskh strokes wash out on a lit screen, especially in the dark themes.
|
||||
* Both files are variable fonts, registered at four weights so a reader can thicken the text —
|
||||
* thin naskh strokes wash out on a lit screen, especially in the dark themes.
|
||||
*
|
||||
* Real axis interpolation needs API 26+; on 24 and 25 the variation settings are ignored and
|
||||
* Android falls back to synthesising the heavier weights, which still darkens the text.
|
||||
* Only `wght` is asked for, and deliberately: both files carry that one axis and nothing else.
|
||||
* `FontVariation.Settings(weight, style)` would also send `ital`, and a font with no italic axis
|
||||
* is entitled to refuse the whole request rather than the part it cannot honour. A platform that
|
||||
* does so leaves every weight drawing at 400 — and silently, because each entry here declares the
|
||||
* weight it was asked for, so the text is never a candidate for synthetic bolding either.
|
||||
*/
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
private fun variable(resId: Int, weight: FontWeight) = Font(
|
||||
resId = resId,
|
||||
weight = weight,
|
||||
variationSettings = FontVariation.Settings(weight, FontStyle.Normal),
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(weight.weight)),
|
||||
)
|
||||
|
||||
private val weights =
|
||||
listOf(FontWeight.Normal, FontWeight.Medium, FontWeight.SemiBold, FontWeight.Bold)
|
||||
|
||||
val Naskh = FontFamily(weights.map { variable(R.font.noto_naskh_arabic, it) })
|
||||
val Nastaliq = FontFamily(weights.map { variable(R.font.noto_nastaliq_urdu, it) })
|
||||
/**
|
||||
* Variable axes need API 26. Below that the settings above are dropped, and declaring four
|
||||
* weights of the same file would make the heavier ones unreachable: Android would match the
|
||||
* entry claiming 700, draw it at 400, and skip synthetic bolding because the entry said it was
|
||||
* already bold. One entry at its real weight instead, so asking for bold actually synthesises it.
|
||||
*/
|
||||
private fun scriptFamily(resId: Int): FontFamily =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
FontFamily(weights.map { variable(resId, it) })
|
||||
} else {
|
||||
FontFamily(Font(resId, FontWeight.Normal))
|
||||
}
|
||||
|
||||
val Naskh = scriptFamily(R.font.noto_naskh_arabic)
|
||||
val Nastaliq = scriptFamily(R.font.noto_nastaliq_urdu)
|
||||
|
||||
/**
|
||||
* The English UI only. Libron is a reading serif (OFL, github.com/nicoverbruggen/libron) and
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.ganjoor.android
|
||||
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The download the README offers must be the release the build actually produces.
|
||||
*
|
||||
* A README that advertises last version is worse than one that says nothing: someone follows the
|
||||
* link, installs an old build, and has no way to know. tools/update_release_docs.py keeps these in
|
||||
* step; this is what makes forgetting to run it a failed build rather than a quiet wrong answer.
|
||||
*/
|
||||
class ReleaseDocsTest {
|
||||
|
||||
private val root: File =
|
||||
generateSequence(File(System.getProperty("user.dir")!!)) { it.parentFile }
|
||||
.first { File(it, "settings.gradle.kts").exists() }
|
||||
|
||||
private fun read(path: String) = File(root, path).readText()
|
||||
|
||||
private fun sha256(file: File): String =
|
||||
MessageDigest.getInstance("SHA-256").digest(file.readBytes())
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
|
||||
private val declaredVersion: String by lazy {
|
||||
Regex("versionName\\s*=\\s*\"([^\"]+)\"")
|
||||
.find(read("app/build.gradle.kts"))!!
|
||||
.groupValues[1]
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the README offers the version the build declares`() {
|
||||
val offered = Regex("### . \\[Download ganjoor-([^\\]]+)\\.apk]")
|
||||
.find(read("README.md"))
|
||||
?.groupValues?.get(1)
|
||||
assertEquals(
|
||||
"README.md offers a different version than app/build.gradle.kts declares — " +
|
||||
"run tools/update_release_docs.py",
|
||||
declaredVersion,
|
||||
offered,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the APK the README links to is archived`() {
|
||||
val apk = File(root, "releases/ganjoor-" + declaredVersion + ".apk")
|
||||
assertTrue(apk.name + " is linked from README.md but not in releases/", apk.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the README short checksum matches the archived APK`() {
|
||||
val short = Regex("`sha256 ([0-9a-f]+)…([0-9a-f]+)`")
|
||||
.find(read("README.md"))!!
|
||||
.groupValues
|
||||
val full = sha256(File(root, "releases/ganjoor-" + declaredVersion + ".apk"))
|
||||
assertTrue(
|
||||
"README.md's checksum does not match releases/ganjoor-" + declaredVersion + ".apk",
|
||||
full.startsWith(short[1]) && full.endsWith(short[2]),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every checksum in releases matches the APK beside it`() {
|
||||
val listed = Regex("^([0-9a-f]{64}) (ganjoor-.+\\.apk)$", RegexOption.MULTILINE)
|
||||
.findAll(read("releases/README.md"))
|
||||
.map { it.groupValues[1] to it.groupValues[2] }
|
||||
.toList()
|
||||
assertTrue("no checksums found in releases/README.md", listed.isNotEmpty())
|
||||
listed.forEach { (digest, name) ->
|
||||
val apk = File(root, "releases/" + name)
|
||||
assertTrue(name + " is listed in releases/README.md but missing", apk.exists())
|
||||
assertEquals(name + " does not match its listed checksum", digest, sha256(apk))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
The font weight control now works on every device.
|
||||
|
||||
Both bundled faces carry one variable axis, wght, and nothing else — but the app
|
||||
asked for two, sending an italic axis alongside it. A font with no italic axis
|
||||
may refuse the whole request rather than the part it cannot honour, and on a
|
||||
platform that does, every weight drew at Regular with nothing to show for it.
|
||||
Reported on Android 16.
|
||||
|
||||
On Android 7.0 and 7.1, where variable fonts do not exist at all, the heavier
|
||||
weights were unreachable for a related reason. They now work there too.
|
||||
@@ -0,0 +1,9 @@
|
||||
تنظیم ضخامت قلم اکنون روی همهٔ دستگاهها کار میکند.
|
||||
|
||||
هر دو قلم همراه برنامه تنها یک محور متغیر دارند، wght، و بس — اما برنامه دو
|
||||
محور میخواست و محور ایتالیک را هم میفرستاد. قلمی که محور ایتالیک ندارد میتواند
|
||||
کل درخواست را نپذیرد، نه فقط بخشی را که از عهدهاش برنمیآید؛ روی چنین سیستمی هر
|
||||
ضخامتی با همان حالت معمولی کشیده میشد. این مورد روی اندروید ۱۶ گزارش شد.
|
||||
|
||||
روی اندروید ۷٫۰ و ۷٫۱ که اصلاً قلم متغیر ندارند، ضخامتهای بیشتر به دلیلی
|
||||
نزدیک به همین در دسترس نبودند. آنجا هم درست شد.
|
||||
@@ -0,0 +1,9 @@
|
||||
فونٹ کی موٹائی کا انتخاب اب ہر آلے پر کام کرتا ہے۔
|
||||
|
||||
ایپ کے ساتھ آنے والے دونوں فونٹس میں صرف ایک متغیر محور ہے، wght، اور بس — مگر
|
||||
ایپ دو مانگ رہی تھی اور ساتھ اٹالک محور بھی بھیج رہی تھی۔ جس فونٹ میں اٹالک محور
|
||||
نہ ہو وہ پورا مطالبہ رد کر سکتا ہے، نہ کہ صرف وہ حصہ جو وہ پورا نہیں کر سکتا؛ ایسے
|
||||
نظام پر ہر موٹائی عام ہی کی طرح بنتی رہی۔ یہ اینڈرائیڈ ۱۶ پر رپورٹ ہوا۔
|
||||
|
||||
اینڈرائیڈ ۷٫۰ اور ۷٫۱ پر، جہاں متغیر فونٹ ہوتے ہی نہیں، زیادہ موٹائیاں اسی سے
|
||||
ملتی جلتی وجہ سے ناقابلِ رسائی تھیں۔ وہاں بھی اب درست ہے۔
|
||||
@@ -14,6 +14,7 @@ from source and signs with its own key, so these are for direct installation onl
|
||||
| 0.2.2 | 4 | `d7568d1` | Credits Material Symbols for the glyphs, and states the terms around the optional assistant. |
|
||||
| 0.3.0 | 5 | `5d12a1c` | Pin poets to the home screen: three views (pinned, Ganjoor's order, alphabetical), as cards or a list, with a download control on each row. |
|
||||
| 0.4.0 | 6 | `270c28c` | Columns on tablets and unfolded foldables; the dictionary and reading settings beside the poem rather than over it; couplets in cards, two hemistichs to a line where there is room; skeletons while pages load; a seek bar on recitations. |
|
||||
| 0.4.1 | 7 | `HEAD` | The font weight control works on every device: the app was asking the bundled faces for an italic axis they do not have, and a font may refuse the whole request rather than the part it cannot honour. |
|
||||
|
||||
## Checksums
|
||||
|
||||
@@ -24,6 +25,7 @@ from source and signs with its own key, so these are for direct installation onl
|
||||
e473269044710813d2cf4119de663fe180f887239df7bdb1ccee0239ef84fd3e ganjoor-0.2.2.apk
|
||||
9c38d5cacf9e809c2189ba581f8aa6eb4072d4fa0316f00d66434461cf4b1459 ganjoor-0.3.0.apk
|
||||
1c64cefa0fb2b51bf73386460ec1b88bda3e20832fa6926c2f7f1fb863ab9fc7 ganjoor-0.4.0.apk
|
||||
6c85da41bf1416e34d6cb9c9eda9b4867e01b6793f21e476a6e3ece357615daa ganjoor-0.4.1.apk
|
||||
```
|
||||
|
||||
Installing: `adb install -r releases/ganjoor-<version>.apk`. A phone holding a *debug* build has
|
||||
|
||||
Binary file not shown.
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bring the download line in README.md and the checksum list in releases/README.md
|
||||
into step with whatever version app/build.gradle.kts declares.
|
||||
|
||||
Run it after building and copying the signed APK into releases/:
|
||||
|
||||
./gradlew :app:assembleRelease
|
||||
cp app/build/outputs/apk/release/ganjoor-<v>-release.apk releases/ganjoor-<v>.apk
|
||||
python3 tools/update_release_docs.py
|
||||
|
||||
It reads the version from the build file rather than taking an argument, so the
|
||||
documentation can only ever describe the release the build actually produces.
|
||||
ReleaseDocsTest fails if either file falls behind, so forgetting this is caught
|
||||
rather than shipped.
|
||||
"""
|
||||
import hashlib
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def version() -> str:
|
||||
build = (ROOT / "app/build.gradle.kts").read_text(encoding="utf-8")
|
||||
m = re.search(r'versionName\s*=\s*"([^"]+)"', build)
|
||||
if not m:
|
||||
sys.exit("could not find versionName in app/build.gradle.kts")
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
v = version()
|
||||
apk = ROOT / "releases" / f"ganjoor-{v}.apk"
|
||||
if not apk.exists():
|
||||
sys.exit(f"{apk.relative_to(ROOT)} is missing — build and copy it in first")
|
||||
|
||||
digest = hashlib.sha256(apk.read_bytes()).hexdigest()
|
||||
megabytes = apk.stat().st_size / 1_000_000
|
||||
|
||||
# README.md — the download heading and the line of facts under it.
|
||||
readme = ROOT / "README.md"
|
||||
text = readme.read_text(encoding="utf-8")
|
||||
text, n = re.subn(
|
||||
r"### ⬇ \[Download ganjoor-[^\]]+\]\(releases/ganjoor-[^)]+\)",
|
||||
f"### ⬇ [Download ganjoor-{v}.apk](releases/ganjoor-{v}.apk)",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if n != 1:
|
||||
sys.exit("could not find the download heading in README.md")
|
||||
short = f"{digest[:8]}…{digest[-11:]}"
|
||||
text, n = re.subn(
|
||||
r"Android 7\.0 and up · [^·]+· \[older releases\]\(releases/\) · `sha256 [^`]+`",
|
||||
f"Android 7.0 and up · {megabytes:.1f} MB · [older releases](releases/) · `sha256 {short}`",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if n != 1:
|
||||
sys.exit("could not find the download facts line in README.md")
|
||||
readme.write_text(text, encoding="utf-8")
|
||||
|
||||
# releases/README.md — one checksum line per archived APK, in version order.
|
||||
index = ROOT / "releases" / "README.md"
|
||||
listing = index.read_text(encoding="utf-8")
|
||||
line = f"{digest} ganjoor-{v}.apk"
|
||||
if line not in listing:
|
||||
# replace an existing line for this version, or append to the block
|
||||
existing = re.search(rf"^[0-9a-f]{{64}} ganjoor-{re.escape(v)}\.apk$", listing, re.M)
|
||||
if existing:
|
||||
listing = listing[: existing.start()] + line + listing[existing.end():]
|
||||
else:
|
||||
last = None
|
||||
for last in re.finditer(r"^[0-9a-f]{64} ganjoor-[^\n]+$", listing, re.M):
|
||||
pass
|
||||
if not last:
|
||||
sys.exit("could not find the checksum block in releases/README.md")
|
||||
listing = listing[: last.end()] + "\n" + line + listing[last.end():]
|
||||
index.write_text(listing, encoding="utf-8")
|
||||
|
||||
print(f"{v} {megabytes:.1f} MB {digest}")
|
||||
print("README.md and releases/README.md are in step")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user