Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Checks:
-hicpp-move-const-arg,
llvm-*,
-llvm-header-guard
WarningsAsErrors: '*'
WarningsAsErrors: ''
HeaderFilterRegex: '(Browser|Common|JNI)/[^/]+\.h$'
AnalyzeTemporaryDtors: false
FormatStyle: file
Expand Down
9 changes: 8 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ plugins {
}

tasks.register("clean", Delete) {
delete rootProject.layout.buildDirectory
def buildDir = rootProject.layout.buildDirectory.get().asFile
if (buildDir.exists()) {
buildDir.eachFile { file ->
if (file.name != "cerbero" && file.name != "sysroot") {
delete file
}
}
}
delete file(".git/hooks/pre-commit")
}

Expand Down
37 changes: 37 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ vanniktech = "0.30.0"
androidx-navigation-safeargs = "2.8.5"

androidx-activity = "1.10.1"
androidx-activity-compose = "1.10.1"
androidx-annotation = "1.9.1"
androidx-test-runner = "1.6.2"
androidx-test-rules = "1.6.1"
Expand All @@ -28,16 +29,31 @@ androidx-fragment-ktx = "1.8.5"
androidx-navigation-fragment-ktx = "2.8.5"
androidx-navigation-ui-ktx = "2.8.5"
androidx-preference-ktx = "1.2.1"
androidx-lifecycle-runtime-compose = "2.9.0"
androidx-lifecycle-viewmodel-compose = "2.9.0"

# Compose
compose-bom = "2025.01.01"
compose-compiler = "1.5.15"

# Material3 Adaptive Navigation3
material3-adaptive = "1.1.0"
material3-adaptive-navigation3 = "1.0.0"
navigation3 = "0.0.1"
kotlinx-serialization = "1.7.3"

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech" }
androidx-navigation-safeargs = { id = "androidx.navigation.safeargs", version.ref = "androidx-navigation-safeargs" }

[libraries]
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidx-activity" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidx-annotation" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidx-test-rules" }
Expand All @@ -52,3 +68,24 @@ androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", ve
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "androidx-navigation-fragment-ktx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "androidx-navigation-ui-ktx" }
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "androidx-preference-ktx" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-viewmodel-compose" }

# Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }

# Material3 Adaptive Navigation3
material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "material3-adaptive" }
material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "material3-adaptive" }
material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "material3-adaptive" }
material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3-adaptive-navigation3" }
navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" }
navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
31 changes: 31 additions & 0 deletions tools/minibrowser/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
plugins {
alias libs.plugins.android.application
alias libs.plugins.kotlin.android
alias libs.plugins.kotlin.compose
alias libs.plugins.kotlin.serialization
alias libs.plugins.androidx.navigation.safeargs
}

