diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 28428b1..8ad7175 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.util.prefixIfNot + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -17,6 +19,13 @@ android { namespace = "com.paulcoding.hviewer" compileSdk = 35 + val repoUrl = providers.exec { + commandLine = "git remote get-url origin".split(' ') + }.standardOutput.asText.get().trim().removePrefix("https://github.com/") + .removePrefix("git@github.com:") + .removeSuffix(".git") + .prefixIfNot("https://github.com/") + defaultConfig { applicationId = "com.paulcoding.hviewer" minSdk = 26 @@ -24,6 +33,8 @@ android { versionCode = 1 versionName = "1.3.2" + buildConfigField("String", "REPO_URL", "\"$repoUrl\"") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" splits { @@ -101,6 +112,7 @@ dependencies { implementation(libs.ktor.serialization.gson) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.logging) + implementation(libs.androidx.webkit) implementation(libs.androidx.room.runtime) ksp(libs.androidx.room.compiler) diff --git a/app/src/main/java/com/paulcoding/hviewer/extensions/Activity.kt b/app/src/main/java/com/paulcoding/hviewer/extensions/Activity.kt new file mode 100644 index 0000000..6d0a116 --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/extensions/Activity.kt @@ -0,0 +1,10 @@ +package com.paulcoding.hviewer.extensions + +import android.app.Activity +import android.content.Intent +import android.net.Uri + +fun Activity.openInBrowser(url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) +} \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt index 7dfb75c..382a0cf 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt @@ -34,6 +34,7 @@ import com.paulcoding.hviewer.ui.page.posts.PostsPage import com.paulcoding.hviewer.ui.page.search.SearchPage import com.paulcoding.hviewer.ui.page.settings.SettingsPage import com.paulcoding.hviewer.ui.page.sites.SitesPage +import com.paulcoding.hviewer.ui.page.web.WebPage @Composable fun AppEntry() { @@ -116,6 +117,10 @@ fun AppEntry() { animatedComposable(Route.POST) { PostPage( appViewModel, + navToWebView = { + appViewModel.setWebViewUrl(it) + navController.navigate(Route.WEBVIEW) + }, goBack = { navController.popBackStack() }) @@ -189,6 +194,10 @@ fun AppEntry() { deleteHistory = appViewModel::deleteHistory ) } + animatedComposable(Route.WEBVIEW) { + val url = appViewModel.getWebViewUrl() + WebPage(goBack = { navController.popBackStack() }, url = url) + } } } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt index a7e248e..e770a08 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt @@ -48,11 +48,18 @@ class AppViewModel : ViewModel() { data class UiState( val post: PostItem = PostItem(), + val url: String = "", val site: Pair = "" to SiteConfig(), val tag: Tag = Tag(), val isDevMode: Boolean = BuildConfig.DEBUG, ) + fun setWebViewUrl(url: String) { + _stateFlow.update { it.copy(url = url) } + } + + fun getWebViewUrl() = _stateFlow.value.url + fun setDevMode(isDevMode: Boolean) { _stateFlow.update { it.copy(isDevMode = isDevMode) } } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt index b6ec0df..101a5e3 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt @@ -14,4 +14,5 @@ object Route { const val LIST_SCRIPT = "list_script" const val LOCK = "lock" const val HISTORY = "history" + const val WEBVIEW = "webview" } \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt index c9c9e40..e89089d 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt @@ -1,8 +1,9 @@ package com.paulcoding.hviewer.ui.page.post +import android.annotation.SuppressLint import android.widget.Toast import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -14,6 +15,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,13 +28,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel +import com.paulcoding.hviewer.MainActivity import com.paulcoding.hviewer.MainApp.Companion.appContext import com.paulcoding.hviewer.extensions.isScrolledToEnd import com.paulcoding.hviewer.extensions.isScrollingUp +import com.paulcoding.hviewer.extensions.openInBrowser import com.paulcoding.hviewer.helper.makeToast import com.paulcoding.hviewer.ui.component.HBackIcon import com.paulcoding.hviewer.ui.component.HGoTop @@ -46,8 +53,9 @@ import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.rememberZoomableState import me.saket.telephoto.zoomable.zoomable +@SuppressLint("UseOfNonLambdaOffsetOverload") @Composable -fun PostPage(appViewModel: AppViewModel, goBack: () -> Unit) { +fun PostPage(appViewModel: AppViewModel, navToWebView: (String) -> Unit, goBack: () -> Unit) { val appState by appViewModel.stateFlow.collectAsState() val post = appState.post val siteConfig = appState.site.second @@ -84,17 +92,14 @@ fun PostPage(appViewModel: AppViewModel, goBack: () -> Unit) { verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(uiState.images, key = { it }) { image -> - HImage( - modifier = Modifier.clickable { selectedImage = image }, - url = image - ) + PostImage(url = image) { + selectedImage = image + } } if (uiState.isLoading) item { Box( - modifier = Modifier - .statusBarsPadding() - .statusBarsPadding() + modifier = Modifier.statusBarsPadding() ) { HLoading() } @@ -164,4 +169,41 @@ fun ImageModal(url: String, dismiss: () -> Unit) { } } } +} + +@Composable +fun PostImage(url: String, onTap: () -> Unit = {}) { + val showMenu = remember { mutableStateOf(false) } + val menuOffset = remember { mutableStateOf(Pair(0f, 0f)) } + val context = LocalContext.current as MainActivity + + Box(modifier = Modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { offset -> + println("pressed $url") + showMenu.value = true + menuOffset.value = Pair(offset.x, offset.y) + }, + onTap = { onTap() } + ) + }) { + HImage( + url = url + ) + + DropdownMenu( + expanded = showMenu.value, + onDismissRequest = { showMenu.value = false }, + ) { + DropdownMenuItem( + onClick = { + showMenu.value = false + context.openInBrowser(url) + }, + text = { + Text("Open in browser") + } + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt index b583b71..96fbeb3 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt @@ -28,9 +28,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.paulcoding.hviewer.MainActivity +import com.paulcoding.hviewer.extensions.openInBrowser import com.paulcoding.hviewer.model.PostItem import com.paulcoding.hviewer.model.Tag import com.paulcoding.hviewer.ui.component.HFavoriteIcon @@ -69,6 +72,7 @@ fun PostCard( ) { var isBottomSheetVisible by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState() + val context = LocalContext.current as MainActivity LaunchedEffect(isBottomSheetVisible) { if (isBottomSheetVisible) { @@ -123,12 +127,19 @@ fun PostCard( SelectionContainer { Text(text = name, fontSize = 20.sp) } - Text( - text = url, - textDecoration = TextDecoration.Underline, - fontSize = 12.sp, - color = Color.Blue - ) + TextButton( + onClick = { + isBottomSheetVisible = false + context.openInBrowser(url) + }, + ) { + Text( + text = url, + textDecoration = TextDecoration.Underline, + fontSize = 12.sp, + color = Color.Blue + ) + } if (size != null) { Text(text = "Size: $size") } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/web/WebPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/web/WebPage.kt new file mode 100644 index 0000000..fbf543a --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/web/WebPage.kt @@ -0,0 +1,29 @@ +package com.paulcoding.hviewer.ui.page.web + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.paulcoding.hviewer.ui.component.HBackIcon + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WebPage(goBack: () -> Unit, url: String) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + HBackIcon { goBack() } + }, + ) + } + ) { paddings -> + HWebView(modifier = Modifier.padding(paddings), url = url) + } +} + diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/web/WebView.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/web/WebView.kt new file mode 100644 index 0000000..0325cb0 --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/web/WebView.kt @@ -0,0 +1,40 @@ +package com.paulcoding.hviewer.ui.page.web + +import android.annotation.SuppressLint +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun HWebView( + modifier: Modifier = Modifier, + url: String, +) { + class WebAppInterface() { + @JavascriptInterface + fun senData(data: String) { + println(data) + } + } + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + webViewClient = object : WebViewClient() { + override fun onPageFinished(webview: WebView, url: String?) { + super.onPageFinished(webview, url) + webview.loadUrl("javascript:window.HViewer.senData('Hello from WebView')") + } + } + val jsInterface = WebAppInterface() + addJavascriptInterface(jsInterface, "HViewer") + loadUrl(url) + } + } + ) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca806dc..f356cbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ mmkv = "2.0.0" navigationCompose = "2.8.5" rhino = "1.7.15" roomRuntime = "2.6.1" +webkit = "1.13.0-alpha02" zoomable = "0.14.0" material3 = "1.3.1" @@ -43,6 +44,7 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } bom = { module = "io.github.Rosemoe.sora-editor:bom", version.ref = "bom" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilNetworkOkhttp" } coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coilNetworkOkhttp" }