Skip to content

Commit 1fc7e6c

Browse files
Add per-link preferred app selection for opening links
Adds an 'openWithPackage' field to the Deepr table that stores the package name of a preferred app for opening each link. When set, links will open directly in the chosen app instead of showing the Android app chooser. Changes: - Database: Add openWithPackage column with migration 10.sqm - SQL: Update insert, update, and select queries - Utils: openDeeplink() accepts optional package name - ShortcutUtils: Shortcuts use preferred app when set - AddLinkScreen: New "Open with App" dropdown selector - All import paths updated to include new column Co-authored-by: yogeshpaliyal <[email protected]>
1 parent 8716c71 commit 1fc7e6c

File tree

14 files changed

+181
-13
lines changed

14 files changed

+181
-13
lines changed

app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class CsvBookmarkImporter(
8383
isFavourite = isFavourite,
8484
createdAt = createdAt,
8585
profileId = profileID,
86+
openWithPackage = "",
8687
)
8788
val linkId = deeprQueries.lastInsertRowId().executeAsOne()
8889

app/src/main/java/com/yogeshpaliyal/deepr/data/LinkRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ interface LinkRepository {
8989
notes: String,
9090
thumbnail: String,
9191
profileId: Long,
92+
openWithPackage: String = "",
9293
)
9394

9495
suspend fun lastInsertRowId(): Long?
@@ -99,6 +100,7 @@ interface LinkRepository {
99100
notes: String,
100101
thumbnail: String,
101102
profileId: Long,
103+
openWithPackage: String,
102104
id: Long,
103105
)
104106

app/src/main/java/com/yogeshpaliyal/deepr/data/LinkRepositoryImpl.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ class LinkRepositoryImpl(
183183
notes: String,
184184
thumbnail: String,
185185
profileId: Long,
186+
openWithPackage: String,
186187
) {
187188
withContext(Dispatchers.IO) {
188-
deeprQueries.insertDeepr(link, name, openedCount, notes, thumbnail, profileId)
189+
deeprQueries.insertDeepr(link, name, openedCount, notes, thumbnail, profileId, openWithPackage)
189190
}
190191
scheduleAutoBackup()
191192
}
@@ -201,10 +202,11 @@ class LinkRepositoryImpl(
201202
notes: String,
202203
thumbnail: String,
203204
profileId: Long,
205+
openWithPackage: String,
204206
id: Long,
205207
) {
206208
withContext(Dispatchers.IO) {
207-
deeprQueries.updateDeeplink(newLink, newName, notes, thumbnail, profileId, id)
209+
deeprQueries.updateDeeplink(newLink, newName, notes, thumbnail, profileId, openWithPackage, id)
208210
}
209211
scheduleAutoBackup()
210212
}

app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ open class LocalServerRepositoryImpl(
430430
isFavourite = deeplink.isFavourite,
431431
createdAt = deeplink.createdAt,
432432
profileId = 1L, // Default profile
433+
openWithPackage = "",
433434
)
434435

435436
val insertedId = deeprQueries.lastInsertRowId().executeAsOne()

app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.yogeshpaliyal.deepr.ui.screens.addlink
22

3+
import android.content.Intent
4+
import android.content.pm.PackageManager
5+
import android.content.pm.ResolveInfo
36
import android.widget.Toast
47
import androidx.activity.compose.rememberLauncherForActivityResult
58
import androidx.compose.animation.AnimatedVisibility
@@ -63,6 +66,7 @@ import androidx.compose.ui.platform.LocalContext
6366
import androidx.compose.ui.res.stringResource
6467
import androidx.compose.ui.text.font.FontWeight
6568
import androidx.compose.ui.unit.dp
69+
import androidx.core.net.toUri
6670
import androidx.lifecycle.compose.collectAsStateWithLifecycle
6771
import coil3.compose.AsyncImage
6872
import com.journeyapps.barcodescanner.ScanOptions
@@ -81,6 +85,7 @@ import compose.icons.TablerIcons
8185
import compose.icons.tablericons.ArrowLeft
8286
import compose.icons.tablericons.Check
8387
import compose.icons.tablericons.Download
88+
import compose.icons.tablericons.ExternalLink
8489
import compose.icons.tablericons.Link
8590
import compose.icons.tablericons.Note
8691
import compose.icons.tablericons.Photo
@@ -213,6 +218,7 @@ fun AddLinkScreen(
213218
deeprInfo.notes,
214219
deeprInfo.thumbnail,
215220
selectedProfileId,
221+
deeprInfo.openWithPackage,
216222
)
217223
} else {
218224
// Edit
@@ -224,10 +230,11 @@ fun AddLinkScreen(
224230
deeprInfo.notes,
225231
deeprInfo.thumbnail,
226232
selectedProfileId,
233+
deeprInfo.openWithPackage,
227234
)
228235
}
229236
if (executeAfterSave) {
230-
openDeeplink(context, normalizedLink)
237+
openDeeplink(context, normalizedLink, deeprInfo.openWithPackage)
231238
}
232239
navigator.removeLast()
233240
}
@@ -637,6 +644,143 @@ fun AddLinkScreen(
637644
}
638645
}
639646

