Skip to content

Commit fca8393

Browse files
committed
feat(ui): implement repository sharing functionality
This commit introduces a sharing feature to repository cards, allowing users to share repository links directly from the home and search screens. It also integrates snackbar notifications to provide feedback on sharing actions. - **feat(ui)**: Added a share button to `RepositoryCard` with a new `onShareClick` callback. - **feat(home)**: Integrated `ShareManager` into `HomeViewModel` and added `OnShareClick` action to handle repository link generation and sharing. - **feat(search)**: Integrated `ShareManager` into `SearchViewModel` and added `OnShareClick` action to handle repository link generation and sharing. - **feat(presentation)**: Added `SnackbarHost` to `HomeScreen` and `SearchScreen` to display success/error messages during sharing. - **feat(presentation)**: Introduced `SearchEvent` and updated `HomeEvent` to include `OnMessage` for triggering snackbar notifications from ViewModels. - **refactor(ui)**: Adjusted `SnackbarHost` positioning in `Scaffold` to account for bottom navigation height. - **fix(presentation)**: Improved sharing logic to provide a "link copied" message on non-Android platforms when a link is shared.
1 parent 50bdb92 commit fca8393

9 files changed

Lines changed: 138 additions & 16 deletions

File tree

core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import androidx.compose.material.icons.automirrored.outlined.CallSplit
2020
import androidx.compose.material.icons.filled.CheckCircle
2121
import androidx.compose.material.icons.filled.Favorite
2222
import androidx.compose.material.icons.filled.OpenInBrowser
23+
import androidx.compose.material.icons.filled.Share
2324
import androidx.compose.material.icons.filled.Star
2425
import androidx.compose.material.icons.filled.Update
25-
import androidx.compose.material3.Card
26-
import androidx.compose.material3.CardDefaults
2726
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
2827
import androidx.compose.material3.Icon
2928
import androidx.compose.material3.IconButton
@@ -48,22 +47,19 @@ import zed.rainxch.core.presentation.model.DiscoveryRepository
4847
import zed.rainxch.core.presentation.theme.GithubStoreTheme
4948
import zed.rainxch.core.presentation.utils.formatReleasedAt
5049
import zed.rainxch.core.presentation.utils.hasWeekNotPassed
51-
import zed.rainxch.githubstore.core.presentation.res.Res
52-
import zed.rainxch.githubstore.core.presentation.res.forked_repository
53-
import zed.rainxch.githubstore.core.presentation.res.home_view_details
54-
import zed.rainxch.githubstore.core.presentation.res.installed
55-
import zed.rainxch.githubstore.core.presentation.res.open_in_browser
56-
import zed.rainxch.githubstore.core.presentation.res.update_available
50+
import zed.rainxch.githubstore.core.presentation.res.*
5751

