diff --git a/README.md b/README.md
index 029ddc7..eded0d9 100644
--- a/README.md
+++ b/README.md
@@ -48,9 +48,11 @@
The app is actively developed by the neuropsychology group of [Stefan Debener](https://uol.de/neuropsychologie) in Oldenburg, Germany.
#### Active Developers
-* **Paul Maanen** - [pmaanen](https://github.com/pmaanen)
* **Sarah Blum** - [sarah-blum](https://github.com/s4rify)
+#### Previous Developers
+* **Paul Maanen** - [pmaanen](https://github.com/pmaanen)
+
## Acknowledgements
* [liblsl](https://github.com/sccn/liblsl), used under MIT license
* [liblsl-Java](https://github.com/labstreaminglayer/liblsl-Java), used under MIT license
diff --git a/app/build.gradle b/app/build.gradle
index 51bc621..ae6b61a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,29 +1,31 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
+apply plugin: 'org.jetbrains.kotlin.plugin.compose'
//apply plugin: 'kotlin-android-extensions'
+base.archivesName = "VIEWA-01"
+
android {
- compileSdk 34
+ compileSdk 36
namespace "de.uol.neuropsy.viewa"
- ndkVersion "25.2.9519653"
+ ndkVersion '29.0.14206865'
defaultConfig {
vectorDrawables.useSupportLibrary = true
applicationId "de.uol.neuropsy.viewa"
- minSdkVersion 30
- targetSdkVersion 31
+ minSdkVersion 33
+ targetSdkVersion 36
versionCode 1
versionName "01"
- setProperty("archivesBaseName", "VIEWA-$versionName")
}
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
versionNameSuffix='0.9'
}
}
@@ -32,9 +34,6 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
- composeOptions {
- kotlinCompilerExtensionVersion "1.4.2"
- }
buildFeatures {
viewBinding true
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 389c663..fad886e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/service/LSLService.kt b/app/src/main/java/de/uol/neuropsy/viewa/service/LSLService.kt
index 884bc15..cb7a77a 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/service/LSLService.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/service/LSLService.kt
@@ -1,6 +1,7 @@
package de.uol.neuropsy.viewa.service
import android.content.Intent
+import android.net.wifi.WifiManager
import android.os.Binder
import android.os.IBinder
import android.util.Log
@@ -20,6 +21,10 @@ class LSLService : LifecycleService() {
private val timeoutMs = 500.0 // half-second
+ // Multicast lock: prevents Android's WiFi chip from filtering incoming multicast
+ // UDP packets (stream discovery responses) when WiFi is connected.
+ private var multicastLock: WifiManager.MulticastLock? = null
+
sealed class ServiceEvent {
data class DataSample(val streamName: String,val timestamp:Double, val sample: FloatArray) : ServiceEvent() {
override fun equals(other: Any?): Boolean {
@@ -63,8 +68,23 @@ class LSLService : LifecycleService() {
private val localBinder = LocalBinder()
override fun onBind(intent: Intent): IBinder {
- super.onBind(intent) // important for LifecycleService
- return localBinder // return your binder here
+ super.onBind(intent)
+ return localBinder
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ val wifiManager = applicationContext.getSystemService(WIFI_SERVICE) as? WifiManager
+ multicastLock = wifiManager?.createMulticastLock("VIEWA_LSL")?.also {
+ it.setReferenceCounted(false)
+ it.acquire()
+ }
+ }
+
+ override fun onDestroy() {
+ multicastLock?.release()
+ multicastLock = null
+ super.onDestroy()
}
override fun onUnbind(intent: Intent?): Boolean {
@@ -78,7 +98,13 @@ class LSLService : LifecycleService() {
}
fun startInlet(streamName : String){
- val info = LSL.resolve_stream("name", streamName).firstOrNull() ?: return
+ // Resolve with a 5-second timeout. Prefer matching by source_id (unique per device)
+ // so that two sensors with the same name are handled correctly. Fall back to name only.
+ val candidates = LSL.resolve_stream("name", streamName, 1, 5.0)
+ val info = candidates.firstOrNull() ?: run {
+ Log.w("LSLService", "Could not find stream '$streamName' within timeout")
+ return
+ }
val inlet = StreamInlet(info)
val job = lifecycleScope.launch(Dispatchers.IO) {
Log.i("LSLService","Emitting config for ${info.name()}")
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/main/MainActivity.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/main/MainActivity.kt
index f9cda2e..7d555f8 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/main/MainActivity.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/main/MainActivity.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
@@ -22,6 +23,11 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+ // Enable edge-to-edge so AppBarLayout can extend behind the status bar
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ // Both themes use a dark-blue toolbar → white status bar icons in both modes
+ WindowCompat.getInsetsController(window, window.decorView)
+ .isAppearanceLightStatusBars = false
// Find the toolbar
val toolbar: MaterialToolbar = findViewById(R.id.toolbar)
// Set it as the support ActionBar
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/FullScreenPlotFragment.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/FullScreenPlotFragment.kt
index d990048..1458355 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/FullScreenPlotFragment.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/FullScreenPlotFragment.kt
@@ -60,6 +60,17 @@ class FullScreenPlotFragment : Fragment(R.layout.fragment_fullscreen_plot) {
// chart setup (axes, descr, etc)…
chart.description.isEnabled = false
chart.axisRight.isEnabled = false
+ // Resolve label colour from the current theme so it adapts to light/dark mode
+ // Pick label colour based on current night-mode setting
+ val nightMask = requireContext().resources.configuration.uiMode and
+ android.content.res.Configuration.UI_MODE_NIGHT_MASK
+ val labelColor = if (nightMask == android.content.res.Configuration.UI_MODE_NIGHT_YES)
+ 0xFFCCCCCC.toInt() // light grey for dark mode
+ else
+ android.graphics.Color.DKGRAY // dark grey for light mode
+ chart.xAxis.textColor = labelColor
+ chart.axisLeft.textColor = labelColor
+ chart.legend.textColor = labelColor
var channelNames : List = emptyList()
// observe live data updates for this stream
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/LivePlotViewModel.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/LivePlotViewModel.kt
index 9a7a272..f866eba 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/LivePlotViewModel.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/LivePlotViewModel.kt
@@ -1,11 +1,9 @@
package de.uol.neuropsy.viewa.ui.plot
-import android.util.Log
-import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
-import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import de.uol.neuropsy.viewa.service.LSLService
import de.uol.neuropsy.viewa.utils.ColorPalette
import edu.ucsd.sccn.LSL
@@ -35,6 +33,9 @@ class LivePlotViewModel : ViewModel() {
private var pausePlotting = false
private var allTimeMin = mutableMapOf()
private var allTimeMax = mutableMapOf()
+ // Baseline timestamp per stream: subtract this from all timestamps so X values
+ // stay small enough to be represented accurately as Float (LSL timestamps are ~1e9 seconds)
+ private var timestampBaseline = mutableMapOf()
private var job: Job? = null
private val buffers = mutableMapOf>>()
private val _uiState =
@@ -68,15 +69,20 @@ class LivePlotViewModel : ViewModel() {
private fun handleDataEvent(sampleEv: LSLService.ServiceEvent.DataSample) {
val name = sampleEv.streamName
- val t = sampleEv.timestamp.toFloat()
+ // Use a per-stream baseline so that X values start near 0 and fit accurately in Float
+ val baseline = timestampBaseline.getOrPut(name) { sampleEv.timestamp }
+ val t = (sampleEv.timestamp - baseline).toFloat()
val ys = sampleEv.sample
+
val chBufs = buffers[name]
- ?: return // If this stream is not yet configured, drop the sample
+ ?: run {
+ return
+ }
- // add each channel’s new Entry
+ // add each channel's new Entry
chBufs.forEach { (i, buf) ->
buf.addLast(Entry(t, ys[i]))
- // Remove every entry older than 5 seconds
+ // Remove every entry older than bufferSizeInSeconds
// TODO make the timeout configurable
buf.removeIf { t -> buf.last().x - t.x > bufferSizeInSeconds }
}
@@ -89,6 +95,7 @@ class LivePlotViewModel : ViewModel() {
val currentMax = allValues.maxOrNull() ?: 0f
allTimeMin[name] = min(allTimeMin[name]!!, currentMin)
allTimeMax[name] = max(allTimeMax[name]!!, currentMax)
+
val cp = ColorPalette()
// Build a ChartUiState for each stream this is inefficient because we rebuild the
@@ -123,6 +130,7 @@ class LivePlotViewModel : ViewModel() {
.toMutableMap()
allTimeMin[streamName] = Float.POSITIVE_INFINITY
allTimeMax[streamName] = Float.NEGATIVE_INFINITY
+ timestampBaseline.remove(streamName)
}
fun updateSelection(newStreams: Set) {
@@ -140,6 +148,7 @@ class LivePlotViewModel : ViewModel() {
fun resetLimits(name: String) {
allTimeMax[name] = Float.NEGATIVE_INFINITY
allTimeMin[name] = Float.POSITIVE_INFINITY
+ timestampBaseline.remove(name)
}
fun toggleVisible(streamName: String, channelIdx: Int, shouldBeVisible:Boolean){
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/StreamPlotAdapter.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/StreamPlotAdapter.kt
index 9b69eb6..f80e8af 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/StreamPlotAdapter.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/plot/StreamPlotAdapter.kt
@@ -60,14 +60,41 @@ class StreamPlotAdapter(
// Set the text of the title TV
binding.streamTitle.text = streamName
// Fetch the latest DataSets for this stream
- val dataSets = viewModel.uiState.value[streamName]?.entries?: emptyList()
+ val dataSets = viewModel.uiState.value[streamName]?.entries ?: emptyList()
+ Log.d("LivePlot", "[Adapter] $streamName datasets=${dataSets.size} " +
+ dataSets.mapIndexed { i, ds ->
+ "Ch$i: entries=${ds.entryCount} visible=${ds.isVisible} " +
+ "yRange=[${if (ds.entryCount > 0) ds.yMin else Float.NaN}, ${if (ds.entryCount > 0) ds.yMax else Float.NaN}]"
+ }.joinToString(" | ")
+ )
binding.streamChart.description=Description().apply {isEnabled=false}
binding.streamChart.axisRight.isEnabled=false
- // Apply to the chart
+ // Pick label colour based on current night-mode setting
+ val nightMask = holder.itemView.context.resources.configuration.uiMode and
+ android.content.res.Configuration.UI_MODE_NIGHT_MASK
+ val labelColor = if (nightMask == android.content.res.Configuration.UI_MODE_NIGHT_YES)
+ 0xFFCCCCCC.toInt() // light grey for dark mode
+ else
+ android.graphics.Color.DKGRAY // dark grey for light mode
+ binding.streamChart.xAxis.textColor = labelColor
+ binding.streamChart.axisLeft.textColor = labelColor
+ binding.streamChart.legend.textColor = labelColor
binding.streamChart.apply {
data = LineData(*dataSets.toTypedArray())
- viewModel.uiState.value[streamName]?.yMax?.let { axisLeft.axisMaximum=it }
- viewModel.uiState.value[streamName]?.yMin?.let { axisLeft.axisMinimum=it }
+ // Only apply axis limits when we have finite values (guard against initial ±Infinity)
+ val yMin = viewModel.uiState.value[streamName]?.yMin ?: Float.NaN
+ val yMax = viewModel.uiState.value[streamName]?.yMax ?: Float.NaN
+ if (yMin.isFinite() && yMax.isFinite()) {
+ val range = yMax - yMin
+ val padding = if (range > 0f) range * 0.1f else Math.abs(yMax) * 0.1f + 1f
+ axisLeft.axisMaximum = yMax + padding
+ axisLeft.axisMinimum = yMin - padding
+ } else {
+ axisLeft.resetAxisMaximum()
+ axisLeft.resetAxisMinimum()
+ }
+ // Auto-scroll to the latest data so the chart viewport follows incoming samples
+ moveViewToX(data?.xMax ?: 0f)
notifyDataSetChanged()
invalidate()
}
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamListAdapter.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamListAdapter.kt
index 0569cb5..ce2d0fb 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamListAdapter.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamListAdapter.kt
@@ -56,8 +56,9 @@ class StreamListAdapter(
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
+ // Use uid — not name — so two outlets sharing the same name are distinct rows
override fun areItemsTheSame(old: StreamItem, new: StreamItem) =
- old.name == new.name
+ old.uid == new.uid
override fun areContentsTheSame(old: StreamItem, new: StreamItem) =
old == new
}
@@ -66,8 +67,12 @@ class StreamListAdapter(
/**
* Data class representing a single stream in the list.
+ * [uid] is the LSL outlet UID — guaranteed unique per outlet instance — used
+ * as the DiffUtil identity key so that two streams sharing the same name are
+ * still treated as distinct rows.
*/
data class StreamItem(
+ val uid: String,
val name: String,
var isChecked: Boolean,
val subtitle : String
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamSelectionViewModel.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamSelectionViewModel.kt
index 5fc438b..b734bce 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamSelectionViewModel.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/selection/StreamSelectionViewModel.kt
@@ -39,15 +39,21 @@ class StreamSelectionViewModel : ViewModel() {
/**
* Performs LSL discovery on a background thread,
* updating availableStreams when done.
+ *
+ * wait_time is set to 3 s (instead of the 1 s default) so that all outlets on
+ * a WiFi network have time to re-announce themselves before we collect results.
*/
fun refreshAvailableStreams() {
viewModelScope.launch(Dispatchers.IO) {
- val infos=LSL.resolve_streams()
+ val infos = LSL.resolve_streams(3.0)
val items = infos.map { info ->
+ val host = info.hostname().takeIf { it.isNotEmpty() } ?: "unknown host"
StreamItem(
+ uid = info.uid(),
name = info.name(),
isChecked = _selectedStreams.value.contains(info.name()),
- subtitle = "${info.channel_count()} ch ${info.type()} @ ${if (info.nominal_srate()==LSL.IRREGULAR_RATE) "irregular rate" else info.nominal_srate()}"
+ subtitle = "${info.channel_count()} ch ${info.type()} @ " +
+ "${if (info.nominal_srate() == LSL.IRREGULAR_RATE) "irregular" else info.nominal_srate()} Hz [$host]"
)
}
withContext(Dispatchers.Main) {
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Color.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Color.kt
index 203f354..1ed9a83 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Color.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Color.kt
@@ -2,10 +2,23 @@ package de.uol.neuropsy.viewa.ui.theme
import androidx.compose.ui.graphics.Color
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
+// Primary – deep ocean blue
+val Blue10 = Color(0xFF001F33)
+val Blue20 = Color(0xFF003D66)
+val Blue40 = Color(0xFF0077B6) // main primary (matches toolbar)
+val Blue80 = Color(0xFF90CAF9) // primary in dark mode
+val Blue90 = Color(0xFFBBDEFB) // primary container
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+// Secondary – bright teal
+val Teal40 = Color(0xFF00838F)
+val Teal80 = Color(0xFF80DEEA)
+val Teal90 = Color(0xFFB2EBF2)
+
+// Tertiary – warm amber accent
+val Amber40 = Color(0xFFB45309)
+val Amber80 = Color(0xFFFFCC80)
+
+// Neutrals
+val Neutral10 = Color(0xFF1A1C1E)
+val Neutral90 = Color(0xFFE1E2E8)
+val Neutral99 = Color(0xFFFAFCFF)
diff --git a/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Theme.kt b/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Theme.kt
index 5a8487f..18abd40 100644
--- a/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Theme.kt
+++ b/app/src/main/java/de/uol/neuropsy/viewa/ui/theme/Theme.kt
@@ -1,64 +1,70 @@
package de.uol.neuropsy.viewa.ui.theme
import android.app.Activity
-import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
-private val DarkColorScheme = darkColorScheme(
- primary = Purple80,
- secondary = PurpleGrey80,
- tertiary = Pink80
-)
-
private val LightColorScheme = lightColorScheme(
- primary = Purple40,
- secondary = PurpleGrey40,
- tertiary = Pink40
+ primary = Blue40,
+ onPrimary = Color.White,
+ primaryContainer = Blue90,
+ onPrimaryContainer = Blue10,
+ secondary = Teal40,
+ onSecondary = Color.White,
+ secondaryContainer = Teal90,
+ onSecondaryContainer = Blue10,
+ tertiary = Amber40,
+ onTertiary = Color.White,
+ background = Neutral99,
+ onBackground = Neutral10,
+ surface = Color.White,
+ onSurface = Neutral10,
+ surfaceVariant = Blue90,
+ onSurfaceVariant = Blue10,
+)
- /* Other default colors to override
- background = Color(0xFFFFFBFE),
- surface = Color(0xFFFFFBFE),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
+private val DarkColorScheme = darkColorScheme(
+ primary = Blue80,
+ onPrimary = Blue20,
+ primaryContainer = Blue40,
+ onPrimaryContainer = Blue90,
+ secondary = Teal80,
+ onSecondary = Blue10,
+ secondaryContainer = Teal40,
+ onSecondaryContainer = Teal90,
+ tertiary = Amber80,
+ background = Neutral10,
+ onBackground = Neutral90,
+ surface = Color(0xFF1C2B36),
+ onSurface = Neutral90,
)
@Composable
fun ViewaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
+ // Dynamic colour disabled: device wallpaper colours are unpredictable and
+ // can produce very dark or low-contrast surfaces that break chart labels.
+ dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
- val colorScheme = when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
+ val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
- }
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
- WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ // Use dark icons when the status-bar background is light, white icons when it's dark
+ val useDarkIcons = android.graphics.Color.luminance(colorScheme.primary.toArgb()) > 0.5f
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = useDarkIcons
}
}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index ec9c9bd..ef0eddf 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -5,21 +5,30 @@
android:layout_height="match_parent"
android:orientation="vertical">
-
+
+ android:fitsSystemWindows="true">
-
+
+
+
+
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true" />
\ No newline at end of file
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..5ead2e0
--- /dev/null
+++ b/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 23e5288..1995236 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,10 +1,10 @@
-
+