Skip to content

Commit c34a4d0

Browse files
authored
Merge pull request #246 from rainxchzed/deeplink
feat(app): Implement deep linking for repository details
2 parents cdb429a + aae5594 commit c34a4d0

30 files changed

Lines changed: 694 additions & 61 deletions

File tree

composeApp/build.gradle.kts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,23 @@ compose.desktop {
121121
macOS {
122122
iconFile.set(project.file("logo/app_icon.icns"))
123123
bundleID = "zed.rainxch.githubstore"
124+
125+
// Register githubstore:// URI scheme so macOS opens the app for deep links
126+
infoPlist {
127+
extraKeysRawXml = """
128+
<key>CFBundleURLTypes</key>
129+
<array>
130+
<dict>
131+
<key>CFBundleURLName</key>
132+
<string>GitHub Store Deep Link</string>
133+
<key>CFBundleURLSchemes</key>
134+
<array>
135+
<string>githubstore</string>
136+
</array>
137+
</dict>
138+
</array>
139+
""".trimIndent()
140+
}
124141
}
125142
linux {
126143
iconFile.set(project.file("logo/app_icon.png"))

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@
2727
<!-- Expose cache files via FileProvider for APK install -->
2828
<activity
2929
android:name=".MainActivity"
30-
android:exported="true">
30+
android:exported="true"
31+
android:launchMode="singleTask">
3132
<intent-filter>
3233
<action android:name="android.intent.action.MAIN" />
3334

3435
<category android:name="android.intent.category.LAUNCHER" />
3536
</intent-filter>
3637

38+
<!-- Auth callback (existing) -->
3739
<intent-filter>
3840
<action android:name="android.intent.action.VIEW" />
3941

@@ -44,6 +46,44 @@
4446
android:host="callback"
4547
android:scheme="githubstore" />
4648
</intent-filter>
49+
50+
<!-- Custom scheme: githubstore://repo/{owner}/{repo} -->
51+
<intent-filter>
52+
<action android:name="android.intent.action.VIEW" />
53+
54+
<category android:name="android.intent.category.DEFAULT" />
55+
<category android:name="android.intent.category.BROWSABLE" />
56+
57+
<data
58+
android:host="repo"
59+
android:scheme="githubstore" />
60+
</intent-filter>
61+
62+
<!-- GitHub repository links: https://github.com/{owner}/{repo} -->
63+
<intent-filter>
64+
<action android:name="android.intent.action.VIEW" />
65+
66+
<category android:name="android.intent.category.DEFAULT" />
67+
<category android:name="android.intent.category.BROWSABLE" />
68+
69+
<data
70+
android:host="github.com"
71+
android:pathPattern="/.*/..*"
72+
android:scheme="https" />
73+
</intent-filter>
74+
75+
<!-- App website links: https://github-store.org/app/?repo={owner}/{repo} -->
76+
<intent-filter>
77+
<action android:name="android.intent.action.VIEW" />
78+
79+
<category android:name="android.intent.category.DEFAULT" />
80+
<category android:name="android.intent.category.BROWSABLE" />
81+
82+
<data
83+
android:host="github-store.org"
84+
android:pathPrefix="/app/"
85+
android:scheme="https" />
86+
</intent-filter>
4787
</activity>
4888

4989
<provider

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,46 @@
11
package zed.rainxch.githubstore
22

3+
import android.content.Intent
34
import android.os.Bundle
45
import androidx.activity.ComponentActivity
56
import androidx.activity.compose.setContent
67
import androidx.activity.enableEdgeToEdge
78
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.DisposableEffect
10+
import androidx.compose.runtime.getValue
11+
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.runtime.setValue
813
import androidx.compose.ui.tooling.preview.Preview
914
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
15+
import androidx.core.util.Consumer
1016

1117
class MainActivity : ComponentActivity() {
18+
19+
private var deepLinkUri by mutableStateOf<String?>(null)
20+
1221
override fun onCreate(savedInstanceState: Bundle?) {
1322
installSplashScreen()
1423

1524
enableEdgeToEdge()
1625

1726
super.onCreate(savedInstanceState)
1827

28+
deepLinkUri = intent?.data?.toString()
29+
1930
setContent {
20-
App()
31+
DisposableEffect(Unit) {
32+
val listener = Consumer<Intent> { newIntent ->
33+
newIntent.data?.toString()?.let {
34+
deepLinkUri = it
35+
}
36+
}
37+
addOnNewIntentListener(listener)
38+
onDispose {
39+
removeOnNewIntentListener(listener)
40+
}
41+
}
42+
43+
App(deepLinkUri = deepLinkUri)
2144
}
2245
}
2346
}

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,47 @@ package zed.rainxch.githubstore
33
import androidx.compose.foundation.isSystemInDarkTheme
44
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
55
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.LaunchedEffect
67
import androidx.compose.runtime.getValue
78
import androidx.lifecycle.compose.collectAsStateWithLifecycle
89
import androidx.navigation.compose.rememberNavController
910
import org.jetbrains.compose.ui.tooling.preview.Preview
1011
import org.koin.compose.viewmodel.koinViewModel
1112
import zed.rainxch.core.presentation.theme.GithubStoreTheme
1213
import zed.rainxch.core.presentation.utils.ApplyAndroidSystemBars
14+
import zed.rainxch.githubstore.app.deeplink.DeepLinkDestination
15+
import zed.rainxch.githubstore.app.deeplink.DeepLinkParser
1316
import zed.rainxch.githubstore.app.navigation.AppNavigation
1417
import zed.rainxch.githubstore.app.navigation.GithubStoreGraph
1518
import zed.rainxch.githubstore.app.components.RateLimitDialog
1619

1720
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
1821
@Composable
1922
@Preview
20-
fun App() {
23+
fun App(deepLinkUri: String? = null) {
2124
val viewModel: MainViewModel = koinViewModel()
2225
val state by viewModel.state.collectAsStateWithLifecycle()
2326

2427
val navBackStack = rememberNavController()
2528

29+
LaunchedEffect(deepLinkUri) {
30+
deepLinkUri?.let { uri ->
31+
when (val destination = DeepLinkParser.parse(uri)) {
32+
is DeepLinkDestination.Repository -> {
33+
navBackStack.navigate(
34+
GithubStoreGraph.DetailsScreen(
35+
owner = destination.owner,
36+
repo = destination.repo
37+
)
38+
)
39+
}
40+
41+
DeepLinkDestination.None -> { /* ignore unrecognized deep links */
42+
}
43+
}
44+
}
45+
}
46+
2647
GithubStoreTheme(
2748
fontTheme = state.currentFontTheme,
2849
appTheme = state.currentColorTheme,
@@ -31,19 +52,21 @@ fun App() {
3152
) {
3253
ApplyAndroidSystemBars(state.isDarkTheme)
3354

34-
if (state.showRateLimitDialog && state.rateLimitInfo != null) {
35-
RateLimitDialog(
36-
rateLimitInfo = state.rateLimitInfo,
37-
isAuthenticated = state.isLoggedIn,
38-
onDismiss = {
39-
viewModel.onAction(MainAction.DismissRateLimitDialog)
40-
},
41-
onSignIn = {
42-
viewModel.onAction(MainAction.DismissRateLimitDialog)
43-
44-
navBackStack.navigate(GithubStoreGraph.AuthenticationScreen)
45-
}
46-
)
55+
if (state.showRateLimitDialog) {
56+
state.rateLimitInfo?.let {
57+
RateLimitDialog(
58+
rateLimitInfo = it,
59+
isAuthenticated = state.isLoggedIn,
60+
onDismiss = {
61+
viewModel.onAction(MainAction.DismissRateLimitDialog)
62+
},
63+
onSignIn = {
64+
viewModel.onAction(MainAction.DismissRateLimitDialog)
65+
66+
navBackStack.navigate(GithubStoreGraph.AuthenticationScreen)
67+
}
68+
)
69+
}
4770
}
4871

4972
AppNavigation(

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/RateLimitDialog.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ import zed.rainxch.githubstore.core.presentation.res.*
2525

2626
@Composable
2727
fun RateLimitDialog(
28-
rateLimitInfo: RateLimitInfo?,
28+
rateLimitInfo: RateLimitInfo,
2929
isAuthenticated: Boolean,
3030
onDismiss: () -> Unit,
3131
onSignIn: () -> Unit
3232
) {
3333
val timeUntilReset = remember(rateLimitInfo) {
34-
rateLimitInfo?.timeUntilReset()?.inWholeMinutes?.toInt()
34+
rateLimitInfo.timeUntilReset().inWholeMinutes.toInt()
3535
}
3636

3737
AlertDialog(
@@ -59,12 +59,12 @@ fun RateLimitDialog(
5959
text = if (isAuthenticated) {
6060
stringResource(
6161
Res.string.rate_limit_used_all,
62-
rateLimitInfo?.limit ?: 0
62+
rateLimitInfo.limit
6363
)
6464
} else {
6565
stringResource(
6666
Res.string.rate_limit_used_all_free,
67-
rateLimitInfo?.limit ?: 0
67+
60
6868
)
6969
},
7070
style = MaterialTheme.typography.bodyMedium,
@@ -74,7 +74,7 @@ fun RateLimitDialog(
7474
Text(
7575
text = stringResource(
7676
Res.string.rate_limit_resets_in_minutes,
77-
timeUntilReset ?: 0
77+
timeUntilReset
7878
),
7979
style = MaterialTheme.typography.bodyMedium,
8080
fontWeight = FontWeight.Bold,
@@ -83,6 +83,7 @@ fun RateLimitDialog(
8383

8484
if (!isAuthenticated) {
8585
Spacer(modifier = Modifier.height(8.dp))
86+
8687
Text(
8788
text = stringResource(Res.string.rate_limit_tip_sign_in),
8889
style = MaterialTheme.typography.bodySmall,
@@ -127,7 +128,11 @@ fun RateLimitDialog(
127128
fun RateLimitDialogPreview() {
128129
GithubStoreTheme {
129130
RateLimitDialog(
130-
rateLimitInfo = null,
131+
rateLimitInfo = RateLimitInfo(
132+
limit = 1000,
133+
remaining = 2000,
134+
resetTimestamp = 0L,
135+
),
131136
isAuthenticated = false,
132137
onDismiss = {
133138

0 commit comments

Comments
 (0)