Compare commits
No commits in common. "main" and "fix/recitation-keeps-playing" have entirely different histories.
main
...
fix/recita
@ -13,15 +13,6 @@ 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
|
240 poets and ~135,000 poems, laid out for comfortable long-form reading in Persian, Urdu
|
||||||
and Arabic script, online or fully offline.
|
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.
|
Jetpack Compose, Material 3, `minSdk 24`. No account, no tracking, no server of its own.
|
||||||
|
|
||||||
## Reading
|
## Reading
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import org.gradle.api.tasks.PathSensitivity
|
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@ -17,7 +16,7 @@ val keystoreProperties = Properties().apply {
|
|||||||
|
|
||||||
// A delivered file should say what it is without being opened.
|
// A delivered file should say what it is without being opened.
|
||||||
base {
|
base {
|
||||||
archivesName = "ganjoor-0.4.1"
|
archivesName = "ganjoor-0.3.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -33,8 +32,8 @@ android {
|
|||||||
// Pre-1.0 while the app is still being shaped. versionCode only ever climbs: F-Droid
|
// 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
|
// refuses an update that does not, and one changelog file per code lives in
|
||||||
// fastlane/metadata/android/*/changelogs/.
|
// fastlane/metadata/android/*/changelogs/.
|
||||||
versionCode = 7
|
versionCode = 5
|
||||||
versionName = "0.4.1"
|
versionName = "0.3.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@ -76,14 +75,6 @@ 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 {
|
dependencies {
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|||||||
@ -97,22 +97,6 @@ fun PoemScreen(
|
|||||||
// finds, so the couplet's own actions live at its foot rather than behind a tap between words.
|
// finds, so the couplet's own actions live at its foot rather than behind a tap between words.
|
||||||
var tapped by remember { mutableStateOf<WordTap?>(null) }
|
var tapped by remember { mutableStateOf<WordTap?>(null) }
|
||||||
|
|
||||||
// One side panel at a time. The dictionary and the reading settings both want the left of the
|
|
||||||
// screen, and opening the second put two panels there at once — or, where there was no longer
|
|
||||||
// room for two, left the dictionary as a sheet in the middle of the page while the settings
|
|
||||||
// sat beside it. Either way the reader is asked to look in two places. The settings replace
|
|
||||||
// the dictionary instead; closing them leaves the poem, which is where the reader was.
|
|
||||||
val sidePanelOpen = LocalSidePanelOpen.current
|
|
||||||
LaunchedEffect(sidePanelOpen) { if (sidePanelOpen) tapped = null }
|
|
||||||
|
|
||||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
|
||||||
// Whether the dictionary gets a column of its own is not a question about the window but
|
|
||||||
// about what is left of this page once the columns have taken theirs. On a book-style
|
|
||||||
// foldable held open in portrait the page is already down to ~450dp, and a panel beside it
|
|
||||||
// left the verse a couple of characters a line. Where it does not fit, the sheet is the
|
|
||||||
// better answer: it covers the foot of the poem but leaves the lines whole.
|
|
||||||
val roomForPanel = dictionaryFitsBeside(maxWidth, wide)
|
|
||||||
|
|
||||||
Load(
|
Load(
|
||||||
key = fullUrl,
|
key = fullUrl,
|
||||||
block = { Ganjoor.poem(fullUrl) },
|
block = { Ganjoor.poem(fullUrl) },
|
||||||
@ -247,7 +231,7 @@ fun PoemScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (roomForPanel) {
|
if (wide) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = tapped != null,
|
visible = tapped != null,
|
||||||
enter = expandHorizontally() + fadeIn(),
|
enter = expandHorizontally() + fadeIn(),
|
||||||
@ -265,7 +249,7 @@ fun PoemScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!roomForPanel) {
|
if (!wide) {
|
||||||
tapped?.let { tap ->
|
tapped?.let { tap ->
|
||||||
WordSheet(
|
WordSheet(
|
||||||
word = tap.word,
|
word = tap.word,
|
||||||
@ -274,7 +258,6 @@ fun PoemScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The positions that make a line of verse; anything else (Single, Paragraph, Comment) is prose. */
|
/** The positions that make a line of verse; anything else (Single, Paragraph, Comment) is prose. */
|
||||||
|
|||||||
@ -39,7 +39,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.ganjoor.android.R
|
import com.ganjoor.android.R
|
||||||
import com.ganjoor.android.data.Definition
|
import com.ganjoor.android.data.Definition
|
||||||
@ -49,37 +48,6 @@ import com.ganjoor.android.data.LocalAssistant
|
|||||||
import com.ganjoor.android.data.Pronunciation
|
import com.ganjoor.android.data.Pronunciation
|
||||||
import com.ganjoor.android.ui.theme.readingStyle
|
import com.ganjoor.android.ui.theme.readingStyle
|
||||||
|
|
||||||
/**
|
|
||||||
* How wide the dictionary sits beside the poem.
|
|
||||||
*
|
|
||||||
* Narrower than the reading settings, deliberately: the settings fold the columns away and take
|
|
||||||
* the room that frees, while the dictionary is read *against* the line it came from, so the verse
|
|
||||||
* keeps the width instead. A definition is short; a hemistich is not.
|
|
||||||
*/
|
|
||||||
val DictionaryPanelWidth = 216.dp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The narrowest the page may be left once the dictionary opens beside it. Below this the panel is
|
|
||||||
* not worth its room: on a book-style foldable held open in portrait the columns have already
|
|
||||||
* taken theirs, and a panel there left the verse a couple of characters a line.
|
|
||||||
*/
|
|
||||||
val MinPageMeasure = 400.dp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the dictionary earns a column of its own beside a page [pageWidth] wide.
|
|
||||||
*
|
|
||||||
* Not a question about the window but about the page: by the time a poem is open the columns have
|
|
||||||
* already taken their room, and on a book-style foldable held open in portrait what is left is
|
|
||||||
* about 450dp. A panel beside that leaves the verse a couple of characters a line, so below
|
|
||||||
* [MinPageMeasure] the bottom sheet is used instead — it covers the foot of the poem but leaves
|
|
||||||
* every line whole.
|
|
||||||
*
|
|
||||||
* A pure function so the rule can be tested: the gesture that reaches it cannot be driven
|
|
||||||
* reliably, but the arithmetic behind it can.
|
|
||||||
*/
|
|
||||||
internal fun dictionaryFitsBeside(pageWidth: Dp, wide: Boolean): Boolean =
|
|
||||||
wide && pageWidth - DictionaryPanelWidth >= MinPageMeasure
|
|
||||||
|
|
||||||
/** English prose inside an otherwise right-to-left sheet. */
|
/** English prose inside an otherwise right-to-left sheet. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun LeftToRight(content: @Composable () -> Unit) {
|
private fun LeftToRight(content: @Composable () -> Unit) {
|
||||||
@ -134,7 +102,7 @@ fun WordPanel(word: String, onDismiss: () -> Unit, passage: Bookmark? = null) {
|
|||||||
BackHandler(onBack = onDismiss)
|
BackHandler(onBack = onDismiss)
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
modifier = Modifier.width(DictionaryPanelWidth).fillMaxHeight(),
|
modifier = Modifier.width(360.dp).fillMaxHeight(),
|
||||||
) {
|
) {
|
||||||
Column(Modifier.statusBarsPadding()) {
|
Column(Modifier.statusBarsPadding()) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
package com.ganjoor.android.ui.theme
|
package com.ganjoor.android.ui.theme
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.ExperimentalTextApi
|
import androidx.compose.ui.text.ExperimentalTextApi
|
||||||
import androidx.compose.ui.text.PlatformTextStyle
|
import androidx.compose.ui.text.PlatformTextStyle
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
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.FontVariation
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
@ -15,40 +15,24 @@ import com.ganjoor.android.R
|
|||||||
import com.ganjoor.android.ui.ReadingFont
|
import com.ganjoor.android.ui.ReadingFont
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Both files are variable fonts, registered at four weights so a reader can thicken the text —
|
* Both files are variable fonts with a `wght` axis, registered at four weights so a reader can
|
||||||
* thin naskh strokes wash out on a lit screen, especially in the dark themes.
|
* thicken the text — thin naskh strokes wash out on a lit screen, especially in the dark themes.
|
||||||
*
|
*
|
||||||
* Only `wght` is asked for, and deliberately: both files carry that one axis and nothing else.
|
* Real axis interpolation needs API 26+; on 24 and 25 the variation settings are ignored and
|
||||||
* `FontVariation.Settings(weight, style)` would also send `ital`, and a font with no italic axis
|
* Android falls back to synthesising the heavier weights, which still darkens the text.
|
||||||
* 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)
|
@OptIn(ExperimentalTextApi::class)
|
||||||
private fun variable(resId: Int, weight: FontWeight) = Font(
|
private fun variable(resId: Int, weight: FontWeight) = Font(
|
||||||
resId = resId,
|
resId = resId,
|
||||||
weight = weight,
|
weight = weight,
|
||||||
variationSettings = FontVariation.Settings(FontVariation.weight(weight.weight)),
|
variationSettings = FontVariation.Settings(weight, FontStyle.Normal),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val weights =
|
private val weights =
|
||||||
listOf(FontWeight.Normal, FontWeight.Medium, FontWeight.SemiBold, FontWeight.Bold)
|
listOf(FontWeight.Normal, FontWeight.Medium, FontWeight.SemiBold, FontWeight.Bold)
|
||||||
|
|
||||||
/**
|
val Naskh = FontFamily(weights.map { variable(R.font.noto_naskh_arabic, it) })
|
||||||
* Variable axes need API 26. Below that the settings above are dropped, and declaring four
|
val Nastaliq = FontFamily(weights.map { variable(R.font.noto_nastaliq_urdu, it) })
|
||||||
* 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
|
* The English UI only. Libron is a reading serif (OFL, github.com/nicoverbruggen/libron) and
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
package com.ganjoor.android
|
|
||||||
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.ganjoor.android.ui.dictionaryFitsBeside
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The rule that decides whether the dictionary opens beside the poem or as a sheet over it.
|
|
||||||
*
|
|
||||||
* Driving the gesture that reaches it — a tap landing on the glyphs of one Persian word — is not
|
|
||||||
* something adb does reliably, so the arithmetic is tested directly instead.
|
|
||||||
*/
|
|
||||||
class DictionaryPanelFitTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a tablet has room beside the poem`() {
|
|
||||||
// 1280dp window, less the poets rail and two list columns, leaves the page about 900dp.
|
|
||||||
assertTrue(dictionaryFitsBeside(900.dp, wide = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a foldable open in portrait does not`() {
|
|
||||||
// 700dp window, less the rail and the newest column, leaves about 450dp — and 450 less
|
|
||||||
// the 216dp panel is 234dp, which is a cramped phone column, not a measure for a verse.
|
|
||||||
assertFalse(dictionaryFitsBeside(450.dp, wide = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a phone never gets the panel, however the page is measured`() {
|
|
||||||
assertFalse(dictionaryFitsBeside(420.dp, wide = false))
|
|
||||||
assertFalse(dictionaryFitsBeside(2000.dp, wide = false))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `the boundary is the panel plus the minimum measure`() {
|
|
||||||
assertTrue(dictionaryFitsBeside(616.dp, wide = true)) // 400 + 216, exactly
|
|
||||||
assertFalse(dictionaryFitsBeside(615.dp, wide = true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -45,10 +45,6 @@ On large screens, tapping a word opens **WordPanel** on the left instead of the
|
|||||||
- The poem moves over to make room, so nothing being read is covered.
|
- The poem moves over to make room, so nothing being read is covered.
|
||||||
- The tapped word stays highlighted in the verse (`secondary-container` / `on-secondary-container`), on phones too.
|
- The tapped word stays highlighted in the verse (`secondary-container` / `on-secondary-container`), on phones too.
|
||||||
- ✕ or Back closes the panel.
|
- ✕ or Back closes the panel.
|
||||||
- The panel only opens where the page keeps at least **400dp** beside it. On a book-style
|
|
||||||
foldable held open in portrait the columns have already taken their room, and a panel
|
|
||||||
there left the verse a couple of characters a line. Below that the sheet is used instead:
|
|
||||||
it covers the foot of the poem but leaves the lines whole.
|
|
||||||
|
|
||||||
## Reading settings
|
## Reading settings
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
Tablets and unfolded foldables now read in columns: the poets on the right, then
|
|
||||||
a column for each level of the open book, and the poem in whatever room is left.
|
|
||||||
Each column scrolls on its own and narrows as you read, so the poem keeps the
|
|
||||||
middle. A button in the top bar folds them away.
|
|
||||||
|
|
||||||
Where there is room, a couplet's two hemistichs sit on one line, as Ganjoor sets
|
|
||||||
them on a desktop. Each couplet now sits in a soft card.
|
|
||||||
|
|
||||||
The dictionary and the reading settings open beside the poem rather than over
|
|
||||||
it, so nothing you are reading is covered.
|
|
||||||
|
|
||||||
Pages fill in where they load instead of blanking the screen, and Home and the
|
|
||||||
reading settings stay reachable while they do.
|
|
||||||
|
|
||||||
Tapping a word no longer carries the comma or question mark after it into the
|
|
||||||
lookup.
|
|
||||||
|
|
||||||
A reading keeps playing when you scroll down the poem, and a seek bar moves it
|
|
||||||
back and forth.
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
تبلتها و گوشیهای تاشوی باز حالا ستونی خوانده میشوند: شاعران در سمت راست، سپس
|
|
||||||
یک ستون برای هر لایهٔ کتاب باز، و شعر در هر فضایی که میماند. هر ستون جداگانه
|
|
||||||
پیمایش میشود و با خواندن باریکتر میشود تا شعر میانه را نگه دارد. دکمهای در
|
|
||||||
نوار بالا آنها را جمع میکند.
|
|
||||||
|
|
||||||
هر جا جا باشد، دو مصرع یک بیت در یک سطر مینشینند، همانگونه که گنجور روی رایانه
|
|
||||||
میچیند. هر بیت اکنون در کارتی ملایم مینشیند.
|
|
||||||
|
|
||||||
لغتنامه و تنظیمات خواندن کنار شعر باز میشوند، نه رویش، تا چیزی از آنچه
|
|
||||||
میخوانید پوشیده نشود.
|
|
||||||
|
|
||||||
صفحهها همانجا که بار میشوند پر میشوند و صفحه سفید نمیماند؛ خانه و تنظیمات
|
|
||||||
خواندن در همان حال هم در دسترساند.
|
|
||||||
|
|
||||||
ضربه بر یک واژه دیگر ویرگول یا علامت پرسش پس از آن را به جستوجو نمیبرد.
|
|
||||||
|
|
||||||
خوانش با پیمایش شعر قطع نمیشود، و نواری تازه آن را پس و پیش میبرد.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
تنظیم ضخامت قلم اکنون روی همهٔ دستگاهها کار میکند.
|
|
||||||
|
|
||||||
هر دو قلم همراه برنامه تنها یک محور متغیر دارند، wght، و بس — اما برنامه دو
|
|
||||||
محور میخواست و محور ایتالیک را هم میفرستاد. قلمی که محور ایتالیک ندارد میتواند
|
|
||||||
کل درخواست را نپذیرد، نه فقط بخشی را که از عهدهاش برنمیآید؛ روی چنین سیستمی هر
|
|
||||||
ضخامتی با همان حالت معمولی کشیده میشد. این مورد روی اندروید ۱۶ گزارش شد.
|
|
||||||
|
|
||||||
روی اندروید ۷٫۰ و ۷٫۱ که اصلاً قلم متغیر ندارند، ضخامتهای بیشتر به دلیلی
|
|
||||||
نزدیک به همین در دسترس نبودند. آنجا هم درست شد.
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
ٹیبلٹ اور کھلے فولڈ ایبل اب ستونوں میں پڑھے جاتے ہیں: شعرا دائیں طرف، پھر کھلی
|
|
||||||
کتاب کی ہر سطح کے لیے ایک ستون، اور باقی جگہ میں کلام۔ ہر ستون الگ سکرول ہوتا ہے
|
|
||||||
اور پڑھتے وقت تنگ ہو جاتا ہے تاکہ کلام درمیان میں رہے۔ اوپر کی پٹی کا ایک بٹن
|
|
||||||
انہیں سمیٹ دیتا ہے۔
|
|
||||||
|
|
||||||
جہاں گنجائش ہو، شعر کے دونوں مصرعے ایک ہی سطر میں آتے ہیں، جیسے گنجور کمپیوٹر پر
|
|
||||||
دکھاتا ہے۔ ہر شعر اب ایک ہلکے کارڈ میں بیٹھتا ہے۔
|
|
||||||
|
|
||||||
لغت نامہ اور مطالعے کی ترتیبات کلام کے ساتھ کھلتی ہیں، اُس پر نہیں، تاکہ جو آپ
|
|
||||||
پڑھ رہے ہیں وہ ڈھکا نہ جائے۔
|
|
||||||
|
|
||||||
صفحات وہیں بھرتے ہیں جہاں لوڈ ہوتے ہیں، خالی اسکرین نہیں آتی؛ ہوم اور مطالعے کی
|
|
||||||
ترتیبات اُس دوران بھی دستیاب رہتی ہیں۔
|
|
||||||
|
|
||||||
کسی لفظ پر ٹیپ اب اُس کے بعد والا کوما یا سوالیہ نشان ساتھ نہیں لے جاتی۔
|
|
||||||
|
|
||||||
سکرول کرنے پر تلاوت بند نہیں ہوتی، اور ایک نئی سیک بار اُسے آگے پیچھے کرتی ہے۔
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
فونٹ کی موٹائی کا انتخاب اب ہر آلے پر کام کرتا ہے۔
|
|
||||||
|
|
||||||
ایپ کے ساتھ آنے والے دونوں فونٹس میں صرف ایک متغیر محور ہے، wght، اور بس — مگر
|
|
||||||
ایپ دو مانگ رہی تھی اور ساتھ اٹالک محور بھی بھیج رہی تھی۔ جس فونٹ میں اٹالک محور
|
|
||||||
نہ ہو وہ پورا مطالبہ رد کر سکتا ہے، نہ کہ صرف وہ حصہ جو وہ پورا نہیں کر سکتا؛ ایسے
|
|
||||||
نظام پر ہر موٹائی عام ہی کی طرح بنتی رہی۔ یہ اینڈرائیڈ ۱۶ پر رپورٹ ہوا۔
|
|
||||||
|
|
||||||
اینڈرائیڈ ۷٫۰ اور ۷٫۱ پر، جہاں متغیر فونٹ ہوتے ہی نہیں، زیادہ موٹائیاں اسی سے
|
|
||||||
ملتی جلتی وجہ سے ناقابلِ رسائی تھیں۔ وہاں بھی اب درست ہے۔
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
# F-Droid metadata for Ganjoor for Android.
|
|
||||||
#
|
|
||||||
# This file is not read by the app or the build; it is the recipe F-Droid's build
|
|
||||||
# server uses, kept here so it travels with the source it describes. To submit or
|
|
||||||
# update the listing, copy it into a fork of gitlab.com/fdroid/fdroiddata as
|
|
||||||
# metadata/com.ganjoor.android.yml and open a merge request.
|
|
||||||
#
|
|
||||||
# F-Droid builds from source and signs with their own key, which is why the
|
|
||||||
# release build must succeed with no keystore present — see app/build.gradle.kts.
|
|
||||||
|
|
||||||
Categories:
|
|
||||||
- Reading
|
|
||||||
License: MIT
|
|
||||||
AuthorName: Muhammad Anas Rashid
|
|
||||||
SourceCode: https://github.com/anas-rashid/ganjoorandroid
|
|
||||||
IssueTracker: https://github.com/anas-rashid/ganjoorandroid/issues
|
|
||||||
Changelog: https://github.com/anas-rashid/ganjoorandroid/releases
|
|
||||||
|
|
||||||
AutoName: گنجور
|
|
||||||
|
|
||||||
RepoType: git
|
|
||||||
Repo: https://github.com/anas-rashid/ganjoorandroid.git
|
|
||||||
|
|
||||||
Builds:
|
|
||||||
- versionName: 0.4.0
|
|
||||||
versionCode: 6
|
|
||||||
commit: v0.4.0
|
|
||||||
subdir: app
|
|
||||||
gradle:
|
|
||||||
- yes
|
|
||||||
|
|
||||||
AutoUpdateMode: Version
|
|
||||||
UpdateCheckMode: Tags
|
|
||||||
CurrentVersion: 0.4.0
|
|
||||||
CurrentVersionCode: 6
|
|
||||||
@ -12,9 +12,7 @@ from source and signs with its own key, so these are for direct installation onl
|
|||||||
| 0.2.0 | 2 | `d92445f` | Share a poem, couplet or selection; look a word up from any app's selection menu; the optional AI assistant. |
|
| 0.2.0 | 2 | `d92445f` | Share a poem, couplet or selection; look a word up from any app's selection menu; the optional AI assistant. |
|
||||||
| 0.2.1 | 3 | `ec05989` | Assistant answers in the page rather than over it, a visible chevron for each couplet's actions, and answers remembered so scrolling doesn't re-ask. |
|
| 0.2.1 | 3 | `ec05989` | Assistant answers in the page rather than over it, a visible chevron for each couplet's actions, and answers remembered so scrolling doesn't re-ask. |
|
||||||
| 0.2.2 | 4 | `d7568d1` | Credits Material Symbols for the glyphs, and states the terms around the optional assistant. |
|
| 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.3.0 | 5 | `HEAD` | 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
|
## Checksums
|
||||||
|
|
||||||
@ -24,8 +22,6 @@ from source and signs with its own key, so these are for direct installation onl
|
|||||||
5163554eb01845c131c31a9da7bdd748170bb9b5f82edaece754b4cf40b02b0c ganjoor-0.2.1.apk
|
5163554eb01845c131c31a9da7bdd748170bb9b5f82edaece754b4cf40b02b0c ganjoor-0.2.1.apk
|
||||||
e473269044710813d2cf4119de663fe180f887239df7bdb1ccee0239ef84fd3e ganjoor-0.2.2.apk
|
e473269044710813d2cf4119de663fe180f887239df7bdb1ccee0239ef84fd3e ganjoor-0.2.2.apk
|
||||||
9c38d5cacf9e809c2189ba581f8aa6eb4072d4fa0316f00d66434461cf4b1459 ganjoor-0.3.0.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
|
Installing: `adb install -r releases/ganjoor-<version>.apk`. A phone holding a *debug* build has
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -1,87 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
Loading…
Reference in New Issue
Block a user