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
60 changes: 60 additions & 0 deletions .github/workflows/i18n.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Generate Translations

on:
push:
branches:
- main

jobs:
generate-translations:
name: Update and Generate Translations
runs-on: ubuntu-latest
permissions: write-all

steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch full history for creating PRs

# Set up JDK
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: wrapper
cache-read-only: false

# Cache Gradle dependencies
- name: Cache Gradle dependencies
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Update Strings XML
run: ./gradlew updateStringsXml

- name: Generate Translations
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: ./gradlew generateTranslations

- name: Create Pull Request
if: steps.changes-check.outputs.changes != 'false'
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: "Update Translations ${{ github.event.pull_request.number }}"
body: "Update Translations ${{ github.event.pull_request.number }}"
branch: i18n-${{ github.event.pull_request.number }}
base: main
labels: |
translations
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
local.properties
buildSrc/build
12 changes: 12 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import org.w3c.dom.Element
import java.io.FileNotFoundException
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
Expand Down Expand Up @@ -117,4 +124,4 @@ tasks.register<JacocoReport>("jacocoTestReport") {
)
)
)
}
}
3 changes: 3 additions & 0 deletions app/src/main/java/co/stonephone/stonecamera/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import co.stonephone.stonecamera.utils.TranslationManager

class MainActivity : ComponentActivity() {

Expand Down Expand Up @@ -43,6 +44,8 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

TranslationManager.loadTranslationsForLocale(applicationContext)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this guarantee to return a set of elements (translations) regardless of the application's context? In other words are there ever going to be cases where it wouldn't return a) something the program can run with, b) something the human user can understand?


