diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72503b8d..342a8a0e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.kotlin.dsl.support.kotlinCompilerOptions + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -211,6 +213,8 @@ dependencies { debugImplementation(libs.androidx.compose.ui.test.manifest) detektPlugins(libs.detekt.compose) + implementation(libs.androidx.datastore.preferences) + add("androidTestScreenshotImplementation", libs.junit) add("androidTestScreenshotImplementation", libs.fastlane.screengrab) add("androidTestScreenshotImplementation", libs.androidx.test.rules) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 72d496e5..1eb5ffec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - @@ -8,10 +9,18 @@ - - - - + + + + @@ -19,32 +28,39 @@ - - + + - + android:theme="@style/splashTheme"> + + @@ -62,14 +78,18 @@ + + + + @@ -86,8 +106,8 @@ - + + + - + - + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/App.kt b/app/src/main/java/de/rwth_aachen/phyphox/App.kt index b16162f1..187fe3d2 100644 --- a/app/src/main/java/de/rwth_aachen/phyphox/App.kt +++ b/app/src/main/java/de/rwth_aachen/phyphox/App.kt @@ -2,12 +2,37 @@ package de.rwth_aachen.phyphox import androidx.multidex.MultiDexApplication import dagger.hilt.android.HiltAndroidApp +import de.rwth_aachen.phyphox.common.AppDelegate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Inject //This extension to application is only used to store measured data in memory as this may easily exceed the amount of data allowed on the transaction stack @HiltAndroidApp class App : MultiDexApplication() { + private val applicationScope = CoroutineScope(SupervisorJob()) + + @Inject + lateinit var appDelegates: Set<@JvmSuppressWildcards AppDelegate> + + + //Need to get rid off of this ASAP @JvmField var experiment: PhyphoxExperiment? = null + + override fun onCreate() { + super.onCreate() + + appDelegates.forEach { it.start(applicationScope) } + } + + override fun onTerminate() { + appDelegates.forEach { it.stop() } + super.onTerminate() + } + } + + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ExperimentList/ExperimentListActivity.java b/app/src/main/java/de/rwth_aachen/phyphox/ExperimentList/ExperimentListActivity.java index 76073da3..d7c91246 100644 --- a/app/src/main/java/de/rwth_aachen/phyphox/ExperimentList/ExperimentListActivity.java +++ b/app/src/main/java/de/rwth_aachen/phyphox/ExperimentList/ExperimentListActivity.java @@ -48,7 +48,6 @@ import android.widget.TextView; import android.widget.Toast; -import androidx.activity.EdgeToEdge; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; @@ -57,9 +56,6 @@ import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; import androidx.preference.PreferenceManager; import com.google.android.material.floatingactionbutton.FloatingActionButton; @@ -87,6 +83,7 @@ import de.rwth_aachen.phyphox.Bluetooth.BluetoothExperimentLoader; import de.rwth_aachen.phyphox.Bluetooth.BluetoothScanDialog; +import de.rwth_aachen.phyphox.BuildConfig; import de.rwth_aachen.phyphox.Experiment; import de.rwth_aachen.phyphox.ExperimentList.datasource.AssetExperimentLoader; import de.rwth_aachen.phyphox.ExperimentList.handler.BluetoothScanner; @@ -107,6 +104,7 @@ import de.rwth_aachen.phyphox.SettingsActivity.SettingsFragment; import de.rwth_aachen.phyphox.camera.depth.DepthInput; import de.rwth_aachen.phyphox.camera.helper.CameraHelper; +import de.rwth_aachen.phyphox.features.settings.presentation.NewSettingsActivity; public class ExperimentListActivity extends AppCompatActivity { @@ -282,6 +280,13 @@ public void onBluetoothScanError(String msg, Boolean isError, Boolean isFatal) { } }); + if(BuildConfig.DEBUG){ + creditsV.setOnLongClickListener(v -> { + startActivity(new Intent(this, NewSettingsActivity.class)); + return true; + }); + } + } private void showPopupMenu(View v) { diff --git a/app/src/main/java/de/rwth_aachen/phyphox/Helper/Helper.java b/app/src/main/java/de/rwth_aachen/phyphox/Helper/Helper.java index 004f49c7..adff69e8 100644 --- a/app/src/main/java/de/rwth_aachen/phyphox/Helper/Helper.java +++ b/app/src/main/java/de/rwth_aachen/phyphox/Helper/Helper.java @@ -1,7 +1,6 @@ package de.rwth_aachen.phyphox.Helper; import static android.content.Context.BATTERY_SERVICE; -import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import android.Manifest; import android.app.Activity; @@ -9,7 +8,6 @@ import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; @@ -28,10 +26,8 @@ import android.os.Build; import android.os.Handler; import android.util.Base64; -import android.util.DisplayMetrics; import android.util.Log; import android.view.PixelCopy; -import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.Window; @@ -39,9 +35,6 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; import androidx.preference.PreferenceManager; import org.w3c.dom.Document; @@ -55,7 +48,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Locale; -import java.util.Map; import java.util.Vector; import java.util.zip.CRC32; diff --git a/app/src/main/java/de/rwth_aachen/phyphox/SettingsActivity/SettingsActivity.java b/app/src/main/java/de/rwth_aachen/phyphox/SettingsActivity/SettingsActivity.java index d9cd7fa6..13d18ec0 100644 --- a/app/src/main/java/de/rwth_aachen/phyphox/SettingsActivity/SettingsActivity.java +++ b/app/src/main/java/de/rwth_aachen/phyphox/SettingsActivity/SettingsActivity.java @@ -5,19 +5,14 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; import android.os.Bundle; -import android.view.ViewGroup; -import dagger.hilt.android.AndroidEntryPoint; -import de.rwth_aachen.phyphox.Helper.Helper; import de.rwth_aachen.phyphox.Helper.WindowInsetHelper; import de.rwth_aachen.phyphox.R; -@AndroidEntryPoint + public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/de/rwth_aachen/phyphox/common/AppDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/common/AppDelegate.kt new file mode 100644 index 00000000..7a1583a2 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/common/AppDelegate.kt @@ -0,0 +1,8 @@ +package de.rwth_aachen.phyphox.common + +import kotlinx.coroutines.CoroutineScope + +interface AppDelegate { + fun start(coroutineScope: CoroutineScope) + fun stop() +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/di/AppModule.kt b/app/src/main/java/de/rwth_aachen/phyphox/di/AppModule.kt index bda88bd7..c782855b 100644 --- a/app/src/main/java/de/rwth_aachen/phyphox/di/AppModule.kt +++ b/app/src/main/java/de/rwth_aachen/phyphox/di/AppModule.kt @@ -3,8 +3,9 @@ package de.rwth_aachen.phyphox.di import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import de.rwth_aachen.phyphox.features.settings.di.SettingsModule -@Module(includes = []) +@Module(includes = [SettingsModule::class]) @InstallIn(SingletonComponent::class) abstract class AppModule {} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/di/ApplicationScope.kt b/app/src/main/java/de/rwth_aachen/phyphox/di/ApplicationScope.kt new file mode 100644 index 00000000..8289f3fd --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/di/ApplicationScope.kt @@ -0,0 +1,7 @@ +package de.rwth_aachen.phyphox.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ApplicationScope diff --git a/app/src/main/java/de/rwth_aachen/phyphox/di/CoroutineScopesModule.kt b/app/src/main/java/de/rwth_aachen/phyphox/di/CoroutineScopesModule.kt new file mode 100644 index 00000000..72c83990 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/di/CoroutineScopesModule.kt @@ -0,0 +1,21 @@ +package de.rwth_aachen.phyphox.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CoroutineScopesModule { + + @Provides + @Singleton + @ApplicationScope + fun provideApplicationScope(): CoroutineScope = + CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/DefaultAppPreferencesRepository.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/DefaultAppPreferencesRepository.kt new file mode 100644 index 00000000..0f8d277f --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/DefaultAppPreferencesRepository.kt @@ -0,0 +1,62 @@ +package de.rwth_aachen.phyphox.features.settings.data + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.data.local.LocalAppPreferencesDataSource +import de.rwth_aachen.phyphox.features.settings.domain.data.local.converter.toAppLanguage +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + + +class DefaultAppPreferencesRepository @Inject constructor( + private val localAppPreferencesDataSource: LocalAppPreferencesDataSource, +) : AppPreferencesRepository { + override fun observeCurrentAccessPort(): Flow { + return localAppPreferencesDataSource.observeCurrentAccessPort() + } + + override suspend fun updateAccessPort(port: Int) { + return localAppPreferencesDataSource.updateAccessPort(port) + } + + override fun observeGraphSize(): Flow { + return localAppPreferencesDataSource.observeGraphSize() + } + + override suspend fun updateGraphSize(size: Float) { + localAppPreferencesDataSource.updateGraphSize(size) + } + + override fun observeCurrentLanguage(): Flow { + return localAppPreferencesDataSource.observeLanguage().map { + toAppLanguage(it) + } + } + + override suspend fun getSupportedLanguages(): List { + return localAppPreferencesDataSource.getSupportedLanguages().map { AppLanguage(it) } + } + + override suspend fun updateLanguage(language: AppLanguage) { + localAppPreferencesDataSource.updateLanguage(language.identifier) + } + + override fun observeProximityLockEnabled(): Flow { + return localAppPreferencesDataSource.observeProximityLockEnabled() + } + + override suspend fun updateProximityLockStatus(enabled: Boolean) { + localAppPreferencesDataSource.updateProximityLockEnabled(enabled) + } + + override fun observeAppUiMode(): Flow { + return localAppPreferencesDataSource.observeAppUiMode() + } + + override suspend fun updateAppUiMode(mode: AppUiMode) { + localAppPreferencesDataSource.updateAppUiMode(mode) + } +} + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/local/DefaultLocalAppPreferencesDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/local/DefaultLocalAppPreferencesDataSource.kt new file mode 100644 index 00000000..7b6decc5 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/local/DefaultLocalAppPreferencesDataSource.kt @@ -0,0 +1,86 @@ +package de.rwth_aachen.phyphox.features.settings.data.local + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import de.rwth_aachen.phyphox.features.settings.di.SettingsDataStore +import de.rwth_aachen.phyphox.features.settings.domain.data.local.LocalAppPreferencesDataSource +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultLocalAppPreferencesDataSource @Inject constructor( + @param:SettingsDataStore private val dataStore: DataStore, + private val systemDataSource: SystemDataSource, +) : LocalAppPreferencesDataSource { + + override fun observeCurrentAccessPort(): Flow = dataStore.data.map { preferences -> + preferences[PreferencesKeys.ACCESS_PORT] + } + + override suspend fun updateAccessPort(port: Int) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.ACCESS_PORT] = port + } + } + + override fun observeGraphSize(): Flow = dataStore.data.map { preferences -> + preferences[PreferencesKeys.GRAPH_SIZE] + } + + override suspend fun updateGraphSize(size: Float) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.GRAPH_SIZE] = size + } + } + + override fun observeLanguage(): Flow = dataStore.data.map { preferences -> + preferences[PreferencesKeys.LANGUAGE] + } + + override suspend fun updateLanguage(language: String) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.LANGUAGE] = language + } + } + + override suspend fun getSupportedLanguages(): List { + return systemDataSource.getSupportedLanguages() + } + + override fun observeProximityLockEnabled(): Flow = dataStore.data.map { preferences -> + preferences[PreferencesKeys.PROXIMITY_LOCK_ENABLED] + } + + override suspend fun updateProximityLockEnabled(enabled: Boolean) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.PROXIMITY_LOCK_ENABLED] = enabled + } + } + + override fun observeAppUiMode(): Flow = dataStore.data.map { preferences -> + preferences[PreferencesKeys.APP_UI_MODE]?.let { AppUiMode.fromString(it) } + } + + override suspend fun updateAppUiMode(mode: AppUiMode) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.APP_UI_MODE] = mode.identifier + } + } + + private object PreferencesKeys { + val ACCESS_PORT = intPreferencesKey("access_port") + val GRAPH_SIZE = floatPreferencesKey("graph_size") + val LANGUAGE = stringPreferencesKey("language") + val PROXIMITY_LOCK_ENABLED = booleanPreferencesKey("proximity_lock_enabled") + val APP_UI_MODE = stringPreferencesKey("app_ui_mode") + } + +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/local/SystemDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/local/SystemDataSource.kt new file mode 100644 index 00000000..9afede08 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/data/local/SystemDataSource.kt @@ -0,0 +1,12 @@ +package de.rwth_aachen.phyphox.features.settings.data.local + +import de.rwth_aachen.phyphox.BuildConfig +import javax.inject.Inject + +class SystemDataSource @Inject constructor() { + suspend fun getSupportedLanguages(): List { + return BuildConfig.LOCALE_ARRAY + .filterNot { it.contains("+") } + .map { it.replace("-r", "-") } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/di/SettingsModule.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/di/SettingsModule.kt new file mode 100644 index 00000000..0f01f57b --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/di/SettingsModule.kt @@ -0,0 +1,92 @@ +package de.rwth_aachen.phyphox.features.settings.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import de.rwth_aachen.phyphox.common.AppDelegate +import de.rwth_aachen.phyphox.features.settings.data.DefaultAppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.data.local.DefaultLocalAppPreferencesDataSource +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.data.local.LocalAppPreferencesDataSource +import de.rwth_aachen.phyphox.features.settings.presentation.AppLanguageDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport.AccessPortViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport.DefaultAccessPortViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.AppLanguageViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.DefaultAppLanguageViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.appuimode.UiModeViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.appuimode.DefaultUiModeViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.graphsize.DefaultGraphSizeViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.graphsize.GraphSizeViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.proximitylock.DefaultProximityLockViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.proximitylock.ProximityLockViewmodelDelegate +import javax.inject.Qualifier +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SettingsModule { + + @Binds + internal abstract fun bindsAccessPortDelegate( + implementation: DefaultAccessPortViewmodelDelegate, + ): AccessPortViewmodelDelegate + + @Binds + internal abstract fun bindsAppLanguageDelegate( + implementation: DefaultAppLanguageViewmodelDelegate, + ): AppLanguageViewmodelDelegate + + @Binds + internal abstract fun bindsAppUiModeDelegate( + implementation: DefaultUiModeViewmodelDelegate, + ): UiModeViewmodelDelegate + + @Binds + internal abstract fun bindsGraphSizeDelegate( + implementation: DefaultGraphSizeViewmodelDelegate, + ): GraphSizeViewmodelDelegate + + @Binds + internal abstract fun bindsProximityLockDelegate( + implementation: DefaultProximityLockViewmodelDelegate, + ): ProximityLockViewmodelDelegate + + @Binds + internal abstract fun bindLocalDataSource( + implementation: DefaultLocalAppPreferencesDataSource, + ): LocalAppPreferencesDataSource + + @Binds + internal abstract fun bndAppPreferencesRepository( + implementation: DefaultAppPreferencesRepository, + ): AppPreferencesRepository + + @Binds + @Singleton + @IntoSet + abstract fun provideAppLanguageDelegate(implementation: AppLanguageDelegate): AppDelegate + + companion object { + @Provides + @Singleton + @SettingsDataStore + fun providePreferencesDataStore(@ApplicationContext context: Context): DataStore { + return PreferenceDataStoreFactory.create( + produceFile = { context.preferencesDataStoreFile("preferences") }, + ) + } + } +} + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class SettingsDataStore diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/AppPreferencesRepository.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/AppPreferencesRepository.kt new file mode 100644 index 00000000..0d11de3d --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/AppPreferencesRepository.kt @@ -0,0 +1,23 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import kotlinx.coroutines.flow.Flow + +interface AppPreferencesRepository { + fun observeCurrentAccessPort(): Flow + suspend fun updateAccessPort(port: Int) + + fun observeGraphSize(): Flow + suspend fun updateGraphSize(size: Float) + + fun observeCurrentLanguage(): Flow + suspend fun updateLanguage(language: AppLanguage) + suspend fun getSupportedLanguages():List + + fun observeProximityLockEnabled(): Flow + suspend fun updateProximityLockStatus(enabled: Boolean) + + fun observeAppUiMode(): Flow + suspend fun updateAppUiMode(mode: AppUiMode) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalAccessPortPreferencesDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalAccessPortPreferencesDataSource.kt new file mode 100644 index 00000000..78c7d391 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalAccessPortPreferencesDataSource.kt @@ -0,0 +1,8 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data.local + +import kotlinx.coroutines.flow.Flow + +interface LocalAccessPortPreferencesDataSource { + fun observeCurrentAccessPort(): Flow + suspend fun updateAccessPort(port: Int) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalAppPreferencesDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalAppPreferencesDataSource.kt new file mode 100644 index 00000000..14e67b1a --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalAppPreferencesDataSource.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data.local + +interface LocalAppPreferencesDataSource : + LocalAccessPortPreferencesDataSource, + LocalGraphSizePreferencesDataSource, + LocalLanguagePreferencesDataSource, + LocalProximityLockPreferencesDataSource, + LocalUiConfigPreferencesDataSource + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalGraphSizePreferencesDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalGraphSizePreferencesDataSource.kt new file mode 100644 index 00000000..856afabf --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalGraphSizePreferencesDataSource.kt @@ -0,0 +1,8 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data.local + +import kotlinx.coroutines.flow.Flow + +interface LocalGraphSizePreferencesDataSource { + fun observeGraphSize(): Flow + suspend fun updateGraphSize(size: Float) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalLanguagePreferencesDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalLanguagePreferencesDataSource.kt new file mode 100644 index 00000000..3ac08f3a --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalLanguagePreferencesDataSource.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data.local + +import kotlinx.coroutines.flow.Flow + +interface LocalLanguagePreferencesDataSource { + fun observeLanguage(): Flow + suspend fun updateLanguage(language: String) + suspend fun getSupportedLanguages(): List +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalProximityLockPreferencesDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalProximityLockPreferencesDataSource.kt new file mode 100644 index 00000000..4d3a77d5 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalProximityLockPreferencesDataSource.kt @@ -0,0 +1,8 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data.local + +import kotlinx.coroutines.flow.Flow + +interface LocalProximityLockPreferencesDataSource { + fun observeProximityLockEnabled(): Flow + suspend fun updateProximityLockEnabled(enabled: Boolean) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalUiConfigPreferencesDataSource.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalUiConfigPreferencesDataSource.kt new file mode 100644 index 00000000..8e93b43b --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/LocalUiConfigPreferencesDataSource.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data.local + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import kotlinx.coroutines.flow.Flow + +interface LocalUiConfigPreferencesDataSource { + fun observeAppUiMode(): Flow + suspend fun updateAppUiMode(mode: AppUiMode) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/converter/AppLanguageConverter.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/converter/AppLanguageConverter.kt new file mode 100644 index 00000000..472c193c --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/data/local/converter/AppLanguageConverter.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.features.settings.domain.data.local.converter + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage + + +fun toAppLanguage(identifier: String?): AppLanguage? { + return identifier?.let { + AppLanguage(it) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/AppLanguage.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/AppLanguage.kt new file mode 100644 index 00000000..465bfb5d --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/AppLanguage.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.features.settings.domain.model + +data class AppLanguage( + val identifier: String, +){ + companion object{ + const val SYSTEM_DEFAULT_IDENTIFIER = "system_default" + val SYSTEM_DEFAULT = AppLanguage(SYSTEM_DEFAULT_IDENTIFIER) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/AppUiMode.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/AppUiMode.kt new file mode 100644 index 00000000..9a2ef654 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/AppUiMode.kt @@ -0,0 +1,16 @@ +package de.rwth_aachen.phyphox.features.settings.domain.model + +enum class AppUiMode(val identifier: String) { + DARK("dark"), + LIGHT("light"), + SYSTEM("system"); + + + + companion object { + @Suppress("unused") + fun fromString(identifier: String): AppUiMode? { + return entries.find { it.identifier == identifier } + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/GraphConfig.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/GraphConfig.kt new file mode 100644 index 00000000..b766bf2d --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/GraphConfig.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.model + +data class GraphConfig( + val labelSizeMultiplier: Float, + val textSizeMultiplier: Float, + val lineWidthMultiplier: Float, + val borderWidthMultiplier: Float, +) + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/GraphItemsRange.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/GraphItemsRange.kt new file mode 100644 index 00000000..b39f3dfd --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/GraphItemsRange.kt @@ -0,0 +1,8 @@ +package de.rwth_aachen.phyphox.features.settings.domain.model + +data class GraphItemsRange( + val labelSizeRange: ClosedFloatingPointRange = 0.5f..2.5f, + val textSizeRange: ClosedFloatingPointRange = 0.5f..2.5f, + val lineWidthRange: ClosedFloatingPointRange = 0.5f..2.5f, + val borderWidthRange: ClosedFloatingPointRange = 0.5f..2.5f, +) diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/errors/AccessPortOutOfRange.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/errors/AccessPortOutOfRange.kt new file mode 100644 index 00000000..095b4bba --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/model/errors/AccessPortOutOfRange.kt @@ -0,0 +1,3 @@ +package de.rwth_aachen.phyphox.features.settings.domain.model.errors + +class AccessPortOutOfRange(val intRange: IntRange) : Throwable(message = null) diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/GetAccessPortRangeApplicationService.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/GetAccessPortRangeApplicationService.kt new file mode 100644 index 00000000..e03d1708 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/GetAccessPortRangeApplicationService.kt @@ -0,0 +1,14 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.accessport + +import javax.inject.Inject + +class GetAccessPortRangeApplicationService @Inject constructor() { + operator fun invoke(): IntRange { + return MIN_PORT..MAX_PORT + } + + companion object { + const val MIN_PORT = 1024 + const val MAX_PORT = 65536 + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/ObserveCurrentAccessPortUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/ObserveCurrentAccessPortUseCase.kt new file mode 100644 index 00000000..ae9d82bc --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/ObserveCurrentAccessPortUseCase.kt @@ -0,0 +1,20 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.accessport + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class ObserveCurrentAccessPortUseCase @Inject constructor( + private val repository: AppPreferencesRepository, +) { + operator fun invoke(): Flow { + return repository.observeCurrentAccessPort().map { port -> + port ?: DEFAULT_ACCESS_PORT + } + } + + companion object { + const val DEFAULT_ACCESS_PORT = 8080 + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/UpdateAccessPortUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/UpdateAccessPortUseCase.kt new file mode 100644 index 00000000..298efba5 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/accessport/UpdateAccessPortUseCase.kt @@ -0,0 +1,36 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.accessport + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.model.errors.AccessPortOutOfRange +import javax.inject.Inject + +class UpdateAccessPortUseCase @Inject constructor( + private val repository: AppPreferencesRepository, + private val getAccessPortRange: GetAccessPortRangeApplicationService, +) { + suspend operator fun invoke(port: Int): Result { + return try { + validatePort(port) + repository.updateAccessPort(port) + Result.success(Unit) + } catch (error: Throwable) { + Result.failure(error) + } + } + + suspend operator fun invoke(port: String): Result { + return try { + val number = port.toInt() + invoke(number) + } catch (error: NumberFormatException) { + Result.failure(error) + } + } + + private fun validatePort(port: Int) { + val validRange = getAccessPortRange() + if (!validRange.contains(port)) { + throw AccessPortOutOfRange(validRange) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetBorderWidthMultiplierRangeDomainService.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetBorderWidthMultiplierRangeDomainService.kt new file mode 100644 index 00000000..33d731fa --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetBorderWidthMultiplierRangeDomainService.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import javax.inject.Inject + +class GetBorderWidthMultiplierRangeDomainService @Inject constructor() { + suspend operator fun invoke(): ClosedFloatingPointRange { + return 0.5f..2.5f + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetGraphSizeRangeUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetGraphSizeRangeUseCase.kt new file mode 100644 index 00000000..66df7a2a --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetGraphSizeRangeUseCase.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import javax.inject.Inject + +@Deprecated("Migrating to multiplier based graph config instead.") +class GetGraphSizeRangeUseCase @Inject constructor() { + suspend operator fun invoke(): ClosedFloatingPointRange { + return 0f..3f + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetGraphSizeRangesUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetGraphSizeRangesUseCase.kt new file mode 100644 index 00000000..1e1cfb3c --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetGraphSizeRangesUseCase.kt @@ -0,0 +1,27 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import de.rwth_aachen.phyphox.features.settings.domain.model.GraphItemsRange +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +class GetGraphSizeRangesUseCase @Inject constructor( + private val getLabelSizeMultiplierRange: GetLabelSizeMultiplierRangeDomainService, + private val getTextSizeMultiplierRange: GetTextSizeMultiplierRangeDomainService, + private val getLineWidthMultiplierRange: GetLineWidthMultiplierRangeDomainService, + private val getBorderWidthMultiplierRange: GetBorderWidthMultiplierRangeDomainService, +) { + suspend operator fun invoke(): GraphItemsRange = coroutineScope { + val labelSizeRangeDeferred = async { getLabelSizeMultiplierRange() } + val textSizeRangeDeferred = async { getTextSizeMultiplierRange() } + val lineWidthRangeDeferred = async { getLineWidthMultiplierRange() } + val borderWidthRangeDeferred = async { getBorderWidthMultiplierRange() } + + GraphItemsRange( + labelSizeRange = labelSizeRangeDeferred.await(), + textSizeRange = textSizeRangeDeferred.await(), + lineWidthRange = lineWidthRangeDeferred.await(), + borderWidthRange = borderWidthRangeDeferred.await(), + ) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetLabelSizeMultiplierRangeDomainService.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetLabelSizeMultiplierRangeDomainService.kt new file mode 100644 index 00000000..ab04ff17 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetLabelSizeMultiplierRangeDomainService.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import javax.inject.Inject + +class GetLabelSizeMultiplierRangeDomainService @Inject constructor() { + suspend operator fun invoke(): ClosedFloatingPointRange { + return 0.5f..2.5f + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetLineWidthMultiplierRangeDomainService.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetLineWidthMultiplierRangeDomainService.kt new file mode 100644 index 00000000..9ef3e57b --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetLineWidthMultiplierRangeDomainService.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import javax.inject.Inject + +class GetLineWidthMultiplierRangeDomainService @Inject constructor() { + suspend operator fun invoke(): ClosedFloatingPointRange { + return 0.5f..2.5f + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetTextSizeMultiplierRangeDomainService.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetTextSizeMultiplierRangeDomainService.kt new file mode 100644 index 00000000..10c81382 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/GetTextSizeMultiplierRangeDomainService.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import javax.inject.Inject + +class GetTextSizeMultiplierRangeDomainService @Inject constructor() { + suspend operator fun invoke(): ClosedFloatingPointRange { + return 0.5f..2.5f + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/ObserveCurrentGraphConfigUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/ObserveCurrentGraphConfigUseCase.kt new file mode 100644 index 00000000..d7f0c7c7 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/ObserveCurrentGraphConfigUseCase.kt @@ -0,0 +1,22 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.model.GraphConfig +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class ObserveCurrentGraphConfigUseCase @Inject constructor( + private val repository: AppPreferencesRepository +) { + operator fun invoke(): Flow { + return flowOf( + GraphConfig( + labelSizeMultiplier = 1.0f, + textSizeMultiplier = 1.0f, + lineWidthMultiplier = 1.0f, + borderWidthMultiplier = 1.0f, + ), + ) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/ObserveCurrentGraphSizeUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/ObserveCurrentGraphSizeUseCase.kt new file mode 100644 index 00000000..d0a2229b --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/ObserveCurrentGraphSizeUseCase.kt @@ -0,0 +1,15 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +@Deprecated("Migrating to multiplier based graph config instead.") +class ObserveCurrentGraphSizeUseCase @Inject constructor( + private val repository: AppPreferencesRepository +) { + operator fun invoke(): Flow { + return flowOf(0f) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/UpdateGraphSizeUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/UpdateGraphSizeUseCase.kt new file mode 100644 index 00000000..d2684c91 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/graphsize/UpdateGraphSizeUseCase.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import javax.inject.Inject + +class UpdateGraphSizeUseCase @Inject constructor( + private val repository: AppPreferencesRepository +){ +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/GetSupportedLanguagesUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/GetSupportedLanguagesUseCase.kt new file mode 100644 index 00000000..f4d42392 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/GetSupportedLanguagesUseCase.kt @@ -0,0 +1,14 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.language + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import javax.inject.Inject + +class GetSupportedLanguagesUseCase @Inject constructor( + private val repository: AppPreferencesRepository, +) { + + suspend operator fun invoke(): List { + return listOf(AppLanguage.SYSTEM_DEFAULT) + repository.getSupportedLanguages() + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/ObserveCurrentAppLanguageUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/ObserveCurrentAppLanguageUseCase.kt new file mode 100644 index 00000000..0bc833ca --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/ObserveCurrentAppLanguageUseCase.kt @@ -0,0 +1,15 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.language + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class ObserveCurrentAppLanguageUseCase @Inject constructor( + private val repository: AppPreferencesRepository, +) { + operator fun invoke(): Flow { + return repository.observeCurrentLanguage().map { it ?: AppLanguage.SYSTEM_DEFAULT } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/UpdateAppLanguageUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/UpdateAppLanguageUseCase.kt new file mode 100644 index 00000000..1966a693 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/language/UpdateAppLanguageUseCase.kt @@ -0,0 +1,22 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.language + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import javax.inject.Inject + +class UpdateAppLanguageUseCase @Inject constructor( + private val repository: AppPreferencesRepository, +) { + suspend operator fun invoke(identifier: String): Result { + return invoke(AppLanguage(identifier)) + } + + suspend operator fun invoke(language: AppLanguage): Result { + return try { + repository.updateLanguage(language) + Result.success(Unit) + } catch (e: Throwable) { + Result.failure(e) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/proximitylock/ObserveIsCurrentProximityLockEnabledUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/proximitylock/ObserveIsCurrentProximityLockEnabledUseCase.kt new file mode 100644 index 00000000..a6df5707 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/proximitylock/ObserveIsCurrentProximityLockEnabledUseCase.kt @@ -0,0 +1,14 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.proximitylock + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class ObserveIsCurrentProximityLockEnabledUseCase @Inject constructor( + private val repository: AppPreferencesRepository +){ + operator fun invoke(): Flow { + return flowOf(true) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/proximitylock/UpdateProximityLockStatusUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/proximitylock/UpdateProximityLockStatusUseCase.kt new file mode 100644 index 00000000..17850539 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/proximitylock/UpdateProximityLockStatusUseCase.kt @@ -0,0 +1,17 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.proximitylock + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import javax.inject.Inject + +class UpdateProximityLockStatusUseCase @Inject constructor( + private val repository: AppPreferencesRepository, +) { + suspend operator fun invoke(enabled: Boolean): Result { + return try { + repository.updateProximityLockStatus(enabled) + Result.success(Unit) + } catch (error: Throwable) { + Result.failure(error) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/GetSupportedAppUiModeUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/GetSupportedAppUiModeUseCase.kt new file mode 100644 index 00000000..48232750 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/GetSupportedAppUiModeUseCase.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.uimode + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import javax.inject.Inject + +class GetSupportedAppUiModeUseCase @Inject constructor() { + operator fun invoke(): List { + return AppUiMode.entries + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/ObserveCurrentAppUiModeUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/ObserveCurrentAppUiModeUseCase.kt new file mode 100644 index 00000000..aee2b1cc --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/ObserveCurrentAppUiModeUseCase.kt @@ -0,0 +1,26 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.uimode + +import de.rwth_aachen.phyphox.di.ApplicationScope +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + + +class ObserveCurrentAppUiModeUseCase @Inject constructor( + private val repository: AppPreferencesRepository, + @ApplicationScope private val appScope: CoroutineScope, +) { + operator fun invoke(): StateFlow { + return repository.observeAppUiMode().map { it ?: AppUiMode.SYSTEM }.distinctUntilChanged().stateIn( + scope = appScope, + started = SharingStarted.Eagerly, + initialValue = AppUiMode.SYSTEM, + ) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/UpdateCurrentAppUiModeUseCase.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/UpdateCurrentAppUiModeUseCase.kt new file mode 100644 index 00000000..92863161 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/domain/usecase/uimode/UpdateCurrentAppUiModeUseCase.kt @@ -0,0 +1,18 @@ +package de.rwth_aachen.phyphox.features.settings.domain.usecase.uimode + +import de.rwth_aachen.phyphox.features.settings.domain.data.AppPreferencesRepository +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import javax.inject.Inject + +class UpdateCurrentAppUiModeUseCase @Inject constructor( + private val repository: AppPreferencesRepository, +) { + suspend operator fun invoke(appUiMode: AppUiMode): Result { + return try { + repository.updateAppUiMode(appUiMode) + Result.success(Unit) + } catch (e: Throwable) { + Result.failure(e) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/AppLanguageDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/AppLanguageDelegate.kt new file mode 100644 index 00000000..fb740b7a --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/AppLanguageDelegate.kt @@ -0,0 +1,30 @@ +package de.rwth_aachen.phyphox.features.settings.presentation + +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import de.rwth_aachen.phyphox.common.AppDelegate +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import de.rwth_aachen.phyphox.features.settings.domain.usecase.language.ObserveCurrentAppLanguageUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class AppLanguageDelegate @Inject constructor( + private val observeCurrentAppLanguageUseCase: ObserveCurrentAppLanguageUseCase, +) : AppDelegate { + override fun start(coroutineScope: CoroutineScope) { + observeCurrentAppLanguageUseCase() + .onEach { appLanguage -> + val localeList = + if (appLanguage.identifier == AppLanguage.SYSTEM_DEFAULT_IDENTIFIER) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(appLanguage.identifier) + } + AppCompatDelegate.setApplicationLocales(localeList) + }.launchIn(coroutineScope) + } + + override fun stop() = Unit +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/NewSettingsActivity.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/NewSettingsActivity.kt new file mode 100644 index 00000000..d93278dd --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/NewSettingsActivity.kt @@ -0,0 +1,60 @@ +package de.rwth_aachen.phyphox.features.settings.presentation + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dagger.hilt.android.AndroidEntryPoint +import de.rwth_aachen.phyphox.features.settings.presentation.compose.SettingsRoot +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsEvent +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsViewmodelViewModel +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme + +@AndroidEntryPoint +class NewSettingsActivity : ComponentActivity() { + private val viewModel: SettingsViewmodelViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.enableEdgeToEdge() + setContent { + PhyphoxTheme { + Surface { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + ObserveSettingsEvents() + SettingsRoot( + uiState = uiState, + onActionEvent = viewModel::onActionEvent, + ) + } + + } + } + } + + + @Composable + private fun ObserveSettingsEvents() { + val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + LaunchedEffect(Unit) { + viewModel.uiEvent.collect { event -> + when (event) { + SettingsEvent.NavigateBack -> onBackPressedDispatcher?.onBackPressed() + is SettingsEvent.OpenWebpage -> {} + is SettingsEvent.OpenWebpageFromResourceID -> {} + } + } + } + + } + + +} + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/AccessPortBottomSheet.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/AccessPortBottomSheet.kt new file mode 100644 index 00000000..090370ba --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/AccessPortBottomSheet.kt @@ -0,0 +1,134 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport.AccessPortSheetUiModel +import de.rwth_aachen.phyphox.ui.string.resolve +import de.rwth_aachen.phyphox.ui.string.toStringUIModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccessPortBottomSheet( + uiModel: AccessPortSheetUiModel, + onDismiss: () -> Unit, + onConfirm: (String) -> Unit, + sheetState: SheetState = rememberModalBottomSheetState(), +) { + val context = LocalContext.current + + var port by remember { mutableStateOf(uiModel.currentPort.resolve(context)) } + var isError by remember { mutableStateOf(false) } + LaunchedEffect(uiModel.error) { + isError = uiModel.error != null + } + + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp), + ) { + Text( + text = stringResource(R.string.settingsPort), + style = androidx.compose.material3.MaterialTheme.typography.titleLarge, + ) + Spacer(modifier = Modifier.height(24.dp)) + OutlinedTextField( + value = port, + onValueChange = { + port = it + isError = !uiModel.range.contains(it.toIntOrNull()) + }, + modifier = Modifier.fillMaxWidth(), + label = { + Text( + text = stringResource(R.string.settingsPort), + ) + }, + isError = isError, + supportingText = { + if (uiModel.error != null && isError) { + Text(text = uiModel.error.resolve()) + }else{ + Text(text = uiModel.range.toString()) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true, + ) + Spacer(modifier = Modifier.height(16.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + onConfirm(port) + }, + enabled = !isError, + ) { + Text(stringResource(id = android.R.string.ok)) + } + + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun AccessPortBottomSheetPreview() { + // State for showing the bottom sheet in the preview + val sheetState = rememberModalBottomSheetState() + + // Example with valid input + val uiModel = AccessPortSheetUiModel( + currentPort = "8080".toStringUIModel(), + range = 1024..65535, + error = "Some error".toStringUIModel(), + ) + + // Example with an error state (uncomment to see) + /* + val uiModelWithError = AccessPortSheetUiModel( + currentPort = StringUIModel.StaticString("100"), // Invalid initial value + range = 1024..65535, + error = StringUIModel.StringResource(R.string.settings_we_access_port_invalid, 1024, 65535) + ) + */ + + AccessPortBottomSheet( + uiModel = uiModel, + onDismiss = {}, + onConfirm = {}, + sheetState = sheetState, + ) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/AppLanguageBottomSheet.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/AppLanguageBottomSheet.kt new file mode 100644 index 00000000..dd208adc --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/AppLanguageBottomSheet.kt @@ -0,0 +1,118 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.AppLanguageSheetUiModel +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.LanguageUiModel +import de.rwth_aachen.phyphox.ui.string.resolve + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun AppLanguageBottomSheet( + uiModel: AppLanguageSheetUiModel, + onDismiss: () -> Unit, + onConfirm: (identifier: String) -> Unit, + sheetState: SheetState = rememberModalBottomSheetState(), +) { + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth(), + userScrollEnabled = true, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + stickyHeader { + Text( + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceContainerLow, + ) + .padding(16.dp), + text = stringResource(R.string.settingsLanguage), + style = androidx.compose.material3.MaterialTheme.typography.titleLarge, + ) + } + items(uiModel.availableLocales) { item -> + LanguageListItem( + language = item, + isSelected = item.identifier == uiModel.currentSelection.identifier, + onClick = { + onConfirm(item.identifier) + }, + ) + } + } + } +} + + +@Composable +private fun LanguageListItem( + modifier: Modifier = Modifier, + language: LanguageUiModel, + isSelected: Boolean, + onClick: () -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(R.string.selected), + tint = MaterialTheme.colorScheme.primary, + ) + } else { + Spacer(modifier = Modifier.width(24.dp)) + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Text(language.displayName.resolve()) + language.localDisplayName?.let { + Text( + text = it.resolve(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/SettingsRoot.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/SettingsRoot.kt new file mode 100644 index 00000000..3d755a44 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/SettingsRoot.kt @@ -0,0 +1,114 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.presentation.compose.settingscontent.SettingsContent +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsAction +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsUiState +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport.AccessPortSheetUiModel +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.AppLanguageSheetUiModel +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsRoot( + modifier: Modifier = Modifier, + uiState: SettingsUiState, + onActionEvent: (SettingsAction) -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(uiState.modal) { + if (uiState.modal == null) { + sheetState.hide() + } + } + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(id = R.string.action_settings)) }, + navigationIcon = { + IconButton( + onClick = { + onActionEvent(SettingsAction.OnBackPressed) + }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back), + ) + } + }, + ) + }, + modifier = modifier, + ) { innerPadding -> + SettingsContent( + modifier = modifier.padding(innerPadding), + currentLanguage = uiState.currentLanguage, + seekbarConfig = uiState.graphSize, + appUiMode = uiState.appUiMode, + accessPort = uiState.accessPort, + proximityLockEnabled = uiState.proximityLockEnabled, + onActionEvent = onActionEvent, + ) + } + when (val modal = uiState.modal) { + is AccessPortSheetUiModel -> AccessPortBottomSheet( + uiModel = modal, + sheetState = sheetState, + onDismiss = { + coroutineScope.launch { + sheetState.hide() + onActionEvent.invoke(SettingsAction.OnModalDismissed) + } + }, + onConfirm = { newPort -> + onActionEvent.invoke(SettingsAction.OnAccessPortChanged(newPort)) + }, + ) + + is AppLanguageSheetUiModel -> AppLanguageBottomSheet( + uiModel = modal, + onDismiss = { + coroutineScope.launch { + sheetState.hide() + onActionEvent.invoke(SettingsAction.OnModalDismissed) + } + }, + onConfirm = { newLanguage -> + onActionEvent.invoke(SettingsAction.OnAppLanguageChanged(newLanguage)) + }, + sheetState = sheetState, + ) + } +} + + +@Composable +@PreviewLightDark +internal fun SettingsRootPreview() { + PhyphoxTheme { + SettingsRoot( + uiState = SettingsUiState(), + onActionEvent = {}, + ) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/ClickablePreferenceItem.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/ClickablePreferenceItem.kt new file mode 100644 index 00000000..ee30abfb --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/ClickablePreferenceItem.kt @@ -0,0 +1,71 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.clickablepreferenceitem + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferenceitem.PreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencesummaryitem.PreferenceSummaryItem +import de.rwth_aachen.phyphox.ui.skeleton +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme +import de.rwth_aachen.phyphox.utils.UiResourceState + +@Composable +fun ClickablePreferenceItem( + modifier: Modifier = Modifier, + title: StringUIModel, + summary: UiResourceState, + iconRes: Int? = null, + onClick: () -> Unit = {}, +) { + + PreferenceItem( + modifier = modifier.clickable( + enabled = summary is UiResourceState.Success, + onClick = onClick, + ), + title = title, + iconRes = iconRes, + content = { + when (summary) { + UiResourceState.Loading -> Box( + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth() + .height(16.dp) + .skeleton(), + ) + + is UiResourceState.Success -> PreferenceSummaryItem( + primaryText = summary.data, + ) + } + }, + ) +} + + + +@Preview(showBackground = true) +@Composable +internal fun ClickablePreferenceItemPreview( + @PreviewParameter(ClickablePreferenceItemPreviewProvider::class) preview: UiResourceState, +) { + PhyphoxTheme { + Surface { + ClickablePreferenceItem( + title = LoremIpsumStringUIModel(2), + summary = preview, + ) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/ClickablePreferenceItemPreviewProvider.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/ClickablePreferenceItemPreviewProvider.kt new file mode 100644 index 00000000..d892d13b --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/ClickablePreferenceItemPreviewProvider.kt @@ -0,0 +1,14 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.clickablepreferenceitem + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.utils.UiResourceState + +class ClickablePreferenceItemPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + UiResourceState.Loading, + UiResourceState.Success(LoremIpsumStringUIModel(8)), + ) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/LanguagePreferenceItem.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/LanguagePreferenceItem.kt new file mode 100644 index 00000000..be1ad8e0 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/clickablepreferenceitem/LanguagePreferenceItem.kt @@ -0,0 +1,51 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.clickablepreferenceitem + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferenceitem.PreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencesummaryitem.PreferenceSummaryItem +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.LanguageUiModel +import de.rwth_aachen.phyphox.ui.skeleton +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.utils.UiResourceState + +@Composable +fun LanguagePreferenceItem( + modifier: Modifier = Modifier, + title: StringUIModel, + summary: UiResourceState, + iconRes: Int? = null, + onClick: () -> Unit = {}, +) { + + PreferenceItem( + modifier = modifier.clickable( + enabled = summary is UiResourceState.Success, + onClick = onClick, + ), + title = title, + iconRes = iconRes, + content = { + when (summary) { + UiResourceState.Loading -> Box( + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth() + .height(16.dp) + .skeleton(), + ) + + is UiResourceState.Success -> PreferenceSummaryItem( + primaryText = summary.data.displayName, + secondaryText = summary.data.displayCountry, + ) + } + }, + ) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencecategoryheader/PreferenceCategoryHeader.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencecategoryheader/PreferenceCategoryHeader.kt new file mode 100644 index 00000000..0798c27f --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencecategoryheader/PreferenceCategoryHeader.kt @@ -0,0 +1,41 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencecategoryheader + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.string.resolve +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme + +@Composable +fun PreferenceCategoryHeader( + modifier: Modifier = Modifier, + title: StringUIModel, +) { + Text( + text = title.resolve(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = modifier + .fillMaxWidth(), + ) +} + +@Preview(showBackground = true) +@Composable +internal fun SeekBarPreferenceItemPreview( + @PreviewParameter(PreferenceCategoryHeaderPreviewProvider::class) preview: StringUIModel, +) { + PhyphoxTheme { + Surface { + PreferenceCategoryHeader( + title = preview, + ) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencecategoryheader/PreferenceCategoryHeaderPreviewProvider.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencecategoryheader/PreferenceCategoryHeaderPreviewProvider.kt new file mode 100644 index 00000000..60dcfac1 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencecategoryheader/PreferenceCategoryHeaderPreviewProvider.kt @@ -0,0 +1,15 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencecategoryheader + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel + +class PreferenceCategoryHeaderPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + LoremIpsumStringUIModel(8), + ) +} + + + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferenceitem/PreferenceItem.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferenceitem/PreferenceItem.kt new file mode 100644 index 00000000..1667b939 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferenceitem/PreferenceItem.kt @@ -0,0 +1,112 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferenceitem + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.string.resolve +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme + +@Composable +fun PreferenceItem( + modifier: Modifier = Modifier, + title: StringUIModel, + iconRes: Int? = null, + defaultSpacing: Dp = 4.dp, + trailingContent: @Composable (RowScope.() -> Unit)? = null, + content: @Composable (ColumnScope.() -> Unit)? = null, +) { + Row( + modifier = modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(defaultSpacing), + ) { + iconRes?.let { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.width(16.dp)) + } + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(defaultSpacing), + ) { + Text(text = title.resolve(), style = MaterialTheme.typography.bodyLarge) + content?.let { content() } + } + trailingContent?.let { trailingContent() } + } +} + +@PreviewLightDark +@Composable +internal fun PreferenceItemPreview() { + PhyphoxTheme { + Surface { + PreferenceItem( + title = LoremIpsumStringUIModel(4), + iconRes = R.drawable.ic_dark_mode, + content = { + Text( + text = LoremIpsum(15).values.first(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } + } +} + +@PreviewLightDark +@Composable +internal fun PreferenceItemWithTrailingItemPreview() { + PhyphoxTheme { + Surface { + PreferenceItem( + title = LoremIpsumStringUIModel(4), + iconRes = R.drawable.ic_dark_mode, + content = { + Text( + text = LoremIpsum(15).values.first(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + trailingContent = { + Switch( + checked = true, + onCheckedChange = {}, + ) + }, + ) + } + } +} + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencesummaryitem/PreferenceSummaryItem.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencesummaryitem/PreferenceSummaryItem.kt new file mode 100644 index 00000000..89bf70a8 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/preferencesummaryitem/PreferenceSummaryItem.kt @@ -0,0 +1,53 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencesummaryitem + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.string.resolve +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme + + +@Composable +fun PreferenceSummaryItem( + modifier: Modifier = Modifier, + primaryText: StringUIModel, + secondaryText: StringUIModel? = null, +) { + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + modifier = modifier, + text = primaryText.resolve(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + secondaryText?.let { + Text( + modifier = modifier, + text = it.resolve(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + +} + +@PreviewLightDark +@Composable +internal fun PreferenceSummaryItemPreview() { + PhyphoxTheme { + PreferenceSummaryItem( + primaryText = LoremIpsumStringUIModel(4), + ) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarConfig.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarConfig.kt new file mode 100644 index 00000000..ab3bd2d0 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarConfig.kt @@ -0,0 +1,15 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem + +data class SeekBarConfig( + val currentSize: Float, + val range: ClosedFloatingPointRange, +) { + constructor( + currentSize: Float, + minSize: Float, + maxSize: Float, + ) : this( + currentSize = currentSize, + range = minSize..maxSize, + ) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarPreferenceItem.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarPreferenceItem.kt new file mode 100644 index 00000000..eed93c88 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarPreferenceItem.kt @@ -0,0 +1,74 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferenceitem.PreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencesummaryitem.PreferenceSummaryItem +import de.rwth_aachen.phyphox.ui.skeleton +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme +import de.rwth_aachen.phyphox.utils.UiResourceState + +@Composable +fun SeekBarPreferenceItem( + modifier: Modifier = Modifier, + title: StringUIModel, + iconRes: Int? = null, + summary: StringUIModel? = null, + seekBarConfig: UiResourceState, + onValueChange: (Float) -> Unit, +) { + PreferenceItem( + modifier = modifier, + title = title, + iconRes = iconRes, + content = { + summary?.let { + PreferenceSummaryItem(primaryText = summary) + } + when (seekBarConfig) { + UiResourceState.Loading -> Box( + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth() + .height(16.dp) + .skeleton(), + ) + + is UiResourceState.Success -> Slider( + value = seekBarConfig.data.currentSize, + onValueChange = onValueChange, + valueRange = seekBarConfig.data.range, + steps = (seekBarConfig.data.range.endInclusive - seekBarConfig.data.range.start).toInt() - 1, + ) + } + }, + ) +} + +@Preview(showBackground = true) +@Composable +internal fun SeekBarPreferenceItemPreview( + @PreviewParameter(SeekBarPreferenceItemPreviewProvider::class) graphSize: UiResourceState, +) { + PhyphoxTheme { + Surface { + SeekBarPreferenceItem( + title = LoremIpsumStringUIModel(2), + summary = LoremIpsumStringUIModel(12), + seekBarConfig = graphSize, + onValueChange = {}, + ) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarPreferenceItemPreviewProvider.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarPreferenceItemPreviewProvider.kt new file mode 100644 index 00000000..853e5108 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/seekbarpreferenceitem/SeekBarPreferenceItemPreviewProvider.kt @@ -0,0 +1,18 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.rwth_aachen.phyphox.utils.UiResourceState + +class SeekBarPreferenceItemPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + UiResourceState.Loading, + UiResourceState.Success( + SeekBarConfig( + currentSize = 1f, + minSize = 0f, + maxSize = 3f, + ), + ), + ) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/segmentedbuttonpreferenceitem/SegmentedButtonPreferenceItem.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/segmentedbuttonpreferenceitem/SegmentedButtonPreferenceItem.kt new file mode 100644 index 00000000..b28ee2e5 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/segmentedbuttonpreferenceitem/SegmentedButtonPreferenceItem.kt @@ -0,0 +1,105 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferenceitem.PreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencesummaryitem.PreferenceSummaryItem +import de.rwth_aachen.phyphox.ui.skeleton +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.string.resolve +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme +import de.rwth_aachen.phyphox.utils.UiResourceState + +@Composable +fun SegmentedButtonPreferenceItem( + modifier: Modifier = Modifier, + title: StringUIModel, + summary: StringUIModel? = null, + iconRes: Int? = null, + config: UiResourceState>>, + onOptionSelected: (AppUiMode) -> Unit, +) { + PreferenceItem( + modifier = modifier, + title = title, + iconRes = iconRes, + content = { + summary?.let { + PreferenceSummaryItem(primaryText = summary) + } + when (config) { + UiResourceState.Loading -> Box( + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth() + .height(16.dp) + .skeleton(), + ) + + is UiResourceState.Success>> -> SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth(), + ) { + config.data.forEachIndexed { index, option -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = config.data.size, + ), + onClick = { + onOptionSelected(option.item) + }, + selected = option.isSelected, + label = { Text(option.text.resolve()) }, + ) + } + } + } + + }, + ) +} + +@PreviewLightDark +@Composable +internal fun SegmentedButtonPreferenceItemPreview() { + PhyphoxTheme { + SegmentedButtonPreferenceItem( + title = LoremIpsumStringUIModel(4), + summary = LoremIpsumStringUIModel(12), + iconRes = R.drawable.ic_dark_mode, + config = UiResourceState.Success( + data = listOf( + SegmentedButtonUiModel( + item = AppUiMode.DARK, + text = LoremIpsumStringUIModel(2), + isSelected = true, + ), + SegmentedButtonUiModel( + item = AppUiMode.SYSTEM, + text = LoremIpsumStringUIModel(1), + isSelected = false, + ), + SegmentedButtonUiModel( + item = AppUiMode.LIGHT, + text = LoremIpsumStringUIModel(1), + isSelected = false, + ), + ), + ), + onOptionSelected = {}, + ) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/segmentedbuttonpreferenceitem/SegmentedButtonUiModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/segmentedbuttonpreferenceitem/SegmentedButtonUiModel.kt new file mode 100644 index 00000000..0d3dbafe --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/segmentedbuttonpreferenceitem/SegmentedButtonUiModel.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem + +import de.rwth_aachen.phyphox.ui.string.StringUIModel + +data class SegmentedButtonUiModel( + val item: T, + val text: StringUIModel, + val isSelected: Boolean = false, +) diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/switchpreferenceitem/SwitchPreferenceItem.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/switchpreferenceitem/SwitchPreferenceItem.kt new file mode 100644 index 00000000..004a241c --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/switchpreferenceitem/SwitchPreferenceItem.kt @@ -0,0 +1,91 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.switchpreferenceitem + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferenceitem.PreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencesummaryitem.PreferenceSummaryItem +import de.rwth_aachen.phyphox.ui.skeleton +import de.rwth_aachen.phyphox.ui.string.LoremIpsumStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme +import de.rwth_aachen.phyphox.utils.UiResourceState +import de.rwth_aachen.phyphox.utils.isChecked + +@Composable +fun SwitchPreferenceItem( + modifier: Modifier = Modifier, + title: StringUIModel, + summary: StringUIModel? = null, + iconRes: Int? = null, + checked: UiResourceState, + onCheckedChange: (Boolean) -> Unit, +) { + + PreferenceItem( + modifier = modifier + .clickable(checked is UiResourceState.Success) { + onCheckedChange(checked.isChecked()) + }, + title = title, + iconRes = iconRes, + content = { + summary?.let { + PreferenceSummaryItem(primaryText = summary) + } + }, + trailingContent = { + when (checked) { + UiResourceState.Loading -> Box( + modifier = Modifier + .padding(top = 16.dp) + .width(48.dp) + .height(32.dp) + .skeleton(), + ) + + is UiResourceState.Success -> + Switch( + checked = checked.isChecked(), + onCheckedChange = onCheckedChange, + ) + } + }, + ) +} + +@Preview( + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +@Preview( + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_NO, +) +@Composable +internal fun SwitchPreferenceItemPreview( + @PreviewParameter(SwitchPreferenceItemPreviewProvider::class) value: UiResourceState, +) { + PhyphoxTheme { + Surface { + SwitchPreferenceItem( + title = LoremIpsumStringUIModel(2), + summary = LoremIpsumStringUIModel(15), + iconRes = R.drawable.ic_dark_mode, + checked = value, + onCheckedChange = {}, + ) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/switchpreferenceitem/SwitchPreferenceItemPreviewProvider.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/switchpreferenceitem/SwitchPreferenceItemPreviewProvider.kt new file mode 100644 index 00000000..188526b4 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/common/switchpreferenceitem/SwitchPreferenceItemPreviewProvider.kt @@ -0,0 +1,13 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.common.switchpreferenceitem + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.rwth_aachen.phyphox.utils.UiResourceState + +class SwitchPreferenceItemPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + UiResourceState.Loading, + UiResourceState.Success(true), + UiResourceState.Success(false), + ) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/settingscontent/SettingsContent.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/settingscontent/SettingsContent.kt new file mode 100644 index 00000000..584e3bcb --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/compose/settingscontent/SettingsContent.kt @@ -0,0 +1,183 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.compose.settingscontent + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.clickablepreferenceitem.ClickablePreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.clickablepreferenceitem.LanguagePreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.preferencecategoryheader.PreferenceCategoryHeader +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem.SeekBarConfig +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem.SeekBarPreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem.SegmentedButtonPreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem.SegmentedButtonUiModel +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.switchpreferenceitem.SwitchPreferenceItem +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsAction +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.LanguageUiModel +import de.rwth_aachen.phyphox.ui.string.ResourceStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.theme.PhyphoxTheme +import de.rwth_aachen.phyphox.utils.UiResourceState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsContent( + modifier: Modifier = Modifier, + currentLanguage: UiResourceState, + seekbarConfig: UiResourceState, + appUiMode: UiResourceState>>, + accessPort: UiResourceState, + proximityLockEnabled: UiResourceState, + onActionEvent: (SettingsAction) -> Unit, +) { + LazyColumn( + modifier = modifier + .fillMaxSize(), + contentPadding = PaddingValues(16.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier + .width(172.dp), + contentScale = ContentScale.Fit, + painter = painterResource(R.drawable.settings_logo), + contentDescription = null, + ) + } + } + item { HorizontalDivider() } + item { + PreferenceCategoryHeader( + title = ResourceStringUIModel(resId = R.string.settingsHeadLanguage), + ) + } + item { + LanguagePreferenceItem( + title = ResourceStringUIModel(resId = R.string.settingsLanguage), + summary = currentLanguage, + iconRes = R.drawable.setting_language, + onClick = { + onActionEvent(SettingsAction.OnAppLanguageClicked) + }, + ) + } + item { + ClickablePreferenceItem( + title = ResourceStringUIModel(resId = R.string.settingsTranslation), + summary = UiResourceState.Success(ResourceStringUIModel(R.string.settingsTranslationMore)), + iconRes = R.drawable.setting_translate, + onClick = { + onActionEvent(SettingsAction.OnLearnMoreAboutTranslationClicked) + }, + ) + } + + item { HorizontalDivider() } + // Graph View Category + item { + PreferenceCategoryHeader( + title = ResourceStringUIModel(resId = R.string.settingGraphViewEdit), + ) + } + item { + SeekBarPreferenceItem( + title = ResourceStringUIModel(R.string.settingGraphSize), + summary = ResourceStringUIModel(R.string.settingGraphSizeSubTitle), + iconRes = R.drawable.ic_line_width, + seekBarConfig = seekbarConfig, + onValueChange = { + onActionEvent(SettingsAction.OnGraphSizeChanged(it)) + }, + ) + } + item { + SegmentedButtonPreferenceItem( + title = ResourceStringUIModel(resId = R.string.settings_theme_title), + config = appUiMode, + iconRes = R.drawable.ic_dark_mode, + onOptionSelected = { + onActionEvent(SettingsAction.OnUiModeItemSelected(it)) + }, + ) + } + item { + HorizontalDivider() + } + // Advanced Category + item { + PreferenceCategoryHeader( + title = ResourceStringUIModel(resId = R.string.settingsHeadAdvanced), + ) + } + item { + ClickablePreferenceItem( + title = ResourceStringUIModel(resId = R.string.settingsPort), + summary = accessPort, + iconRes = R.drawable.setting_http, + onClick = { + onActionEvent(SettingsAction.OnAccessPortClicked) + }, + ) + } + item { + SwitchPreferenceItem( + title = ResourceStringUIModel(resId = R.string.settingsProximityLock), + summary = ResourceStringUIModel(resId = R.string.settingsProximityLockDetail), + iconRes = R.drawable.setting_lock, + checked = proximityLockEnabled, + onCheckedChange = { + onActionEvent(SettingsAction.OnProximityLockChanged(it)) + }, + ) + } + } + +} + + +@Preview( + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +@Preview( + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_NO, +) +@Composable +fun SettingsContentPreview() { + PhyphoxTheme { + Surface { + SettingsContent( + currentLanguage = UiResourceState.Loading, + seekbarConfig = UiResourceState.Loading, + appUiMode = UiResourceState.Loading, + accessPort = UiResourceState.Loading, + proximityLockEnabled = UiResourceState.Loading, + onActionEvent = {}, + ) + } + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsAction.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsAction.kt new file mode 100644 index 00000000..2d51669e --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsAction.kt @@ -0,0 +1,18 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem.SegmentedButtonUiModel + +sealed interface SettingsAction { + + data class OnUiModeItemSelected(val appUiMode: AppUiMode) : SettingsAction + data class OnGraphSizeChanged(val size: Float) : SettingsAction + data class OnProximityLockChanged(val enabled: Boolean) : SettingsAction + data class OnAccessPortChanged(val newPort: String) : SettingsAction + data class OnAppLanguageChanged(val newLanguageIdentifier: String) : SettingsAction + data object OnModalDismissed : SettingsAction + data object OnAppLanguageClicked : SettingsAction + data object OnLearnMoreAboutTranslationClicked : SettingsAction + data object OnAccessPortClicked : SettingsAction + data object OnBackPressed : SettingsAction +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsEvent.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsEvent.kt new file mode 100644 index 00000000..501978bd --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsEvent.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel + +import androidx.annotation.StringRes + +sealed interface SettingsEvent { + data class OpenWebpage(val url: String) : SettingsEvent + data class OpenWebpageFromResourceID(@param:StringRes val urlResourceId: Int) : SettingsEvent + + data object NavigateBack : SettingsEvent +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsSheetUiModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsSheetUiModel.kt new file mode 100644 index 00000000..4fd112d4 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsSheetUiModel.kt @@ -0,0 +1,3 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel + +interface SettingsSheetUiModel diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsUiState.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsUiState.kt new file mode 100644 index 00000000..cd67dd59 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsUiState.kt @@ -0,0 +1,38 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem.SeekBarConfig +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem.SegmentedButtonUiModel +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.LanguageUiModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.utils.UiResourceState + +data class SettingsUiState( + val currentLanguage: UiResourceState = UiResourceState.Loading, + val graphSize: UiResourceState = UiResourceState.Loading, + val appUiMode: UiResourceState>> = UiResourceState.Loading, + val accessPort: UiResourceState = UiResourceState.Loading, + val proximityLockEnabled: UiResourceState = UiResourceState.Loading, + val modal: SettingsSheetUiModel? = null, +) { + constructor( + userSettings: UserSettings = UserSettings(), + modal: SettingsSheetUiModel? = null, + ) : this( + currentLanguage = userSettings.currentLanguage, + graphSize = userSettings.graphSize, + appUiMode = userSettings.appUiMode, + accessPort = userSettings.accessPort, + proximityLockEnabled = userSettings.proximityLockEnabled, + modal = modal, + ) +} + +data class UserSettings( + val currentLanguage: UiResourceState = UiResourceState.Loading, + val graphSize: UiResourceState = UiResourceState.Loading, + val appUiMode: UiResourceState>> = UiResourceState.Loading, + val accessPort: UiResourceState = UiResourceState.Loading, + val proximityLockEnabled: UiResourceState = UiResourceState.Loading, + // val dynamicTheme: ResourceState = ResourceState.Loading, +) diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsViewmodelViewModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsViewmodelViewModel.kt new file mode 100644 index 00000000..be0c8d0a --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/SettingsViewmodelViewModel.kt @@ -0,0 +1,114 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsEvent.NavigateBack +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsEvent.OpenWebpageFromResourceID +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport.AccessPortViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.AppLanguageViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.appuimode.UiModeViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.graphsize.GraphSizeViewmodelDelegate +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.proximitylock.ProximityLockViewmodelDelegate +import de.rwth_aachen.phyphox.utils.UIEventFlow +import de.rwth_aachen.phyphox.utils.asFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class SettingsViewmodelViewModel @Inject constructor( + private val accessPortDelegate: AccessPortViewmodelDelegate, + private val appLanguageDelegate: AppLanguageViewmodelDelegate, + private val appUiModeDelegate: UiModeViewmodelDelegate, + private val graphSizeDelegate: GraphSizeViewmodelDelegate, + private val proximityLockDelegate: ProximityLockViewmodelDelegate, +) : ViewModel(), + AccessPortViewmodelDelegate by accessPortDelegate, + AppLanguageViewmodelDelegate by appLanguageDelegate, + UiModeViewmodelDelegate by appUiModeDelegate, + GraphSizeViewmodelDelegate by graphSizeDelegate, + ProximityLockViewmodelDelegate by proximityLockDelegate { + + + private val _uiEvent = UIEventFlow() + val uiEvent = _uiEvent.asFlow() + + init { + start(viewModelScope) + } + + override fun start(scope: CoroutineScope) { + accessPortDelegate.start(scope) + appLanguageDelegate.start(scope) + appUiModeDelegate.start(scope) + graphSizeDelegate.start(scope) + proximityLockDelegate.start(scope) + } + + private val uiModalFlow = combine( + accessPortDelegate.portInputModal, + appLanguageDelegate.languageSelectionModal, + ) { modals: Array -> + modals.firstOrNull { it != null } + } + val uiState = combine( + accessPortDelegate.accessPortFlow, + appLanguageDelegate.currentAppLanguageFlow, + appUiModeDelegate.appUiModeFlow, + graphSizeDelegate.graphSizeFlow, + proximityLockDelegate.proximityLockFlow, + ) { accessPort, currentLanguage, uiMode, graphSize, proximityLockEnabled -> + UserSettings( + currentLanguage = currentLanguage, + graphSize = graphSize, + appUiMode = uiMode, + accessPort = accessPort, + proximityLockEnabled = proximityLockEnabled, + ) + }.combine(uiModalFlow) { userSettings, modal -> + SettingsUiState( + userSettings = userSettings, + modal = modal, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = SettingsUiState(), + ) + + fun onActionEvent(action: SettingsAction) { + viewModelScope.launch { + when (action) { + is SettingsAction.OnGraphSizeChanged -> graphSizeDelegate.updateGraphSize(action.size) + is SettingsAction.OnProximityLockChanged -> proximityLockDelegate.updateProximityLockStatus(action.enabled) + is SettingsAction.OnUiModeItemSelected -> appUiModeDelegate.updateAppUiMode(action.appUiMode) + is SettingsAction.OnAccessPortChanged -> accessPortDelegate.setAccessPort(action.newPort) + is SettingsAction.OnAppLanguageChanged -> appLanguageDelegate.updateLanguage(action.newLanguageIdentifier) + SettingsAction.OnAccessPortClicked -> accessPortDelegate.showAccessPortInputModal() + SettingsAction.OnAppLanguageClicked -> appLanguageDelegate.showLanguagePickerModal() + SettingsAction.OnModalDismissed -> dismissModal() + SettingsAction.OnBackPressed -> sendEvent(NavigateBack) + SettingsAction.OnLearnMoreAboutTranslationClicked -> sendEvent( + OpenWebpageFromResourceID( + R.string.settingsTranslation, + ), + ) + } + } + } + + + private fun sendEvent(event: SettingsEvent) = viewModelScope.launch { + _uiEvent.emit(event) + } + + override fun dismissModal() { + accessPortDelegate.dismissModal() + appLanguageDelegate.dismissModal() + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/UiBuilder.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/UiBuilder.kt new file mode 100644 index 00000000..8aecbd9e --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/UiBuilder.kt @@ -0,0 +1,140 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel + +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import de.rwth_aachen.phyphox.features.settings.domain.model.errors.AccessPortOutOfRange +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem.SeekBarConfig +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem.SegmentedButtonUiModel +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage.LanguageUiModel +import de.rwth_aachen.phyphox.ui.string.ResourceStringUIModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.string.toStringUIModel +import de.rwth_aachen.phyphox.utils.UiResourceState +import java.util.Locale +import javax.inject.Inject + +internal class UiBuilder @Inject constructor( + +) { + + //region - App Language + fun getSortedLanguageModels(supportedLanguages: List): List { + val systemDefaultModel = buildLanguageUiModel(AppLanguage.SYSTEM_DEFAULT) + val otherLanguageModels = supportedLanguages + .filter { it != AppLanguage.SYSTEM_DEFAULT } + .map { + Pair( + it.identifier, + Locale.forLanguageTag(it.identifier), + ) + } + .sortedBy { it.second.displayName } + .map { localeToLanguageUiModel(it.first, it.second) } + return listOf(systemDefaultModel) + otherLanguageModels + } + + fun buildLanguageUiModel(languageResource: UiResourceState): UiResourceState { + return when (languageResource) { + UiResourceState.Loading -> UiResourceState.Loading + is UiResourceState.Success -> UiResourceState.Success( + buildLanguageUiModel(languageResource.data), + ) + } + } + + fun buildLanguageUiModel(appLanguage: AppLanguage): LanguageUiModel { + return if (appLanguage.identifier == AppLanguage.SYSTEM_DEFAULT_IDENTIFIER) { + LanguageUiModel( + identifier = AppLanguage.SYSTEM_DEFAULT_IDENTIFIER, + displayName = R.string.settingsDefault.toStringUIModel(), + ) + } else { + val locale = Locale.forLanguageTag(appLanguage.identifier) + localeToLanguageUiModel(appLanguage.identifier, locale) + } + } + + fun localeToLanguageUiModel(identifier: String, locale: Locale): LanguageUiModel { + return LanguageUiModel( + identifier = identifier, + displayName = locale.getDisplayName(locale).toStringUIModel(), + localDisplayName = locale.displayName.toStringUIModel(), + displayCountry = locale.getDisplayCountry(locale).toStringUIModel(), + ) + } + //endregion + + //region - AppUiMode + fun buildAppUiModeResource( + current: UiResourceState, + modes: UiResourceState>, + ): UiResourceState>> { + return if (current is UiResourceState.Success && modes is UiResourceState.Success) { + UiResourceState.Success( + modes.data.map { mode -> + SegmentedButtonUiModel( + item = mode, + isSelected = current.data == mode, + text = getSupportedUiModeText(mode), + ) + }, + ) + } else { + UiResourceState.Loading + } + } + + private fun getSupportedUiModeText(appUiMode: AppUiMode): StringUIModel { + return when (appUiMode) { + AppUiMode.SYSTEM -> ResourceStringUIModel(R.string.settings_mode_dark_system) + AppUiMode.LIGHT -> ResourceStringUIModel(R.string.settings_mode_no_dark) + AppUiMode.DARK -> ResourceStringUIModel(R.string.settings_mode_dark) + } + } + + //endregion + + //region - AccessPort + fun buildAccessPortUiModel( + portResource: UiResourceState, + ): UiResourceState { + return if (portResource is UiResourceState.Success) { + UiResourceState.Success( + portResource.data.toString().toStringUIModel(), + ) + } else { + UiResourceState.Loading + } + } + //endregion + + //region - Graph Size + fun buildGraphSizeUiModel( + currentGraphSizeFlow: UiResourceState, + graphSizeRangeFlow: UiResourceState>, + ): UiResourceState { + return when { + currentGraphSizeFlow is UiResourceState.Success && graphSizeRangeFlow is UiResourceState.Success -> { + UiResourceState.Success( + SeekBarConfig( + currentSize = currentGraphSizeFlow.data, + range = graphSizeRangeFlow.data.start..graphSizeRangeFlow.data.endInclusive, + ), + ) + } + + else -> UiResourceState.Loading + } + } + + fun getErrorMessage(error: Throwable): StringUIModel { + return when (error) { + is AccessPortOutOfRange -> "${error.intRange.first} - ${error.intRange.last}".toStringUIModel() + else -> "Unknown error".toStringUIModel() + } + } + + + //endregion +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/SettingsViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/SettingsViewmodelDelegate.kt new file mode 100644 index 00000000..46acb2c6 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/SettingsViewmodelDelegate.kt @@ -0,0 +1,7 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates + +import kotlinx.coroutines.CoroutineScope + +interface SettingsViewmodelDelegate { + fun start(scope: CoroutineScope) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortSheetUiModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortSheetUiModel.kt new file mode 100644 index 00000000..d91cb6ef --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortSheetUiModel.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport + +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsSheetUiModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel + +data class AccessPortSheetUiModel( + val currentPort: StringUIModel, + val range: IntRange, + val error: StringUIModel? = null, +) : SettingsSheetUiModel diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortUiState.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortUiState.kt new file mode 100644 index 00000000..4340a980 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortUiState.kt @@ -0,0 +1,9 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport + +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsSheetUiModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel + +data class AccessPortUiState( + val currentAccessPort: StringUIModel, + val accessPortRange: IntRange, +) diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortViewmodelDelegate.kt new file mode 100644 index 00000000..5e8dafcc --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/AccessPortViewmodelDelegate.kt @@ -0,0 +1,17 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport + +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.SettingsViewmodelDelegate +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.flow.Flow + +interface AccessPortViewmodelDelegate : SettingsViewmodelDelegate { + val accessPortFlow: Flow> + val portInputModal: Flow + + suspend fun showAccessPortInputModal() + + suspend fun setAccessPort(newPort: String) + fun dismissModal() +} + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/DefaultAccessPortViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/DefaultAccessPortViewmodelDelegate.kt new file mode 100644 index 00000000..9a3927f2 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/accessport/DefaultAccessPortViewmodelDelegate.kt @@ -0,0 +1,72 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.accessport + +import de.rwth_aachen.phyphox.features.settings.domain.usecase.accessport.GetAccessPortRangeApplicationService +import de.rwth_aachen.phyphox.features.settings.domain.usecase.accessport.ObserveCurrentAccessPortUseCase +import de.rwth_aachen.phyphox.features.settings.domain.usecase.accessport.UpdateAccessPortUseCase +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.UiBuilder +import de.rwth_aachen.phyphox.ui.string.StringUIModel +import de.rwth_aachen.phyphox.ui.string.toStringUIModel +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class DefaultAccessPortViewmodelDelegate @Inject constructor( + private val observeCurrentAccessPort: ObserveCurrentAccessPortUseCase, + private val getAccessPortRange: GetAccessPortRangeApplicationService, + private val updateAccessPort: UpdateAccessPortUseCase, + private val uiBuilder: UiBuilder, +) : AccessPortViewmodelDelegate { + private val currentAccessPortStateFlow = MutableStateFlow>(UiResourceState.Loading) + + override val accessPortFlow: Flow> = currentAccessPortStateFlow.map { accessPort -> + uiBuilder.buildAccessPortUiModel(accessPort) + } + + private val inputModalStaFlow = MutableStateFlow(null) + override val portInputModal: Flow = inputModalStaFlow + + override fun start(scope: CoroutineScope) { + observeCurrentAccessPort().onStart { + currentAccessPortStateFlow.value = UiResourceState.Loading + }.onEach { + currentAccessPortStateFlow.value = UiResourceState.Success(it) + }.launchIn(scope) + } + + override suspend fun showAccessPortInputModal() { + val current = currentAccessPortStateFlow.value + val range = getAccessPortRange() + if ( + current is UiResourceState.Success + ) { + inputModalStaFlow.value = AccessPortSheetUiModel( + currentPort = current.data.toString().toStringUIModel(), + range = range, + ) + } + } + + override suspend fun setAccessPort(newPort: String) { + updateAccessPort(newPort) + .onSuccess { + inputModalStaFlow.value = null + }.onFailure { + inputModalStaFlow.value = inputModalStaFlow.value?.copy( + error = uiBuilder.getErrorMessage(it), + ) + } + } + + override fun dismissModal() { + inputModalStaFlow.value = null + } +} + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/AppLanguageSheetUiModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/AppLanguageSheetUiModel.kt new file mode 100644 index 00000000..c779bb04 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/AppLanguageSheetUiModel.kt @@ -0,0 +1,13 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.SettingsSheetUiModel +import de.rwth_aachen.phyphox.ui.string.StringUIModel + +data class AppLanguageSheetUiModel( + val title: StringUIModel, + val currentSelection: AppLanguage, + val availableLocales: List, +) : SettingsSheetUiModel + + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/AppLanguageViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/AppLanguageViewmodelDelegate.kt new file mode 100644 index 00000000..82ddb554 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/AppLanguageViewmodelDelegate.kt @@ -0,0 +1,17 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage + +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.SettingsViewmodelDelegate +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.flow.Flow + +interface AppLanguageViewmodelDelegate : SettingsViewmodelDelegate { + + val currentAppLanguageFlow: Flow> + + val languageSelectionModal: Flow + + suspend fun showLanguagePickerModal() + + suspend fun updateLanguage(identifier: String) + fun dismissModal() +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/DefaultAppLanguageViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/DefaultAppLanguageViewmodelDelegate.kt new file mode 100644 index 00000000..2a6651d9 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/DefaultAppLanguageViewmodelDelegate.kt @@ -0,0 +1,64 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage + +import de.rwth_aachen.phyphox.R +import de.rwth_aachen.phyphox.features.settings.domain.model.AppLanguage +import de.rwth_aachen.phyphox.features.settings.domain.usecase.language.GetSupportedLanguagesUseCase +import de.rwth_aachen.phyphox.features.settings.domain.usecase.language.ObserveCurrentAppLanguageUseCase +import de.rwth_aachen.phyphox.features.settings.domain.usecase.language.UpdateAppLanguageUseCase +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.UiBuilder +import de.rwth_aachen.phyphox.ui.string.toStringUIModel +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +internal class DefaultAppLanguageViewmodelDelegate @Inject constructor( + private val observeCurrentAppLanguage: ObserveCurrentAppLanguageUseCase, + private val getSupportedLanguages: GetSupportedLanguagesUseCase, + private val updateAppLanguage: UpdateAppLanguageUseCase, + private val uiBuilder: UiBuilder, +) : AppLanguageViewmodelDelegate { + private val currentLanguageFlow = MutableStateFlow>(UiResourceState.Loading) + override val currentAppLanguageFlow: Flow> = + currentLanguageFlow.map { appLanguage -> + uiBuilder.buildLanguageUiModel(appLanguage) + } + private val inputModalStaFlow = MutableStateFlow(null) + + override val languageSelectionModal: Flow = inputModalStaFlow + + override fun start(scope: CoroutineScope) { + observeCurrentAppLanguage().onStart { + currentLanguageFlow.value = UiResourceState.Loading + }.onEach { + currentLanguageFlow.value = UiResourceState.Success(it) + }.launchIn(scope) + } + + override suspend fun showLanguagePickerModal() { + val currentLocale = when (val temp = currentLanguageFlow.value) { + UiResourceState.Loading -> null + is UiResourceState.Success -> temp.data + } + val supportedLanguages = uiBuilder.getSortedLanguageModels(getSupportedLanguages()) + inputModalStaFlow.value = AppLanguageSheetUiModel( + title = R.string.settingsLanguage.toStringUIModel(), + currentSelection = currentLocale ?: AppLanguage.SYSTEM_DEFAULT, + availableLocales = supportedLanguages, + ) + } + + override suspend fun updateLanguage(identifier: String) { + updateAppLanguage(identifier) + dismissModal() + } + + override fun dismissModal() { + inputModalStaFlow.value = null + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/LanguageUiModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/LanguageUiModel.kt new file mode 100644 index 00000000..19b7d038 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/applanguage/LanguageUiModel.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.applanguage + +import de.rwth_aachen.phyphox.ui.string.StringUIModel + +data class LanguageUiModel( + val identifier: String, + val displayName: StringUIModel, + val localDisplayName: StringUIModel? = null, + val displayCountry: StringUIModel? = null, +) diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/appuimode/DefaultUiModeViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/appuimode/DefaultUiModeViewmodelDelegate.kt new file mode 100644 index 00000000..f2305ce5 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/appuimode/DefaultUiModeViewmodelDelegate.kt @@ -0,0 +1,21 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.appuimode + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem.SegmentedButtonUiModel +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +internal class DefaultUiModeViewmodelDelegate @Inject constructor() : UiModeViewmodelDelegate { + + override val appUiModeFlow: Flow>>> = + flowOf(UiResourceState.Loading) + + + override fun start(scope: CoroutineScope) {} + + override suspend fun updateAppUiMode(appUiMode: AppUiMode) {} +} + diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/appuimode/UiModeViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/appuimode/UiModeViewmodelDelegate.kt new file mode 100644 index 00000000..0a58476e --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/appuimode/UiModeViewmodelDelegate.kt @@ -0,0 +1,13 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.appuimode + +import de.rwth_aachen.phyphox.features.settings.domain.model.AppUiMode +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.segmentedbuttonpreferenceitem.SegmentedButtonUiModel +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.SettingsViewmodelDelegate +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.flow.Flow + +interface UiModeViewmodelDelegate : SettingsViewmodelDelegate { + val appUiModeFlow: Flow>>> + + suspend fun updateAppUiMode(appUiMode: AppUiMode) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/graphsize/DefaultGraphSizeViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/graphsize/DefaultGraphSizeViewmodelDelegate.kt new file mode 100644 index 00000000..e25e8606 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/graphsize/DefaultGraphSizeViewmodelDelegate.kt @@ -0,0 +1,65 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.graphsize + +import de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize.GetGraphSizeRangeUseCase +import de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize.ObserveCurrentGraphSizeUseCase +import de.rwth_aachen.phyphox.features.settings.domain.usecase.graphsize.UpdateGraphSizeUseCase +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem.SeekBarConfig +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.UiBuilder +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class DefaultGraphSizeViewmodelDelegate @Inject constructor( + private val observeCurrentGraphSize: ObserveCurrentGraphSizeUseCase, + private val getGraphSizeRange: GetGraphSizeRangeUseCase, + private val updateGraphSize: UpdateGraphSizeUseCase, + private val uiBuilder: UiBuilder, +) : GraphSizeViewmodelDelegate { + private val currentGraphSizeFlow = MutableStateFlow>( + UiResourceState.Loading, + ) + private val graphSizeRangeFlow = + MutableStateFlow>>( + UiResourceState.Loading, + ) + + override val graphSizeFlow: Flow> = combine( + currentGraphSizeFlow, + graphSizeRangeFlow, + ) { graphSize, range -> + uiBuilder.buildGraphSizeUiModel(graphSize, range) + } + + override fun start(scope: CoroutineScope) { + observeCurrentGraphConfig(scope) + scope.launch { + fetchGraphSizeRange() + } + } + + override suspend fun updateGraphSize(size: Float) { + TODO("Not yet implemented") + } + + private fun observeCurrentGraphConfig(scope: CoroutineScope) { + observeCurrentGraphSize().onStart { + currentGraphSizeFlow.value = UiResourceState.Loading + }.onEach { + currentGraphSizeFlow.value = UiResourceState.Success(it) + }.launchIn(scope) + } + + private suspend fun fetchGraphSizeRange() { + graphSizeRangeFlow.value = UiResourceState.Success( + getGraphSizeRange(), + ) + } + +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/graphsize/GraphSizeViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/graphsize/GraphSizeViewmodelDelegate.kt new file mode 100644 index 00000000..bffef838 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/graphsize/GraphSizeViewmodelDelegate.kt @@ -0,0 +1,11 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.graphsize + +import de.rwth_aachen.phyphox.features.settings.presentation.compose.common.seekbarpreferenceitem.SeekBarConfig +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.SettingsViewmodelDelegate +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.flow.Flow + +interface GraphSizeViewmodelDelegate : SettingsViewmodelDelegate{ + val graphSizeFlow: Flow> + suspend fun updateGraphSize(size: Float) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/proximitylock/DefaultProximityLockViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/proximitylock/DefaultProximityLockViewmodelDelegate.kt new file mode 100644 index 00000000..501a4ae0 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/proximitylock/DefaultProximityLockViewmodelDelegate.kt @@ -0,0 +1,37 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.proximitylock + +import de.rwth_aachen.phyphox.features.settings.domain.usecase.proximitylock.ObserveIsCurrentProximityLockEnabledUseCase +import de.rwth_aachen.phyphox.features.settings.domain.usecase.proximitylock.UpdateProximityLockStatusUseCase +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +class DefaultProximityLockViewmodelDelegate @Inject constructor( + private val observeProximityLockEnabled: ObserveIsCurrentProximityLockEnabledUseCase, + private val updateProximityLockEnabled: UpdateProximityLockStatusUseCase, +) : ProximityLockViewmodelDelegate { + + private val proximityLockEnabledFlow = MutableStateFlow>(UiResourceState.Loading) + + override val proximityLockFlow: Flow> = proximityLockEnabledFlow + + override fun start(scope: CoroutineScope) { + observeProximityLockEnabled() + .onStart { + proximityLockEnabledFlow.value = UiResourceState.Loading + } + .onEach { + proximityLockEnabledFlow.value = UiResourceState.Success(it) + }.launchIn(scope) + } + + + override suspend fun updateProximityLockStatus(enabled: Boolean) { + updateProximityLockEnabled(enabled) + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/proximitylock/ProximityLockViewmodelDelegate.kt b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/proximitylock/ProximityLockViewmodelDelegate.kt new file mode 100644 index 00000000..775d29c0 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/features/settings/presentation/viewmodel/delegates/proximitylock/ProximityLockViewmodelDelegate.kt @@ -0,0 +1,11 @@ +package de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.proximitylock + +import de.rwth_aachen.phyphox.features.settings.presentation.viewmodel.delegates.SettingsViewmodelDelegate +import de.rwth_aachen.phyphox.utils.UiResourceState +import kotlinx.coroutines.flow.Flow + +interface ProximityLockViewmodelDelegate: SettingsViewmodelDelegate { + val proximityLockFlow: Flow> + + suspend fun updateProximityLockStatus(enabled: Boolean) +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ui/ModifierExtensions.kt b/app/src/main/java/de/rwth_aachen/phyphox/ui/ModifierExtensions.kt new file mode 100644 index 00000000..225de182 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/ui/ModifierExtensions.kt @@ -0,0 +1,78 @@ +package de.rwth_aachen.phyphox.ui + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Extension to add a skeleton loading effect (shimmer) to a Composable. + */ +fun Modifier.skeleton( + show: Boolean = true, + radius: Dp = 16.dp, + shape: Shape = RoundedCornerShape(radius), +): Modifier = if (show) { + composed { + val transition = rememberInfiniteTransition(label = "skeleton") + val translateAnimation by transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, + easing = LinearEasing, + ), + repeatMode = RepeatMode.Restart, + ), + label = "shimmer", + ) + + val shimmerColors = listOf( + MaterialTheme.colorScheme.surfaceVariant, + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f), + MaterialTheme.colorScheme.surfaceVariant, + ) + + val brush = Brush.linearGradient( + colors = shimmerColors, + start = Offset(translateAnimation - 500f, translateAnimation - 500f), + end = Offset(translateAnimation, translateAnimation), + ) + + background(brush, shape) + } +} else { + this +} + +/** + * Marks a view as loading. When [isLoading] is true, the content is hidden and a skeleton + * shimmer is displayed in its place, preserving the size of the Composable. + */ +fun Modifier.placeholder( + isLoading: Boolean, + shape: Shape = RoundedCornerShape(4.dp), +): Modifier = if (isLoading) { + this + .drawWithContent { + // Do not draw the content + } + .skeleton(show = true, shape = shape) +} else { + this +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ui/string/LoremIpsumStringUIModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/LoremIpsumStringUIModel.kt new file mode 100644 index 00000000..76c475f7 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/LoremIpsumStringUIModel.kt @@ -0,0 +1,8 @@ +package de.rwth_aachen.phyphox.ui.string + +import android.content.Context +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum + +class LoremIpsumStringUIModel(val wordCount: Int = 500) : StringUIModel() { + override fun resolve(context: Context): String = LoremIpsum(wordCount).values.first() +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ui/string/ResourceStringUIModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/ResourceStringUIModel.kt new file mode 100644 index 00000000..11a5eace --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/ResourceStringUIModel.kt @@ -0,0 +1,35 @@ +package de.rwth_aachen.phyphox.ui.string + +import android.content.Context +import androidx.annotation.StringRes + +class ResourceStringUIModel( + @StringRes val resId: Int, + vararg val formatArgs: Any, +) : StringUIModel() { + override fun resolve(context: Context): String { + return if (formatArgs.isEmpty()) { + context.getString(resId, *resolveVarArgs(context, formatArgs)) + } else { + context.getString(resId, *resolveVarArgs(context, formatArgs)) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as ResourceStringUIModel + if (resId != other.resId) return false + if (!formatArgs.contentEquals(other.formatArgs)) return false + return true + } + + override fun hashCode(): Int { + return (9 * resId) + formatArgs.contentHashCode() + } + + override fun toString(): String { + return "ResourceStringUIModel(resId=$resId, formatArgs=${formatArgs.contentToString()})" + } + +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ui/string/StringUIModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/StringUIModel.kt new file mode 100644 index 00000000..dd526d54 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/StringUIModel.kt @@ -0,0 +1,20 @@ +package de.rwth_aachen.phyphox.ui.string + +import android.content.Context + +abstract class StringUIModel { + abstract fun resolve(context: Context): String + + protected fun resolveVarArgs( + context: Context, + args: Array, + ): Array { + return args.map { + if (it is StringUIModel) { + it.resolve(context) + } else { + it + } + }.toTypedArray() + } +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ui/string/StringUIModelExt.kt b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/StringUIModelExt.kt new file mode 100644 index 00000000..d90d81fc --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/StringUIModelExt.kt @@ -0,0 +1,30 @@ +package de.rwth_aachen.phyphox.ui.string + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Suppress("unused") +val emptyStringUiModel = "".toStringUIModel() + +@Suppress("unused") +fun String.toStringUIModel(): StringUIModel = TextStringUIModel(this) + +@Suppress("unused") +fun String?.toStringOrEmptyUIModel(): StringUIModel = this?.toStringUIModel() ?: emptyStringUiModel + +@Suppress("unused") +fun @receiver:StringRes Int.toStringUIModel( + vararg args: Any, +): StringUIModel = ResourceStringUIModel( + resId = this, + formatArgs = args, +) + +@Suppress("unused") +@Composable +fun StringUIModel.resolve() = resolve(LocalContext.current) + +@Suppress("unused") +@Composable +fun StringUIModel?.resolveOrDefault(default: String = ""): String = this?.resolve() ?: default diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ui/string/TextStringUIModel.kt b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/TextStringUIModel.kt new file mode 100644 index 00000000..55e37d4f --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/ui/string/TextStringUIModel.kt @@ -0,0 +1,7 @@ +package de.rwth_aachen.phyphox.ui.string + +import android.content.Context + +class TextStringUIModel(val value: String) : StringUIModel() { + override fun resolve(context: Context): String = value +} diff --git a/app/src/main/java/de/rwth_aachen/phyphox/ui/theme/Theme.kt b/app/src/main/java/de/rwth_aachen/phyphox/ui/theme/Theme.kt index e102b1f3..3b02e386 100644 --- a/app/src/main/java/de/rwth_aachen/phyphox/ui/theme/Theme.kt +++ b/app/src/main/java/de/rwth_aachen/phyphox/ui/theme/Theme.kt @@ -2,6 +2,7 @@ package de.rwth_aachen.phyphox.ui.theme import android.app.Activity import android.os.Build +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/de/rwth_aachen/phyphox/utils/UIEventFlow.kt b/app/src/main/java/de/rwth_aachen/phyphox/utils/UIEventFlow.kt new file mode 100644 index 00000000..41400f4f --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/utils/UIEventFlow.kt @@ -0,0 +1,30 @@ +package de.rwth_aachen.phyphox.utils + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.AbstractFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalCoroutinesApi::class) +class UIEventFlow : AbstractFlow() { + + private val events = Channel(capacity = 32, onBufferOverflow = BufferOverflow.DROP_OLDEST) + override suspend fun collectSafely(collector: FlowCollector) { + events.receiveAsFlow().collect { + collector.emit(it) + } + } + + suspend fun emit(event: T) { + withContext(Dispatchers.Main.immediate) { + events.send(event) + } + } +} + +fun UIEventFlow.asFlow(): Flow = this diff --git a/app/src/main/java/de/rwth_aachen/phyphox/utils/UiResourceState.kt b/app/src/main/java/de/rwth_aachen/phyphox/utils/UiResourceState.kt new file mode 100644 index 00000000..9a9417d9 --- /dev/null +++ b/app/src/main/java/de/rwth_aachen/phyphox/utils/UiResourceState.kt @@ -0,0 +1,10 @@ +package de.rwth_aachen.phyphox.utils + +sealed interface UiResourceState { + data object Loading : UiResourceState + data class Success(val data: T) : UiResourceState +} + +fun UiResourceState.isChecked(): Boolean { + return this is UiResourceState.Success && this.data +} diff --git a/app/src/main/res/drawable-night-xxxhdpi/settings_logo.png b/app/src/main/res/drawable-night-xxxhdpi/settings_logo.png new file mode 100644 index 00000000..99ede5ae Binary files /dev/null and b/app/src/main/res/drawable-night-xxxhdpi/settings_logo.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/settings_logo.png b/app/src/main/res/drawable-xxxhdpi/settings_logo.png new file mode 100644 index 00000000..a666984b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/settings_logo.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf281c4c..99dc1874 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ phyphox https://phyphox.org/privacy Settings + Back phyphox Pan and zoom Pick data diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2bb743d8..fcc98b2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,6 +41,7 @@ agp = "31.13.2" ktlint = "14.0.1" detekt = "1.23.8" detektCompose = "0.5.3" +datastore = "1.1.2" [libraries] androidx-multidex = { group = "androidx.multidex", name = "multidex", version.ref = "multidex" } @@ -90,6 +91,9 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +# DataStore +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } + #lint androidx-lint = { module = "com.android.tools.lint:lint", version.ref = "agp" } detekt-compose = { group = "io.nlopez.compose.rules", name = "detekt", version.ref = "detektCompose" }