Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- android:configChanges="uiMode" is added to prevent the activity from recreating itself
during night mode change, avoiding unnecessary state resets wherever possible.

android:documentLaunchMode="intoExisting" is added to prevent the activity from stacking
on top of the current app while sharing images (if no existing instance of Breadboard was
found) on older Android versions. -->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -12,29 +18,32 @@
android:supportsRtl="true"
android:theme="@style/Theme.Breadboard"
android:windowSoftInputMode="adjustResize"
android:enableOnBackInvokedCallback="true">
android:enableOnBackInvokedCallback="true"
android:configChanges="uiMode"
android:documentLaunchMode="intoExisting">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:taskAffinity="moe.apex.breadboard.MainActivity"
android:exported="true"
android:theme="@style/Theme.Breadboard"
android:configChanges="uiMode"> <!-- Add "uiMode" to "configChanges" to prevent the
activity from recreating itself during night mode
change, avoiding unnecessary state resets wherever
possible. -->
android:theme="@style/Theme.Breadboard">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="SauceNAO">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
android:name=".DeepLinkActivity"
android:taskAffinity="moe.apex.breadboard.DeepLinkActivity"
android:launchMode="singleTop"
android:exported="true"
android:theme="@style/Theme.Breadboard"
android:configChanges="uiMode">
android:theme="@style/Theme.Breadboard">

<intent-filter>
<action android:name="android.intent.action.VIEW" />
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/moe/apex/breadboard/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import moe.apex.breadboard.navigation.Favourites
import moe.apex.breadboard.navigation.Home
import moe.apex.breadboard.navigation.Navigation
import moe.apex.breadboard.navigation.Results
import moe.apex.breadboard.navigation.ReverseSearch
import moe.apex.breadboard.navigation.Search
import moe.apex.breadboard.preferences.DarkTheme
import moe.apex.breadboard.preferences.ImageSource
Expand Down Expand Up @@ -90,6 +91,19 @@ class MainActivity : SingletonImageLoader.Factory, ComponentActivity(), VolumeBu


private fun determineDestination(intent: Intent): Any? {
// Handle Intent.ACTION_SEND intent for reverse search
if (intent.action == Intent.ACTION_SEND) {
/* Sometimes a shared link may contain an image clipData of the favicon, so we must
prioritise the text over the clipData. Providing them both would be bad. */
intent.extras?.getString(Intent.EXTRA_TEXT)?.let { initialImageUrl ->
return ReverseSearch(initialImageUrl = initialImageUrl)
}

intent.clipData?.getItemAt(0)?.uri?.let { initialFileUri ->
return ReverseSearch(initialFileUri = initialFileUri.toString())
}
}

return when (intent.getStringExtra("destination")) {
"artist" -> maybePrepareArtistDestination(intent)
"search" -> maybePrepareResultsDestination(intent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ object FollowedArtists
object Search

@Serializable
object ReverseSearch
data class ReverseSearch(val initialImageUrl: String? = null, val initialFileUri: String? = null)

@Serializable
data class Results(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ fun Navigation(navController: NavHostController, startDestination: Any = Search)
},
onClick = {
if (!currentRoute.routeIs(ReverseSearch::class)) {
navController.navigate(ReverseSearch) {
popUpTo(ReverseSearch) { inclusive = true }
navController.navigate(ReverseSearch()) {
popUpTo(ReverseSearch()) { inclusive = true }
}
}
}
Expand Down Expand Up @@ -288,7 +288,10 @@ fun Navigation(navController: NavHostController, startDestination: Any = Search)
}
composable<Home> { HomeScreen(navController) { isNavigationBarVisible = it } }
composable<Search> { SearchScreen(navController, focusRequester) }
composable<ReverseSearch> { ReverseSearchScreen(navController) }
composable<ReverseSearch> {
val args = it.toRoute<ReverseSearch>()
ReverseSearchScreen(navController, args.initialImageUrl, args.initialFileUri)
}
composable<Results> {
val args = it.toRoute<Results>()
SearchResults(navController, args.source, args.tags)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,18 @@ import moe.apex.breadboard.util.showToast

@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun ReverseSearchScreen(navController: NavController) {
fun ReverseSearchScreen(
navController: NavController,
initialImageUrl: String? = null,
initialFileUri: String? = null
) {
val context = LocalContext.current
val clipboard = LocalClipboard.current
val prefs = LocalPreferences.current
val scope = rememberCoroutineScope()

var imageUrl by rememberSaveable { mutableStateOf("") }
var selectedFileUri by rememberSaveable { mutableStateOf<Uri?>(null) }
var imageUrl by rememberSaveable { mutableStateOf(initialImageUrl ?: "") }
var selectedFileUri by rememberSaveable { mutableStateOf(initialFileUri?.toUri()) }

val imagePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
Expand Down Expand Up @@ -197,13 +201,26 @@ fun ReverseSearchScreen(navController: NavController) {
IconButton(
onClick = {
scope.launch {
val clipboardText =
val clipboardItem =
clipboard.nativeClipboard.primaryClip?.getItemAt(
0
)?.text?.toString()
if (clipboardText != null) {
imageUrl = clipboardText
selectedFileUri = null
)

if (clipboardItem != null) {
val uri = clipboardItem.uri
val mimeType = uri?.let {
context.contentResolver.getType(it)
}

if (mimeType?.startsWith("image/") == true) {
selectedFileUri = uri
} else {
/* onValueChange does not trigger when you mutate
imageUrl via code, so we still have to reset
selectedFileUri here. */
selectedFileUri = null
imageUrl = clipboardItem.text.toString()
}
} else {
showToast(context, "Nothing on the clipboard")
}
Expand Down
Loading