if (!isChromeOS()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumb question and not directly part of this PR... Are we targeting ChromeOS?

requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
Expand Down
12 changes: 2 additions & 10 deletions app/src/main/java/co/stonephone/stonecamera/StoneCameraApp.kt
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the removal of android.util.log what's happening about logging?

Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
// StoneCameraApp.kt
@file:kotlin.OptIn(ExperimentalMaterial3Api::class)

package co.stonephone.stonecamera

import android.annotation.SuppressLint
import android.graphics.Rect
import android.util.Log
import androidx.annotation.OptIn
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlipCameraAndroid
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
Expand Down Expand Up @@ -185,8 +177,8 @@ fun StoneCameraApp(
) {
modePlugins.forEach { modePlugin ->
val modeLabel = modePlugin.modeLabel!!
Text(text = modeLabel.uppercase(),
color = if (modeLabel == selectedMode) Color(0xFFFFCC00) else Color.White,
Text(text = modeLabel.resolve().uppercase(),
color = if (modeLabel.resolve() == selectedMode.resolve()) Color(0xFFFFCC00) else Color.White,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as i've got you can leave it as it was modeLabel == selectedMode

style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.clickable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import co.stonephone.stonecamera.plugins.IPlugin
import co.stonephone.stonecamera.plugins.PluginSetting
import co.stonephone.stonecamera.plugins.PluginUseCase
import co.stonephone.stonecamera.utils.StoneCameraInfo
import co.stonephone.stonecamera.utils.Translatable
import co.stonephone.stonecamera.utils.TranslatableString
import co.stonephone.stonecamera.utils.createCameraSelectorForId
import co.stonephone.stonecamera.utils.i18n
import co.stonephone.stonecamera.utils.selectCameraForStepZoomLevel
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -74,7 +77,8 @@ class StoneCameraViewModel(
var isPaused by mutableStateOf(false)
private set

var selectedMode by mutableStateOf("photo")
@Translatable
var selectedMode by mutableStateOf("photo".i18n())
private set

private val _plugins = mutableListOf<IPlugin>()
Expand Down Expand Up @@ -189,16 +193,25 @@ class StoneCameraViewModel(

@Suppress("UNCHECKED_CAST")
return when (defaultValue) {
is String -> prefs.getString(settingKey, defaultValue) as? T
is TranslatableString -> prefs.getString(settingKey, defaultValue.resolve()) as? T
is Float -> prefs.getFloat(settingKey, defaultValue) as? T
else -> null
}
}

// Retrieve a setting with automatic recomposition support
fun <T> getSetting(settingKey: String): T? {
val _value = settings[settingKey]

val value = when (_value) {
is TranslatableString -> _value
is String -> _value.i18n()
is Float -> prefs.getFloat(settingKey, _value)
else -> null
}

@Suppress("UNCHECKED_CAST")
return settings[settingKey] as? T
return value as? T
}

// Update a setting and notify observers
Expand All @@ -207,7 +220,7 @@ class StoneCameraViewModel(

when (setting) {
is PluginSetting.EnumSetting -> {
prefs.edit().putString(settingKey, value as String).apply()
prefs.edit().putString(settingKey, (value as TranslatableString).raw).apply()
}

is PluginSetting.ScalarSetting -> {
Expand Down Expand Up @@ -262,7 +275,7 @@ class StoneCameraViewModel(
/**
* Switch between Photo and Video modes.
*/
fun selectMode(mode: String) {
fun selectMode(mode: TranslatableString) {
plugins.forEach {
it.onModeSelected(this, selectedMode, mode)
}
Expand Down Expand Up @@ -462,7 +475,6 @@ class StoneCameraViewModel(
try {
// Await all work to finish
workList.awaitAll()
Log.d("Analyzer", "All plugins completed successfully")
} catch (e: Exception) {
Log.e("Analyzer", "Error in one or more plugins", e)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again a question not directly part of this PR, but triggered when reading the code deleted above, what happens with the worklist.awaitAll() if an exception occurs in one or more of the plugins that are running in parallel?

} finally {
Expand Down
31 changes: 17 additions & 14 deletions app/src/main/java/co/stonephone/stonecamera/plugins/AspectRatio.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.stonephone.stonecamera.MyApplication
import co.stonephone.stonecamera.StoneCameraViewModel
import co.stonephone.stonecamera.utils.Translatable
import co.stonephone.stonecamera.utils.TranslatableString
import co.stonephone.stonecamera.utils.getLargestMatchingSize
import co.stonephone.stonecamera.utils.getLargestSensorSize
import co.stonephone.stonecamera.utils.i18n

class AspectRatioPlugin : IPlugin {
override val id: String = "aspectRatioPlugin"
override val name: String = "Aspect Ratio"

override fun initialize(viewModel: StoneCameraViewModel) {
val previewView = viewModel.previewView
val aspectRatio = viewModel.getSetting<String>("aspectRatio")
if (aspectRatio === "FULL") {
val aspectRatio = viewModel.getSetting<TranslatableString>("aspectRatio")
if (aspectRatio?.resolve() === "FULL") {
previewView!!.scaleType = PreviewView.ScaleType.FILL_CENTER
} else {
previewView!!.scaleType = PreviewView.ScaleType.FIT_CENTER
Expand All @@ -39,10 +42,10 @@ class AspectRatioPlugin : IPlugin {
imageCapture: ImageCapture.Builder
): ImageCapture.Builder {
// TODO "FULL" aspect ratio isn't cropping on image capture
val ratioStr = viewModel.getSetting<String>("aspectRatio") ?: "16:9"
val ratioStr = viewModel.getSetting<TranslatableString>("aspectRatio") ?: "16:9".i18n()
val context = MyApplication.getAppContext()

val ratioOrNull = parseRatioOrNull(ratioStr)
val ratioOrNull = parseRatioOrNull(ratioStr.resolve())

val targetSize = if (ratioOrNull == null) {
getLargestSensorSize(viewModel.camera!!, context)
Expand All @@ -60,10 +63,10 @@ class AspectRatioPlugin : IPlugin {
viewModel: StoneCameraViewModel,
preview: Preview.Builder
): Preview.Builder {
val ratioStr = viewModel.getSetting<String>("aspectRatio") ?: "16:9"
val ratioStr = viewModel.getSetting<TranslatableString>("aspectRatio") ?: "16:9".i18n()

val context = MyApplication.getAppContext()
val ratioOrNull = parseRatioOrNull(ratioStr)
val ratioOrNull = parseRatioOrNull(ratioStr.resolve())

// 1) Figure out the best "preferred size" for this ratio
val targetSize = if (ratioOrNull == null) {
Expand Down Expand Up @@ -151,8 +154,8 @@ class AspectRatioPlugin : IPlugin {
viewModel: StoneCameraViewModel,
previewView: PreviewView
): PreviewView {
val aspectRatio = viewModel.getSetting<String>("aspectRatio")
if (aspectRatio == "FULL") {
val aspectRatio = viewModel.getSetting<TranslatableString>("aspectRatio")
if (aspectRatio?.resolve() == "FULL") {
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
} else {
previewView.scaleType = PreviewView.ScaleType.FIT_CENTER
Expand All @@ -171,29 +174,29 @@ class AspectRatioPlugin : IPlugin {
listOf(
PluginSetting.EnumSetting(
key = "aspectRatio",
defaultValue = "16:9",
options = listOf("16:9", "4:3", "FULL"),
defaultValue = @Translatable "16:9".i18n(),
options = listOf(@Translatable "16:9".i18n(), @Translatable "4:3".i18n(), @Translatable "FULL".i18n()),
render = { value, isSelected ->
Text(
text = value,
text = value.resolve(),
color = if (isSelected) Color(0xFFFFCC00) else Color.White,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(8.dp)
)
},
onChange = { viewModel, value ->
onChange = { viewModel, _ ->
val previewView = viewModel.previewView
if (viewModel.getSetting<String>("aspectRatio") === "FULL") {
if (viewModel.getSetting<TranslatableString>("aspectRatio")?.resolve() === "FULL") {
previewView!!.scaleType = PreviewView.ScaleType.FILL_CENTER
} else {
previewView!!.scaleType = PreviewView.ScaleType.FIT_CENTER
}
viewModel.recreateUseCases()
},
renderLocation = SettingLocation.TOP,
label = "Aspect Ratio"
label = @Translatable "Aspect Ratio".i18n()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this call respond if a match isn't found for the string being translated?

)
)
}
Expand Down
29 changes: 18 additions & 11 deletions app/src/main/java/co/stonephone/stonecamera/plugins/Flash.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import co.stonephone.stonecamera.StoneCameraViewModel
import co.stonephone.stonecamera.utils.Translatable
import co.stonephone.stonecamera.utils.TranslatableString
import co.stonephone.stonecamera.utils.i18n

class FlashPlugin : IPlugin {
override val id: String = "flashPlugin"
Expand All @@ -26,8 +29,8 @@ class FlashPlugin : IPlugin {
override fun onImageCapture(
viewModel: StoneCameraViewModel, imageCapture: ImageCapture.Builder
): ImageCapture.Builder {
val flashMode = viewModel.getSetting<String>("flash") ?: "OFF"
val mode = flashModeStringToMode(flashMode)
val flashMode = viewModel.getSetting<TranslatableString>("flash") ?: "OFF".i18n()
val mode = flashModeStringToMode(flashMode.resolve())
imageCapture.setFlashMode(mode)
return imageCapture
}
Expand All @@ -48,26 +51,30 @@ class FlashPlugin : IPlugin {
listOf(
PluginSetting.EnumSetting(
key = "flash",
label = "Flash",
defaultValue = "OFF",
options = listOf("OFF", "ON", "AUTO"),
label = @Translatable "Flash".i18n(),
defaultValue = @Translatable "OFF".i18n(),
options = listOf(
@Translatable "OFF".i18n(),
@Translatable "ON".i18n(),
@Translatable "AUTO".i18n()
),
render = { flashMode, isSelected ->
Box(
modifier = Modifier.padding(8.dp)
) {
Icon(
imageVector = when (flashMode) {
"OFF" -> Icons.Default.FlashOff // Replace with your preferred icon
"ON" -> Icons.Default.FlashOn
"AUTO" -> Icons.Default.FlashAuto // You may need to add custom icons for Flash Auto
"OFF".i18n() -> Icons.Default.FlashOff // Replace with your preferred icon
"ON".i18n() -> Icons.Default.FlashOn
"AUTO".i18n() -> Icons.Default.FlashAuto // You may need to add custom icons for Flash Auto
else -> {
Icons.Default.FlashOff
}
},
contentDescription = when (flashMode) {
"OFF" -> "Flash Off"
"ON" -> "Flash On"
"AUTO" -> "Flash Auto"
"OFF".i18n() -> @Translatable "Flash Off".i18n().resolve()
"ON".i18n() -> @Translatable "Flash On".i18n().resolve()
"AUTO".i18n() -> @Translatable "Flash Auto".i18n().resolve()
else -> {
"Flash"
}
Expand Down
Loading
Loading