Skip to content

Commit f0f01ab

Browse files
authored
Add long press to article images (#1657)
1 parent 9d1ee5b commit f0f01ab

10 files changed

Lines changed: 266 additions & 34 deletions

File tree

app/src/main/java/com/capyreader/app/common/ContextShareLinkExt.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.capyreader.app.common
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.net.Uri
56

67
fun Context.shareLink(url: String, title: String) {
78
val share = Intent.createChooser(Intent().apply {
@@ -12,3 +13,12 @@ fun Context.shareLink(url: String, title: String) {
1213
}, null)
1314
startActivity(share)
1415
}
16+
17+
fun Context.shareImage(uri: Uri) {
18+
val shareIntent = Intent(Intent.ACTION_SEND).apply {
19+
putExtra(Intent.EXTRA_STREAM, uri)
20+
setDataAndType(uri, "image/jpeg")
21+
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
22+
}
23+
startActivity(Intent.createChooser(shareIntent, null))
24+
}

app/src/main/java/com/capyreader/app/common/WebViewInterface.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.serialization.json.Json
99
class WebViewInterface(
1010
private val navigateToMedia: (media: Media) -> Unit,
1111
private val onRequestLinkDialog: (link: ShareLink) -> Unit,
12+
private val onRequestImageDialog: (imageUrl: String) -> Unit = {},
1213
private val onOpenAudioPlayer: (audio: AudioEnclosure) -> Unit = {},
1314
private val onPauseAudio: () -> Unit = {},
1415
) {
@@ -36,6 +37,13 @@ class WebViewInterface(
3637
}
3738
}
3839

40+
@JavascriptInterface
41+
fun showImageDialog(imageUrl: String) {
42+
optionalURL(imageUrl)?.let {
43+
onRequestImageDialog(it.toString())
44+
}
45+
}
46+
3947
@JavascriptInterface
4048
fun openAudioPlayer(audioJson: String) {
4149
try {

app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import com.capyreader.app.ui.components.SearchState
7676
import com.capyreader.app.ui.provideLinkOpener
7777
import com.capyreader.app.ui.rememberLazyListState
7878
import com.capyreader.app.ui.rememberLocalConnectivity
79+
import com.capyreader.app.ui.settings.LocalSnackbarHost
7980
import com.jocmp.capy.Article
8081
import com.jocmp.capy.ArticleFilter
8182
import com.jocmp.capy.ArticleStatus
@@ -160,6 +161,8 @@ fun ArticleScreen(
160161
state = searchState,
161162
)
162163

164+
val snackbarHostState = remember { SnackbarHostState() }
165+
163166
CompositionLocalProvider(
164167
LocalFullContent provides fullContent,
165168
LocalArticleActions provides articleActions,
@@ -170,6 +173,7 @@ fun ArticleScreen(
170173
LocalLinkOpener provides provideLinkOpener(context),
171174
LocalMarkAllReadButtonPosition provides markAllReadButtonPosition,
172175
LocalUnreadCount provides unreadCount,
176+
LocalSnackbarHost provides snackbarHostState,
173177
) {
174178
val openNextFeedOnReadAll = afterReadAll == AfterReadAllBehavior.OPEN_NEXT_FEED
175179

@@ -187,8 +191,6 @@ fun ArticleScreen(
187191
val scaffoldNavigator = rememberArticleScaffoldNavigator()
188192
val showMultipleColumns = scaffoldNavigator.scaffoldDirective.maxHorizontalPartitions > 1
189193
var isPullToRefreshing by remember { mutableStateOf(false) }
190-
191-
val snackbarHostState = remember { SnackbarHostState() }
192194
val addFeedSuccessMessage = stringResource(R.string.add_feed_success)
193195
val currentFeed by viewModel.currentFeed.collectAsStateWithLifecycle(null)
194196
val scrollBehavior = pinnedScrollBehavior()

app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,36 @@ import androidx.compose.runtime.LaunchedEffect
1111
import androidx.compose.runtime.collectAsState
1212
import androidx.compose.runtime.getValue
1313
import androidx.compose.runtime.mutableFloatStateOf
14+
import androidx.compose.runtime.mutableStateOf
1415
import androidx.compose.runtime.remember
16+
import androidx.compose.runtime.rememberCoroutineScope
1517
import androidx.compose.runtime.saveable.rememberSaveable
1618
import androidx.compose.runtime.setValue
1719
import androidx.compose.ui.Modifier
1820
import androidx.compose.ui.layout.onGloballyPositioned
21+
import androidx.compose.ui.platform.LocalContext
22+
import androidx.compose.ui.res.stringResource
23+
import com.capyreader.app.R
1924
import com.capyreader.app.common.AudioEnclosure
2025
import com.capyreader.app.common.Media
2126
import com.capyreader.app.common.rememberTalkbackPreference
27+
import com.capyreader.app.common.shareImage
2228
import com.capyreader.app.preferences.AppPreferences
2329
import com.capyreader.app.preferences.ReaderImageVisibility
2430
import com.capyreader.app.ui.ConnectivityType
2531
import com.capyreader.app.ui.LocalConnectivity
2632
import com.capyreader.app.ui.LocalLinkOpener
2733
import com.capyreader.app.ui.articles.ColumnScrollbar
34+
import com.capyreader.app.ui.articles.media.ImageSaver
2835
import com.capyreader.app.ui.components.WebView
2936
import com.capyreader.app.ui.components.WebViewState
3037
import com.capyreader.app.ui.components.rememberSaveableShareLink
3138
import com.capyreader.app.ui.components.rememberWebViewState
39+
import com.capyreader.app.ui.settings.LocalSnackbarHost
3240
import com.jocmp.capy.Article
41+
import com.jocmp.capy.common.launchIO
42+
import com.jocmp.capy.common.launchUI
43+
import com.jocmp.capy.common.withUIContext
3344
import org.koin.compose.koinInject
3445
import kotlin.math.roundToInt
3546

@@ -43,12 +54,62 @@ fun ArticleReader(
4354
isAudioPlaying: Boolean = false,
4455
) {
4556
val (shareLink, setShareLink) = rememberSaveableShareLink()
57+
val (shareImageUrl, setImageUrl) = rememberSaveable { mutableStateOf<String?>(null) }
4658
val linkOpener = LocalLinkOpener.current
59+
val context = LocalContext.current
60+
val scope = rememberCoroutineScope()
61+
val snackbar = LocalSnackbarHost.current
62+
val successMessage = stringResource(R.string.media_save_success)
63+
val failureMessage = stringResource(R.string.media_save_failure)
64+
val shareFailureMessage = stringResource(R.string.media_share_failure)
65+
66+
fun showSnackbar(message: String) {
67+
scope.launchUI {
68+
snackbar.showSnackbar(message)
69+
}
70+
}
71+
72+
fun saveImage(imageUrl: String) {
73+
scope.launchIO {
74+
val result = ImageSaver.saveImage(imageUrl, context = context)
75+
76+
withUIContext {
77+
result.fold(
78+
onSuccess = {
79+
showSnackbar(successMessage)
80+
},
81+
onFailure = {
82+
showSnackbar(failureMessage)
83+
}
84+
)
85+
86+
}
87+
}
88+
89+
setImageUrl(null)
90+
}
91+
92+
fun shareImage(imageUrl: String) {
93+
scope.launchIO {
94+
ImageSaver.shareImage(imageUrl, context = context)
95+
.fold(
96+
onSuccess = { uri ->
97+
context.shareImage(uri)
98+
},
99+
onFailure = {
100+
showSnackbar(shareFailureMessage)
101+
}
102+
)
103+
}
104+
105+
setImageUrl(null)
106+
}
47107

48108
val webViewState = rememberWebViewState(
49109
key = article.id,
50110
onNavigateToMedia = onSelectMedia,
51111
onRequestLinkDialog = { setShareLink(it) },
112+
onRequestImageDialog = { setImageUrl(it) },
52113
onOpenLink = { linkOpener.open(it) },
53114
onOpenAudioPlayer = onSelectAudio,
54115
onPauseAudio = onPauseAudio,
@@ -88,6 +149,17 @@ fun ArticleReader(
88149
link = shareLink,
89150
)
90151
}
152+
153+
if (shareImageUrl != null) {
154+
ShareImageDialog(
155+
onClose = {
156+
setImageUrl(null)
157+
},
158+
imageUrl = shareImageUrl,
159+
onSave = { saveImage(shareImageUrl) },
160+
onShare = { shareImage(shareImageUrl) },
161+
)
162+
}
91163
}
92164

93165
@Composable

app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
1919
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
2020
import androidx.compose.material3.FlexibleBottomAppBar
2121
import androidx.compose.material3.Scaffold
22+
import androidx.compose.material3.SnackbarHost
23+
import androidx.compose.material3.SnackbarHostState
2224
import androidx.compose.material3.TopAppBarDefaults
2325
import androidx.compose.material3.TopAppBarScrollBehavior
26+
import androidx.compose.runtime.CompositionLocalProvider
2427
import androidx.compose.runtime.Composable
2528
import androidx.compose.runtime.LaunchedEffect
2629
import androidx.compose.runtime.Stable
@@ -51,6 +54,7 @@ import com.capyreader.app.ui.LocalLinkOpener
5154
import com.capyreader.app.ui.articles.LocalFullContent
5255
import com.capyreader.app.ui.collectChangesWithDefault
5356
import com.capyreader.app.ui.components.pullrefresh.SwipeRefresh
57+
import com.capyreader.app.ui.settings.LocalSnackbarHost
5458
import com.jocmp.capy.Article
5559
import org.koin.compose.koinInject
5660

@@ -215,6 +219,8 @@ private fun ArticleViewScaffold(
215219
bottomScrollBehavior: BottomAppBarScrollBehavior,
216220
topToolbarPreference: ToolbarPreferences,
217221
) {
222+
val snackbarHostState = remember { SnackbarHostState() }
223+
218224
Scaffold(
219225
modifier = Modifier
220226
.nestedScroll(bottomScrollBehavior.nestedScrollConnection)
@@ -223,33 +229,40 @@ private fun ArticleViewScaffold(
223229
if (topToolbarPreference.pinned) {
224230
topBar()
225231
}
232+
},
233+
snackbarHost = {
234+
SnackbarHost(hostState = snackbarHostState)
226235
}
227236
) { innerPadding ->
228-
Box(
229-
Modifier
230-
.fillMaxSize()
237+
CompositionLocalProvider(
238+
LocalSnackbarHost provides snackbarHostState,
231239
) {
232240
Box(
233-
modifier = Modifier
234-
.padding(innerPadding)
241+
Modifier
235242
.fillMaxSize()
236243
) {
237-
Column {
238-
reader()
244+
Box(
245+
modifier = Modifier
246+
.padding(innerPadding)
247+
.fillMaxSize()
248+
) {
249+
Column {
250+
reader()
251+
}
239252
}
240-
}
241253

242-
if (!topToolbarPreference.pinned) {
243-
topBar()
244-
}
254+
if (!topToolbarPreference.pinned) {
255+
topBar()
256+
}
245257

246-
if (enableBottomBar) {
247-
Box(
248-
Modifier
249-
.align(Alignment.BottomStart)
250-
.fillMaxWidth()
251-
) {
252-
bottomBar()
258+
if (enableBottomBar) {
259+
Box(
260+
Modifier
261+
.align(Alignment.BottomStart)
262+
.fillMaxWidth()
263+
) {
264+
bottomBar()
265+
}
253266
}
254267
}
255268
}

0 commit comments

Comments
 (0)