5852
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class)
5953
@Composable
6054
fun RepositoryCard(
6155
discoveryRepository: DiscoveryRepository,
6256
onClick: () -> Unit,
57+
onShareClick: () -> Unit,
6358
onDeveloperClick: (String) -> Unit,
6459
modifier: Modifier = Modifier
6560
) {
6661
val uriHandler = LocalUriHandler.current
62+
6763
ExpressiveCard(
6864
onClick = onClick,
6965
modifier = modifier
@@ -263,6 +259,20 @@ fun RepositoryCard(
263259
modifier = Modifier.weight(1f)
264260
)
265261

262+
IconButton(
263+
onClick = onShareClick,
264+
colors = IconButtonDefaults.iconButtonColors(
265+
containerColor = MaterialTheme.colorScheme.primaryContainer,
266+
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
267+
),
268+
shapes = IconButtonDefaults.shapes(),
269+
) {
270+
Icon(
271+
imageVector = Icons.Default.Share,
272+
contentDescription = stringResource(Res.string.share_repository),
273+
)
274+
}
275+
266276
IconButton(
267277
onClick = {
268278
uriHandler.openUri(discoveryRepository.repository.htmlUrl)
@@ -421,6 +431,7 @@ fun RepositoryCardPreview() {
421431
isStarred = false
422432
),
423433
onClick = { },
434+
onShareClick = { },
424435
onDeveloperClick = { }
425436
)
426437
}

feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ sealed interface HomeAction {
1010
data object OnSearchClick : HomeAction
1111
data object OnSettingsClick : HomeAction
1212
data object OnAppsClick : HomeAction
13+
data class OnShareClick (val repo: GithubRepoSummary) : HomeAction
1314
data class SwitchCategory(val category: HomeCategory) : HomeAction
1415
data class OnRepositoryClick(val repo: GithubRepoSummary) : HomeAction
1516
data class OnRepositoryDeveloperClick(val username: String) : HomeAction

feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeEvent.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package zed.rainxch.home.presentation
22

33
sealed interface HomeEvent {
44
data object OnScrollToListTop : HomeEvent
5+
data class OnMessage(val message: String) : HomeEvent
56
}

feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
2626
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
2727
import androidx.compose.material3.MaterialTheme
2828
import androidx.compose.material3.Scaffold
29+
import androidx.compose.material3.SnackbarHost
30+
import androidx.compose.material3.SnackbarHostState
2931
import androidx.compose.material3.Text
3032
import androidx.compose.material3.TopAppBar
3133
import androidx.compose.runtime.Composable
@@ -58,6 +60,7 @@ import org.koin.compose.viewmodel.koinViewModel
5860
import zed.rainxch.core.domain.model.GithubRepoSummary
5961
import zed.rainxch.core.presentation.components.GithubStoreButton
6062
import zed.rainxch.core.presentation.components.RepositoryCard
63+
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
6164
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
6265
import zed.rainxch.core.presentation.theme.GithubStoreTheme
6366
import zed.rainxch.core.presentation.utils.ObserveAsEvents
@@ -77,6 +80,7 @@ fun HomeRoot(
7780
val state by viewModel.state.collectAsStateWithLifecycle()
7881
val listState = rememberLazyStaggeredGridState()
7982
val scope = rememberCoroutineScope()
83+
val snackbarHost = remember { SnackbarHostState() }
8084

8185
ObserveAsEvents(viewModel.events) { event ->
8286
when (event) {
@@ -85,11 +89,18 @@ fun HomeRoot(
8589
listState.animateScrollToItem(0)
8690
}
8791
}
92+
93+
is HomeEvent.OnMessage -> {
94+
scope.launch {
95+
snackbarHost.showSnackbar(event.message)
96+
}
97+
}
8898
}
8999
}
90100

91101
HomeScreen(
92102
state = state,
103+
snackbarHost = snackbarHost,
93104
onAction = { action ->
94105
when (action) {
95106
HomeAction.OnSearchClick -> {
@@ -125,10 +136,12 @@ fun HomeRoot(
125136
@Composable
126137
fun HomeScreen(
127138
state: HomeState,
139+
snackbarHost: SnackbarHostState,
128140
onAction: (HomeAction) -> Unit,
129141
listState: LazyStaggeredGridState,
130142
) {
131143
val liquidState = LocalBottomNavigationLiquid.current
144+
val bottomNavHeight = LocalBottomNavigationHeight.current
132145

133146
val shouldLoadMore by remember {
134147
derivedStateOf {
@@ -162,6 +175,12 @@ fun HomeScreen(
162175
topBar = {
163176
TopAppBar()
164177
},
178+
snackbarHost = {
179+
SnackbarHost(
180+
hostState = snackbarHost,
181+
modifier = Modifier.padding(bottom = bottomNavHeight + 16.dp)
182+
)
183+
},
165184
containerColor = MaterialTheme.colorScheme.background
166185
) { innerPadding ->
167186
Column(
@@ -213,15 +232,18 @@ private fun MainState(
213232
items = state.repos,
214233
key = { it.repository.id },
215234
contentType = { "repo" }
216-
) { homeRepo ->
235+
) { discoveryRepository ->
217236
RepositoryCard(
218-
discoveryRepository = homeRepo,
237+
discoveryRepository = discoveryRepository,
219238
onClick = {
220-
onAction(HomeAction.OnRepositoryClick(homeRepo.repository))
239+
onAction(HomeAction.OnRepositoryClick(discoveryRepository.repository))
221240
},
222241
onDeveloperClick = { username ->
223242
onAction(HomeAction.OnRepositoryDeveloperClick(username))
224243
},
244+
onShareClick = {
245+
onAction(HomeAction.OnShareClick(discoveryRepository.repository))
246+
},
225247
modifier = Modifier
226248
.animateItem()
227249
.liquefiable(bottomNavLiquidState)
@@ -389,7 +411,8 @@ private fun Preview() {
389411
HomeScreen(
390412
state = HomeState(),
391413
onAction = {},
392-
listState = rememberLazyStaggeredGridState()
414+
snackbarHost = SnackbarHostState(),
415+
listState = rememberLazyStaggeredGridState(),
393416
)
394417
}
395418
}

feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import zed.rainxch.core.domain.repository.FavouritesRepository
2020
import zed.rainxch.core.domain.repository.InstalledAppsRepository
2121
import zed.rainxch.core.domain.repository.StarredRepository
2222
import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase
23+
import zed.rainxch.core.domain.utils.ShareManager
2324
import zed.rainxch.core.presentation.model.DiscoveryRepository
2425
import zed.rainxch.githubstore.core.presentation.res.*
2526
import zed.rainxch.home.domain.repository.HomeRepository
@@ -32,7 +33,8 @@ class HomeViewModel(
3233
private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase,
3334
private val favouritesRepository: FavouritesRepository,
3435
private val starredRepository: StarredRepository,
35-
private val logger: GitHubStoreLogger
36+
private val logger: GitHubStoreLogger,
37+
private val shareManager: ShareManager
3638
) : ViewModel() {
3739

3840
private var hasLoadedInitialData = false
@@ -251,6 +253,24 @@ class HomeViewModel(
251253
}
252254
}
253255

256+
is HomeAction.OnShareClick -> {
257+
viewModelScope.launch {
258+
runCatching {
259+
shareManager.shareText("https://github-store.org/app?repo=${action.repo.fullName}")
260+
}.onFailure { t ->
261+
logger.error("Failed to share link: ${t.message}")
262+
_events.send(
263+
HomeEvent.OnMessage(getString(Res.string.failed_to_share_link))
264+
)
265+
return@launch
266+
}
267+
268+
if (platform != Platform.ANDROID) {
269+
_events.send(HomeEvent.OnMessage(getString(Res.string.link_copied_to_clipboard)))
270+
}
271+
}
272+
}
273+
254274
is HomeAction.OnRepositoryClick -> {
255275
/* Handled in composable */
256276
}

feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ sealed interface SearchAction {
1212
data class OnSortBySelected(val sortBy: SortBy) : SearchAction
1313
data class OnRepositoryClick(val repository: GithubRepoSummary) : SearchAction
1414
data class OnRepositoryDeveloperClick(val username: String) : SearchAction
15+
data class OnShareClick (val repo: GithubRepoSummary) : SearchAction
1516
data object OnSearchImeClick : SearchAction
1617
data object OnNavigateBackClick : SearchAction
1718
data object LoadMore : SearchAction
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package zed.rainxch.search.presentation
2+
3+
sealed interface SearchEvent {
4+
data class OnMessage(val message: String) : SearchEvent
5+
}

feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import androidx.compose.material3.Icon
3636
import androidx.compose.material3.IconButton
3737
import androidx.compose.material3.MaterialTheme
3838
import androidx.compose.material3.Scaffold
39+
import androidx.compose.material3.SnackbarHost
40+
import androidx.compose.material3.SnackbarHostState
3941
import androidx.compose.material3.Text
4042
import androidx.compose.material3.TextField
4143
import androidx.compose.material3.TextFieldDefaults
@@ -44,6 +46,7 @@ import androidx.compose.runtime.LaunchedEffect
4446
import androidx.compose.runtime.derivedStateOf
4547
import androidx.compose.runtime.getValue
4648
import androidx.compose.runtime.remember
49+
import androidx.compose.runtime.rememberCoroutineScope
4750
import androidx.compose.runtime.rememberUpdatedState
4851
import androidx.compose.ui.Alignment
4952
import androidx.compose.ui.Modifier
@@ -61,12 +64,15 @@ import io.github.fletchmckee.liquid.liquefiable
6164
import kotlinx.coroutines.delay
6265
import org.jetbrains.compose.resources.stringResource
6366
import androidx.compose.ui.tooling.preview.Preview
67+
import kotlinx.coroutines.launch
6468
import org.koin.compose.viewmodel.koinViewModel
6569
import zed.rainxch.core.domain.model.GithubRepoSummary
6670
import zed.rainxch.core.presentation.components.GithubStoreButton
6771
import zed.rainxch.core.presentation.components.RepositoryCard
72+
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
6873
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
6974
import zed.rainxch.core.presentation.theme.GithubStoreTheme
75+
import zed.rainxch.core.presentation.utils.ObserveAsEvents
7076
import zed.rainxch.domain.model.ProgrammingLanguage
7177
import zed.rainxch.domain.model.SearchPlatform
7278
import zed.rainxch.githubstore.core.presentation.res.Res
@@ -86,6 +92,18 @@ fun SearchRoot(
8692
viewModel: SearchViewModel = koinViewModel()
8793
) {
8894
val state by viewModel.state.collectAsStateWithLifecycle()
95+
val scope = rememberCoroutineScope()
96+
val snackbarHost = remember { SnackbarHostState() }
97+
98+
ObserveAsEvents(viewModel.events) { event ->
99+
when (event) {
100+
is SearchEvent.OnMessage -> {
101+
scope.launch {
102+
snackbarHost.showSnackbar(event.message)
103+
}
104+
}
105+
}
106+
}
89107

90108
if (state.isLanguageSheetVisible) {
91109
LanguageFilterBottomSheet(
@@ -101,6 +119,7 @@ fun SearchRoot(
101119

102120
SearchScreen(
103121
state = state,
122+
snackbarHost = snackbarHost,
104123
onAction = { action ->
105124
when (action) {
106125
is SearchAction.OnRepositoryClick -> {
@@ -127,11 +146,13 @@ fun SearchRoot(
127146
@Composable
128147
fun SearchScreen(
129148
state: SearchState,
149+
snackbarHost: SnackbarHostState,
130150
onAction: (SearchAction) -> Unit,
131151
) {
132152
val focusRequester = remember { FocusRequester() }
133153
val listState = rememberLazyStaggeredGridState()
134154
val liquidState = LocalBottomNavigationLiquid.current
155+
val bottomNavHeight = LocalBottomNavigationHeight.current
135156

136157
val shouldLoadMore by remember {
137158
derivedStateOf {
@@ -142,7 +163,8 @@ fun SearchScreen(
142163
if (totalItems == 0 ||
143164
state.isLoadingMore ||
144165
state.isLoading ||
145-
!state.hasMorePages) {
166+
!state.hasMorePages
167+
) {
146168
return@derivedStateOf false
147169
}
148170

@@ -176,7 +198,8 @@ fun SearchScreen(
176198
layoutInfo.totalItemsCount > 0 &&
177199
!state.isLoadingMore &&
178200
!state.isLoading &&
179-
state.hasMorePages) {
201+
state.hasMorePages
202+
) {
180203

181204
val hasEmptySpace = lastVisible.index == layoutInfo.totalItemsCount - 1 &&
182205
lastVisible.offset.y + lastVisible.size.height < layoutInfo.viewportEndOffset
@@ -202,6 +225,12 @@ fun SearchScreen(
202225
focusRequester = focusRequester
203226
)
204227
},
228+
snackbarHost = {
229+
SnackbarHost(
230+
hostState = snackbarHost,
231+
modifier = Modifier.padding(bottom = bottomNavHeight + 16.dp)
232+
)
233+
},
205234
containerColor = MaterialTheme.colorScheme.background,
206235
modifier = Modifier.liquefiable(liquidState)
207236
) { innerPadding ->
@@ -358,6 +387,9 @@ fun SearchScreen(
358387
onDeveloperClick = { username ->
359388
onAction(SearchAction.OnRepositoryDeveloperClick(username))
360389
},
390+
onShareClick = {
391+
onAction(SearchAction.OnShareClick(discoveryRepository.repository))
392+
},
361393
modifier = Modifier
362394
.animateItem()
363395
.liquefiable(liquidState)
@@ -467,7 +499,8 @@ private fun Preview() {
467499
GithubStoreTheme {
468500
SearchScreen(
469501
state = SearchState(),
470-
onAction = {}
502+
snackbarHost = SnackbarHostState(),
503+
onAction = {},
471504
)
472505
}
473506
}

0 commit comments

Comments
 (0)