Expand All @@ -25,6 +27,7 @@ android {

buildFeatures {
viewBinding = true
compose = true
}

java {
Expand All @@ -50,6 +53,34 @@ dependencies {
implementation libs.androidx.navigation.ui.ktx
implementation libs.androidx.preference.ktx

// Compose
implementation platform(libs.compose.bom)
implementation libs.compose.ui
implementation libs.compose.ui.graphics
implementation libs.compose.ui.tooling.preview
implementation libs.compose.foundation
implementation libs.compose.material3
implementation libs.compose.material.icons.extended
debugImplementation libs.compose.ui.tooling

// Activity Compose
implementation libs.androidx.activity.compose

// Lifecycle Compose
implementation libs.androidx.lifecycle.runtime.compose
implementation libs.androidx.lifecycle.viewmodel.compose

// Material3 Adaptive Navigation3
implementation libs.material3.adaptive
implementation libs.material3.adaptive.layout
implementation libs.material3.adaptive.navigation
implementation libs.material3.adaptive.navigation3
implementation libs.navigation3.runtime
implementation libs.navigation3.ui

// Serialization for NavKey
implementation libs.kotlinx.serialization.json

modules {
module("org.jetbrains.kotlin:kotlin-stdlib-jdk7") {
replacedBy("org.jetbrains.kotlin:kotlin-stdlib", "kotlin-stdlib-jdk7 is now part of kotlin-stdlib")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard()
onCommit(binding.toolbarEditText.text.toString())
binding.toolbarEditText.clearFocus()
selectedTab().webview.requestFocus()
true
} else {
false
Expand Down Expand Up @@ -163,6 +165,18 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) {
}
fullscreenView = null
}

override fun onUriChanged(view: WPEView, uri: String) {
super.onUriChanged(view, uri)
binding.toolbarEditText.setText(uri)
}
}
}

selectedTab.webview.wpeViewClient = object : WPEViewClient() {
override fun onPageStarted(view: WPEView, url: String) {
super.onPageStarted(view, url)
binding.toolbarEditText.setText(url)
}
}
}
Expand Down Expand Up @@ -204,7 +218,7 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) {

private fun onCommit(text: String) {
val url: String = if ((text.contains(".") || text.contains(":")) && !text.contains(" ")) {
Utils.normalizeAddress(text)
normalizeAddress(text)
} else {
SEARCH_URI_BASE + text
}
Expand All @@ -217,7 +231,7 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) {
manager.hideSoftInputFromWindow(requireView().windowToken, 0)
}

private fun selectedTab() : Tab {
internal fun selectedTab() : Tab {
// For now assume we always have at least one tab
val selectedTabId = browserViewModel.browserState.value.selectedTabId
return browserViewModel.findTab(selectedTabId!!)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,139 @@

package org.wpewebkit.tools.minibrowser

import android.content.Context

import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

const val INITIAL_URL = "https://igalia.com"
const val SEARCH_URI_BASE = "https://duckduckgo.com/?q="

@Stable
data class BrowserState(
val tabs: List<Tab> = emptyList(),
val selectedTabId: String? = null,
)
) {
val selectedTab: Tab?
get() = tabs.find { it.id == selectedTabId }

val tabCount: Int
get() = tabs.size
}

class BrowserViewModel : ViewModel() {
private val _browserState = MutableStateFlow(BrowserState())
val browserState = _browserState.asStateFlow()

fun addTab(tab: Tab) {
fun addTab(tab: Tab, select: Boolean = true) {
_browserState.update {
it.copy(
tabs = it.tabs + tab,
selectedTabId = tab.id // Todo maybe make selection optional
selectedTabId = if (select) tab.id else it.selectedTabId
)
}
}

fun createTab(context: Context, url: String = INITIAL_URL): Tab {
val tab = Tab.newTab(context, url)
addTab(tab)
return tab
}

fun selectTab(tabId: String) {
val previousId = _browserState.value.selectedTabId
if (previousId == tabId) return

// update visibility: hide old tab, show new tab
if (previousId != null) {
findTab(previousId)?.setVisibility(false)
}
findTab(tabId)?.setVisibility(true)

_browserState.update {
it.copy(selectedTabId = tabId)
}
}

fun removeTab(tabId: String) {
_browserState.update { state ->
val newTabs = state.tabs.filter { it.id != tabId }
val newSelectedId = when {
state.selectedTabId != tabId -> state.selectedTabId
newTabs.isEmpty() -> null
else -> {
val removedIndex = state.tabs.indexOfFirst { it.id == tabId }
val newIndex = minOf(removedIndex, newTabs.lastIndex)
newTabs.getOrNull(newIndex)?.id
}
}
state.copy(
tabs = newTabs,
selectedTabId = newSelectedId
)
}
}

fun findTab(id: String) : Tab {
return browserState.value.tabs.first{ tab -> tab.id == id }
fun closeAllTabs() {
_browserState.update {
it.copy(tabs = emptyList(), selectedTabId = null)
}
}

fun findTab(id: String): Tab? {
return browserState.value.tabs.find { tab -> tab.id == id }
}

fun getTab(id: String): Tab {
return browserState.value.tabs.first { tab -> tab.id == id }
}

fun selectedTab(): Tab? {
return browserState.value.selectedTab
}

fun isSelected(tabId: String): Boolean {
return browserState.value.selectedTabId == tabId
}

fun moveTab(fromIndex: Int, toIndex: Int) {
_browserState.update { state ->
val tabs = state.tabs.toMutableList()
if (fromIndex in tabs.indices && toIndex in tabs.indices) {
val item = tabs.removeAt(fromIndex)
tabs.add(toIndex, item)
}
state.copy(tabs = tabs)
}
}

// lifecycle: called when activity pauses
fun onPause() {
browserState.value.tabs.forEach { tab ->
tab.webview.setMuted(true)
}
}

// lifecycle: called when activity resumes
fun onResume() {
// only unmute the selected tab
val selectedId = browserState.value.selectedTabId
browserState.value.tabs.forEach { tab ->
if (tab.id == selectedId) {
tab.setVisibility(true)
}
}
}

// cleanup webviews when viewmodel is cleared
override fun onCleared() {
super.onCleared()
browserState.value.tabs.forEach { tab ->
tab.webview.destroy()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,30 @@ package org.wpewebkit.tools.minibrowser
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log

import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import org.wpewebkit.tools.minibrowser.databinding.ActivityMainBinding
import androidx.activity.viewModels

import org.wpewebkit.tools.minibrowser.ui.BrowserScreen
import org.wpewebkit.tools.minibrowser.ui.theme.MiniBrowserTheme

class MainActivity : AppCompatActivity(R.layout.activity_main) {
class MainActivity : ComponentActivity() {

private val TAG = "MiniBrowser"

private val navHost by lazy {
supportFragmentManager.primaryNavigationFragment as NavHostFragment
}

private lateinit var binding: ActivityMainBinding
private val viewModel: BrowserViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate")
enableEdgeToEdge();
binding = ActivityMainBinding.inflate(layoutInflater)
enableEdgeToEdge()

setContent {
MiniBrowserTheme {
BrowserScreen(viewModel = viewModel)
}
}
}

override fun onConfigurationChanged(newConfig: Configuration) {
Expand Down
Loading
Loading