Skip to content
Draft

2 #1

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
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ git submodule update --init --recursive
```

Add in the app's `settings.gradle`:
```groovy
include(":app", ":bohio")
project(":bohio").projectDir = file("bohio")
```kotlin
include(":app")
include(":bohio")

// Activate For Release
//project(":bohio").projectDir = file("bohio")

// Activate For Bohio Development (and locate your local source)
project(":bohio").projectDir = file("../git-mod_bohio")
```

Add in the app's `app/build.gradle`:
```groovy
```kotlin
dependencies {
implementation(project(":bohio"))
}
Expand All @@ -33,9 +39,9 @@ Make sure the app's theme extends `Theme.Rama.Base` in `themes.xml`.

For F-Droid, add `submodules: true` to the relevant build entry in the app's metadata so `git submodule update --init --recursive` runs after checkout. Keep the `bohio` repo public and avoid rewriting history on any commit a published app version's submodule pointer references.

## Maintanence
## Maintenance

Update submodule
```bash
git submodule update --remote bohio
```
```
4 changes: 0 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,5 @@ android {
}

dependencies {
// 'api' so consuming apps get these transitively without redeclaring.
// CsActivity extends AppCompatActivity and uses OnBackPressedCallback
// (from androidx.activity, pulled in transitively by appcompat).
api 'androidx.appcompat:appcompat:1.6.1'
api 'androidx.fragment:fragment-ktx:1.6.2'
}
201 changes: 201 additions & 0 deletions src/main/java/com/rama/bohio/activity/BohioActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package com.rama.bohio.activity

import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
import androidx.activity.ComponentActivity
import com.rama.bohio.managers.FontManager
import com.rama.bohio.managers.ThemeManager
import com.rama.bohio.objects.PrefKeys
import com.rama.bohio.objects.PrefTheme
import com.rama.bohio.util.Dimens.dpToPx
import com.rama.bohio.util.LocaleHelper

/**
* Base activity for all Rama apps.
*
* Never calls [com.rama.bohio.managers.PrefsManager.getInstance] internally —
* all pref reads use [rawPrefs] directly so this class is safe to use before
* any app subclass has called [com.rama.bohio.managers.PrefsManager.register].
*/
abstract class BohioActivity : ComponentActivity() {

private var lastKnownLanguage: String? = null
private var lastKnownTheme: String? = null
private var lastKnownUiScale: Float = -1f

// Context wrapping (locale + UI scale)

override fun attachBaseContext(newBase: Context) {
val localeContext = LocaleHelper.wrapContext(newBase)

val scale = rawPrefs(localeContext).getFloat(PrefKeys.APP_UI_SCALE, 1f)

val context = if (scale != 1f) {
val config = Configuration(localeContext.resources.configuration)
config.densityDpi =
(localeContext.resources.displayMetrics.densityDpi * scale).toInt()
localeContext.createConfigurationContext(config)
} else {
localeContext
}

super.attachBaseContext(context)
}

// Lifecycle

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val p = rawPrefs(this)
lastKnownLanguage = p.getString(PrefKeys.APP_LANGUAGE, "")
lastKnownTheme = p.getString(PrefKeys.APP_THEME_NAME, "")
lastKnownUiScale = p.getFloat(PrefKeys.APP_UI_SCALE, 1f)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}

override fun onResume() {
super.onResume()

val p = rawPrefs(this)

val lang = p.getString(PrefKeys.APP_LANGUAGE, "") ?: ""
if (lang != lastKnownLanguage) {
lastKnownLanguage = lang
if (shouldRecreateOnSettingsChange()) {
recreate(); return
}
}

val theme = p.getString(PrefKeys.APP_THEME_NAME, "") ?: ""
if (theme != lastKnownTheme) {
lastKnownTheme = theme
if (shouldRecreateOnSettingsChange()) {
recreate(); return
}
}

val scale = p.getFloat(PrefKeys.APP_UI_SCALE, 1f)
if (scale != lastKnownUiScale) {
lastKnownUiScale = scale
if (shouldRecreateOnSettingsChange()) {
recreate(); return
}
}

val preventRotation = p.getBoolean(PrefKeys.SYSTEM_PREVENT_ROTATION, false)
applyRotationLock(preventRotation)

ThemeManager.applyTheme(this, contentRoot())
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) applySystemBarVisibility()
}

// Public API

protected open fun shouldRecreateOnSettingsChange(): Boolean = true

fun applyCurrentTheme(root: View? = null) {
ThemeManager.applyTheme(this, root ?: contentRoot())
applyNavBarColor()
}

fun refreshFont() {
FontManager.applyFont(this, contentRoot())
}

fun applyRotationLock(lock: Boolean) {
requestedOrientation =
if (lock) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}

protected fun applyEdgeToEdgePadding(root: View) {
val paddingInline = dpToPx(this, 16f)
val paddingBlock = dpToPx(this, 8f)

root.setOnApplyWindowInsetsListener { view, insets ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val sysBars = insets.getInsets(
WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
)
val ime = insets.getInsets(WindowInsets.Type.ime())
val bottomInset = if (insets.isVisible(WindowInsets.Type.ime())) ime.bottom
else sysBars.bottom
view.setPadding(
sysBars.left + paddingInline,
sysBars.top + paddingBlock,
sysBars.right + paddingInline,
bottomInset + paddingBlock,
)
} else {
@Suppress("DEPRECATION")
view.setPadding(
insets.systemWindowInsetLeft + paddingInline,
insets.systemWindowInsetTop + paddingBlock,
insets.systemWindowInsetRight + paddingInline,
insets.systemWindowInsetBottom + paddingBlock,
)
}
insets
}
}

// Private helpers

protected fun applyNavBarColor() {
val theme = rawPrefs(this).getString(PrefKeys.APP_THEME_NAME, "") ?: ""
val palette = ThemeManager.paletteFor(theme, this)
window.navigationBarColor = palette.bg_1
}

private fun applySystemBarVisibility() {
val visible = rawPrefs(this).getBoolean(PrefKeys.SYSTEM_BAR_VISIBLE, true)
if (visible) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
contentRoot().requestApplyInsets()
}

private fun contentRoot(): View = findViewById(android.R.id.content)

/**
* Opens SharedPreferences directly — never goes through PrefsManager.
* Safe at any point in the activity lifecycle, including [attachBaseContext]
* and [onCreate], before the app subclass has called PrefsManager.register().
*/
private fun rawPrefs(context: Context): SharedPreferences =
context.getSharedPreferences("settings", Context.MODE_PRIVATE)
}
92 changes: 92 additions & 0 deletions src/main/java/com/rama/bohio/dialogs/ColorPickerDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.rama.bohio.dialogs

import android.app.Activity
import android.app.Dialog
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import com.rama.bohio.R
import com.rama.bohio.managers.ThemeManager
import com.rama.bohio.widgets.HSVSquareView
import com.rama.bohio.widgets.HueStripView

object ColorPickerDialog {

fun show(
activity: Activity,
initialColor: Int,
onColorSelected: (Int) -> Unit
) {
val dialog = Dialog(activity)
val view = LayoutInflater.from(activity).inflate(R.layout.wd_color_picker_dialog, null)

dialog.setContentView(view)
dialog.window?.setLayout(MATCH_PARENT, WRAP_CONTENT)

ThemeManager.applyTheme(activity, view)

val preview = view.findViewById<View>(R.id.preview)
val hexInput = view.findViewById<EditText>(R.id.hex_input)
val applyButton = view.findViewById<Button>(R.id.apply_button)
val closeButton = view.findViewById<Button>(R.id.close_button)
val hsvSquare = view.findViewById<HSVSquareView>(R.id.hsv_square)
val hueSlider = view.findViewById<HueStripView>(R.id.hue_slider)

// HSV state (single source of truth)
val hsv = floatArrayOf(0f, 0f, 0f)
Color.colorToHSV(initialColor, hsv)
var hue = hsv[0]
var saturation = hsv[1]
var value = hsv[2]

fun updateUI() {
val color = Color.HSVToColor(floatArrayOf(hue, saturation, value))
preview.background.setTint(color)
hexInput.setText(String.format("#%06X", 0xFFFFFF and color))
hsvSquare.setHue(hue)
}
updateUI()

hueSlider.onHueChanged = { newHue ->
hue = newHue
updateUI()
}

hsvSquare.onSaturationValueChanged = { s, v ->
saturation = s
value = v
updateUI()
}

hexInput.setOnEditorActionListener { _, _, _ ->
try {
val color = Color.parseColor(hexInput.text.toString())
Color.colorToHSV(color, hsv)
hue = hsv[0]
saturation = hsv[1]
value = hsv[2]
updateUI()
} catch (_: Exception) {
Toast.makeText(
activity,
activity.getString(R.string.toast_invalid_color),
Toast.LENGTH_SHORT
).show()
}
true
}

applyButton.setOnClickListener {
onColorSelected(Color.HSVToColor(floatArrayOf(hue, saturation, value)))
dialog.dismiss()
}
closeButton.setOnClickListener { dialog.dismiss() }

dialog.show()
}
}
Loading