647+
// Open With App Section
648+
val resolvedApps = remember(deeprInfo.link) {
649+
if (isValidDeeplink(deeprInfo.link)) {
650+
val normalizedLink = normalizeLink(deeprInfo.link)
651+
val intent = Intent(Intent.ACTION_VIEW, normalizedLink.toUri())
652+
context.packageManager
653+
.queryIntentActivities(intent, PackageManager.MATCH_ALL)
654+
.distinctBy { it.activityInfo.packageName }
655+
} else {
656+
emptyList()
657+
}
658+
}
659+
660+
if (resolvedApps.isNotEmpty()) {
661+
ElevatedCard(
662+
modifier = Modifier.fillMaxWidth(),
663+
colors =
664+
CardDefaults.elevatedCardColors(
665+
containerColor = MaterialTheme.colorScheme.surface,
666+
),
667+
) {
668+
Column(
669+
modifier = Modifier.padding(16.dp),
670+
verticalArrangement = Arrangement.spacedBy(12.dp),
671+
) {
672+
Row(
673+
verticalAlignment = Alignment.CenterVertically,
674+
horizontalArrangement = Arrangement.spacedBy(8.dp),
675+
) {
676+
Icon(
677+
imageVector = TablerIcons.ExternalLink,
678+
contentDescription = null,
679+
tint = MaterialTheme.colorScheme.primary,
680+
)
681+
Text(
682+
text = stringResource(R.string.open_with_app),
683+
style = MaterialTheme.typography.titleMedium,
684+
fontWeight = FontWeight.SemiBold,
685+
)
686+
}
687+
688+
var openWithExpanded by remember { mutableStateOf(false) }
689+
val selectedAppLabel = remember(deeprInfo.openWithPackage, resolvedApps) {
690+
if (deeprInfo.openWithPackage.isEmpty()) {
691+
""
692+
} else {
693+
resolvedApps
694+
.firstOrNull { it.activityInfo.packageName == deeprInfo.openWithPackage }
695+
?.loadLabel(context.packageManager)
696+
?.toString()
697+
?: deeprInfo.openWithPackage
698+
}
699+
}
700+
701+
ExposedDropdownMenuBox(
702+
expanded = openWithExpanded,
703+
onExpandedChange = { openWithExpanded = !openWithExpanded },
704+
modifier = Modifier.fillMaxWidth(),
705+
) {
706+
OutlinedTextField(
707+
value = selectedAppLabel,
708+
onValueChange = {},
709+
readOnly = true,
710+
label = { Text(stringResource(R.string.select_app)) },
711+
placeholder = { Text(stringResource(R.string.system_default)) },
712+
modifier =
713+
Modifier
714+
.fillMaxWidth()
715+
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true),
716+
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = openWithExpanded) },
717+
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
718+
shape = RoundedCornerShape(12.dp),
719+
)
720+
721+
ExposedDropdownMenu(
722+
expanded = openWithExpanded,
723+
onDismissRequest = { openWithExpanded = false },
724+
) {
725+
// System default (no specific app)
726+
DropdownMenuItem(
727+
text = {
728+
Row(
729+
verticalAlignment = Alignment.CenterVertically,
730+
horizontalArrangement = Arrangement.spacedBy(8.dp),
731+
) {
732+
if (deeprInfo.openWithPackage.isEmpty()) {
733+
Icon(
734+
imageVector = TablerIcons.Check,
735+
contentDescription = null,
736+
modifier = Modifier.size(18.dp),
737+
tint = MaterialTheme.colorScheme.primary,
738+
)
739+
}
740+
Text(stringResource(R.string.system_default))
741+
}
742+
},
743+
onClick = {
744+
deeprInfo = deeprInfo.copy(openWithPackage = "")
745+
openWithExpanded = false
746+
},
747+
)
748+
749+
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
750+
751+
resolvedApps.forEach { resolveInfo ->
752+
val packageName = resolveInfo.activityInfo.packageName
753+
val appLabel = resolveInfo.loadLabel(context.packageManager).toString()
754+
755+
DropdownMenuItem(
756+
text = {
757+
Row(
758+
verticalAlignment = Alignment.CenterVertically,
759+
horizontalArrangement = Arrangement.spacedBy(8.dp),
760+
) {
761+
if (packageName == deeprInfo.openWithPackage) {
762+
Icon(
763+
imageVector = TablerIcons.Check,
764+
contentDescription = null,
765+
modifier = Modifier.size(18.dp),
766+
tint = MaterialTheme.colorScheme.primary,
767+
)
768+
}
769+
Text(appLabel)
770+
}
771+
},
772+
onClick = {
773+
deeprInfo = deeprInfo.copy(openWithPackage = packageName)
774+
openWithExpanded = false
775+
},
776+
)
777+
}
778+
}
779+
}
780+
}
781+
}
782+
}
783+
640784
// Tags Section
641785
ElevatedCard(
642786
modifier = Modifier.fillMaxWidth(),
@@ -889,7 +1033,7 @@ fun AddLinkScreen(
8891033
FilledTonalButton(
8901034
modifier = Modifier.weight(1f),
8911035
onClick = {
892-
isError = !openDeeplink(context, deeprInfo.link)
1036+
isError = !openDeeplink(context, deeprInfo.link, deeprInfo.openWithPackage)
8931037
},
8941038
shape = RoundedCornerShape(12.dp),
8951039
) {

app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ fun Content(
625625
when (it) {
626626
is Click -> {
627627
viewModel.incrementOpenedCount(it.item.id)
628-
openDeeplink(context, it.item.link)
628+
openDeeplink(context, it.item.link, it.item.openWithPackage)
629629
analyticsManager.logEvent(
630630
AnalyticsEvents.OPEN_LINK,
631631
mapOf(AnalyticsParams.LINK_ID to it.item.id),

app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/SaveCompleteDialog.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ fun createDeeprObject(
1414
notes: String = "",
1515
thumbnail: String = "",
1616
profileId: Long = 1L,
17+
openWithPackage: String = "",
1718
): GetLinksAndTags =
1819
GetLinksAndTags(
1920
id = 0,
@@ -28,4 +29,5 @@ fun createDeeprObject(
2829
notes = notes,
2930
thumbnail = thumbnail,
3031
profileId = profileId,
32+
openWithPackage = openWithPackage,
3133
)

app/src/main/java/com/yogeshpaliyal/deepr/util/ShortcutUtils.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ fun createShortcut(
1717
useLinkBasedIcon: Boolean,
1818
) {
1919
if (isShortcutSupported(context)) {
20+
val intent = Intent(Intent.ACTION_VIEW, deepr.link.toUri())
21+
if (deepr.openWithPackage.isNotEmpty()) {
22+
intent.setPackage(deepr.openWithPackage)
23+
}
2024
val shortcutInfo =
2125
ShortcutInfoCompat
2226
.Builder(context, "deepr_${deepr.id}")
2327
.setShortLabel(shortcutName)
2428
.setLongLabel(shortcutName)
2529
.setIcon(getShortcutAppIcon(context, deepr.link, useLinkBasedIcon))
26-
.setIntent(
27-
Intent(Intent.ACTION_VIEW, deepr.link.toUri()),
28-
).build()
30+
.setIntent(intent)
31+
.build()
2932
if (alreadyExists) {
3033
// If the shortcut already exists, we update it
3134
ShortcutManagerCompat.updateShortcuts(context, listOf(shortcutInfo))

app/src/main/java/com/yogeshpaliyal/deepr/util/Utils.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ import java.util.Locale
1616
fun openDeeplink(
1717
context: Context,
1818
link: String,
19+
packageName: String = "",
1920
): Boolean {
2021
if (!isValidDeeplink(link)) return false
2122
val normalizedLink = normalizeLink(link)
2223
return try {
2324
val intent = Intent(Intent.ACTION_VIEW, normalizedLink.toUri())
25+
if (packageName.isNotEmpty()) {
26+
intent.setPackage(packageName)
27+
}
2428
context.startActivity(intent)
2529
true
2630
} catch (e: Exception) {

app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,11 @@ class AccountViewModel(
412412
notes: String = "",
413413
thumbnail: String = "",
414414
profileId: Long? = null,
415+
openWithPackage: String = "",
415416
) {
416417
viewModelScope.launch(Dispatchers.IO) {
417418
val targetProfileId = profileId ?: selectedProfileId.first()
418-
linkRepository.insertDeepr(link = link, name, if (executed) 1 else 0, notes, thumbnail, targetProfileId)
419+
linkRepository.insertDeepr(link = link, name, if (executed) 1 else 0, notes, thumbnail, targetProfileId, openWithPackage)
419420
linkRepository.lastInsertRowId()?.let {
420421
modifyTagsForLink(it, tagsList)
421422
analyticsManager.logEvent(
@@ -520,10 +521,11 @@ class AccountViewModel(
520521
notes: String = "",
521522
thumbnail: String = "",
522523
profileId: Long? = null,
524+
openWithPackage: String = "",
523525
) {
524526
viewModelScope.launch(Dispatchers.IO) {
525527
val targetProfileId = profileId ?: selectedProfileId.first()
526-
linkRepository.updateDeeplink(newLink, newName, notes, thumbnail, targetProfileId, id)
528+
linkRepository.updateDeeplink(newLink, newName, notes, thumbnail, targetProfileId, openWithPackage, id)
527529
modifyTagsForLink(id, tagsList)
528530
syncToMarkdown()
529531
analyticsManager.logEvent(

0 commit comments

Comments
 (0)