diff --git a/app/src/main/java/com/ganjoor/android/MainActivity.kt b/app/src/main/java/com/ganjoor/android/MainActivity.kt index ea205dc..45add11 100644 --- a/app/src/main/java/com/ganjoor/android/MainActivity.kt +++ b/app/src/main/java/com/ganjoor/android/MainActivity.kt @@ -16,6 +16,8 @@ import com.ganjoor.android.data.Dictionary import com.ganjoor.android.data.Ganjoor import com.ganjoor.android.data.LocalBookmarks import com.ganjoor.android.ui.GanjoorApp +import com.ganjoor.android.data.AssistantSettings +import com.ganjoor.android.data.LocalAssistant import com.ganjoor.android.ui.LocalSettings import com.ganjoor.android.ui.Settings import com.ganjoor.android.ui.theme.GanjoorTheme @@ -53,11 +55,13 @@ class MainActivity : ComponentActivity() { setContent { val settings = remember { settings } val bookmarks = remember { Bookmarks(applicationContext) } + val assistant = remember { AssistantSettings(applicationContext) } // The client reads this flag on every request, so keep it in step with the setting. Ganjoor.offline = settings.value.offline CompositionLocalProvider( LocalSettings provides settings, LocalBookmarks provides bookmarks, + LocalAssistant provides assistant, // The poetry is Persian, Urdu and Arabic throughout, so the whole app reads and // navigates right-to-left whichever UI language is selected. LocalLayoutDirection provides LayoutDirection.Rtl, diff --git a/app/src/main/java/com/ganjoor/android/ui/AssistantResultSheet.kt b/app/src/main/java/com/ganjoor/android/ui/AssistantResultSheet.kt index c967190..d912b29 100644 --- a/app/src/main/java/com/ganjoor/android/ui/AssistantResultSheet.kt +++ b/app/src/main/java/com/ganjoor/android/ui/AssistantResultSheet.kt @@ -16,7 +16,9 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState +import androidx.compose.runtime.setValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -46,6 +48,26 @@ fun AssistantResultSheet(prompt: String, text: String, onDismiss: () -> Unit) { } } +/** + * A button that asks the reader's own assistant, or hands the question to another app when no + * server is configured. Both paths are one tap and neither requires any setting up, which is the + * point: the feature is optional, so it cannot assume anyone switched it on. + */ +@Composable +fun AssistantAction(prompt: String, text: String, label: Int, instruction: Int) { + val assistant = LocalAssistant.current + val context = LocalContext.current + val ask = stringResource(instruction) + var open by remember { mutableStateOf(false) } + + TextButton(onClick = { + if (assistant.serverReady) open = true else context.shareText("$ask\n\n$text") + }) { + Text(stringResource(label)) + } + if (open) AssistantResultSheet(prompt = prompt, text = text, onDismiss = { open = false }) +} + /** The same answer as a plain screen, for the selection-menu activity. */ @Composable fun AssistantAnswer(text: String, prompt: String = "translate") { diff --git a/app/src/main/java/com/ganjoor/android/ui/GanjoorApp.kt b/app/src/main/java/com/ganjoor/android/ui/GanjoorApp.kt index 6adc2c2..b5a5ebf 100644 --- a/app/src/main/java/com/ganjoor/android/ui/GanjoorApp.kt +++ b/app/src/main/java/com/ganjoor/android/ui/GanjoorApp.kt @@ -42,6 +42,9 @@ object DownloadsRoute @Serializable object AboutRoute +@Serializable +object AssistantRoute + /** * Opens a page with the poet list as the only thing beneath it. * @@ -75,6 +78,10 @@ fun GanjoorApp() { settingsOpen = false nav.navigate(AboutRoute) }, + LocalOpenAssistant provides { + settingsOpen = false + nav.navigate(AssistantRoute) + }, ) { NavHost( navController = nav, @@ -144,6 +151,9 @@ fun GanjoorApp() { composable { DownloadsScreen(onUp = { nav.goHome() }) } + composable { + AssistantScreen(onUp = { nav.navigateUp() }, onHome = { nav.goHome() }) + } composable { AboutScreen(onUp = { nav.navigateUp() }, onHome = { nav.goHome() }) } diff --git a/app/src/main/java/com/ganjoor/android/ui/PoemScreen.kt b/app/src/main/java/com/ganjoor/android/ui/PoemScreen.kt index 8ce7131..187e0d0 100644 --- a/app/src/main/java/com/ganjoor/android/ui/PoemScreen.kt +++ b/app/src/main/java/com/ganjoor/android/ui/PoemScreen.kt @@ -387,6 +387,12 @@ private fun PassageActions(passage: Bookmark) { }) { Text(stringResource(R.string.share)) } + AssistantAction( + prompt = "explain", + text = passage.excerpt.orEmpty(), + label = R.string.assistant_explain, + instruction = R.string.assistant_ask_prompt, + ) } } @@ -405,6 +411,12 @@ private fun PoemSummary(summary: String) { style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + AssistantAction( + prompt = "summary", + text = summary, + label = R.string.assistant_translate, + instruction = R.string.assistant_translate_prompt, + ) } } diff --git a/app/src/main/java/com/ganjoor/android/ui/ReadingSettings.kt b/app/src/main/java/com/ganjoor/android/ui/ReadingSettings.kt index c5c99a4..396d614 100644 --- a/app/src/main/java/com/ganjoor/android/ui/ReadingSettings.kt +++ b/app/src/main/java/com/ganjoor/android/ui/ReadingSettings.kt @@ -44,6 +44,7 @@ val LocalOpenReadingSettings = staticCompositionLocalOf<() -> Unit> { /** Opens the About and licences screen; the sheet is the only place that needs it. */ val LocalOpenAbout = staticCompositionLocalOf<() -> Unit> { error("No about host") } +val LocalOpenAssistant = staticCompositionLocalOf<() -> Unit> { error("No assistant host") } /** Top-bar button that opens the reading settings sheet. */ @Composable @@ -137,11 +138,13 @@ fun ReadingSettingsSheet(onDismiss: () -> Unit) { settings.update { it.copy(showSummaries = on) } } + val openAssistant = LocalOpenAssistant.current + TextButton(onClick = openAssistant, modifier = Modifier.padding(top = 16.dp)) { + Text(stringResource(R.string.assistant_title)) + } + val openAbout = LocalOpenAbout.current - TextButton( - onClick = openAbout, - modifier = Modifier.padding(top = 16.dp), - ) { + TextButton(onClick = openAbout) { Text(stringResource(R.string.about)) } } diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 88e5f92..e8b7de5 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -103,4 +103,6 @@ چه کاری انجام شود؟ پرسش از دستیار این شعر فارسی را ترجمه کن و معنایش را توضیح بده: + هوش مصنوعی + این متن را ترجمه کن: diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 3fe35ad..fa8700a 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -72,7 +72,7 @@ تاریک تھیمز پر لاگو ہوتا ہے؛ OLED اسکرین پر بیٹری بچاتا ہے قرات سنیں وقفہ - اے آئی معاون + مصنوعی ذہانت کا معاون اختیاری ہے۔ ایپ کا ہر حصہ اس کے بغیر کام کرتا ہے۔ جب تک آپ خود ترتیب نہ دیں کچھ کہیں نہیں بھیجا جاتا، اور ایپ کے ساتھ کوئی اکاؤنٹ یا کلید نہیں آتی۔ ماڈل تک رسائی کا طریقہ بند @@ -103,4 +103,6 @@ آپ کیا کرنا چاہیں گے؟ اسسٹنٹ سے پوچھیں اس فارسی کلام کا ترجمہ کریں اور اس کا مطلب بیان کریں: + مصنوعی ذہانت + اس کا ترجمہ کریں: diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e433525..501e3c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,4 +108,6 @@ What would you like to do? Ask an assistant Translate this Persian poetry and explain what it means: + AI assistant + Translate this: diff --git a/app/src/test/java/com/ganjoor/android/ShareTest.kt b/app/src/test/java/com/ganjoor/android/ShareTest.kt new file mode 100644 index 0000000..008ae11 --- /dev/null +++ b/app/src/test/java/com/ganjoor/android/ShareTest.kt @@ -0,0 +1,25 @@ +package com.ganjoor.android + +import com.ganjoor.android.ui.webUrl +import org.junit.Assert.assertEquals +import org.junit.Test + +class ShareTest { + + @Test + fun `builds a ganjoor link from a stored path`() { + assertEquals("https://ganjoor.net/hafez/ghazal/sh1", webUrl("/hafez/ghazal/sh1")) + } + + /** Paths arrive with a leading slash and sometimes a trailing one; neither may double up. */ + @Test + fun `trims slashes rather than doubling them`() { + assertEquals("https://ganjoor.net/saadi/golestan", webUrl("/saadi/golestan/")) + assertEquals("https://ganjoor.net/saadi/golestan", webUrl("saadi/golestan")) + } + + @Test + fun `leaves an absolute url alone`() { + assertEquals("https://ganjoor.net/x", webUrl("https://ganjoor.net/x")) + } +}