Shared KMP core and UI, switch to Koin

This commit is contained in:
2026-10-25 20:28:07 +03:00
parent f63b09a9d3
commit 69048452b8
161 changed files with 6626 additions and 4485 deletions

View File

@@ -0,0 +1,7 @@
package ru.stenfredd.sleepyowl.core.common
import android.content.Context
import java.io.File
/** The places the TV version always used: `files/datastore` (as `preferencesDataStore`) and `files`. */
fun androidAppDirs(context: Context) = AppDirs(preferences = File(context.filesDir, "datastore"), files = context.filesDir)

View File

@@ -0,0 +1,5 @@
package ru.stenfredd.sleepyowl.core.common
import android.os.SystemClock
actual fun monotonicMs(): Long = SystemClock.elapsedRealtime()

View File

@@ -0,0 +1,9 @@
package ru.stenfredd.sleepyowl.core.common
import timber.log.Timber
internal actual fun logEnabled() = Timber.treeCount > 0
internal actual fun platformLog(priority: Int, tag: String, t: Throwable?, message: String, args: Array<out Any?>) {
Timber.tag(tag).log(priority, t, message, *args)
}

View File

@@ -0,0 +1,24 @@
package ru.stenfredd.sleepyowl.core.common
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import java.io.File
import java.util.concurrent.ConcurrentHashMap
/**
* Where the app keeps its files on this platform.
*
* [preferences]: DataStore files (`<name>.preferences_pb`) — accounts, settings, remembered tracks. [files]: data that
* can be fetched again but should outlive a restart, such as the home screen snapshot.
*/
class AppDirs(val preferences: File, val files: File)
/** The DataStore of each preferences file: DataStore allows one instance per file, so everyone gets it here. */
class PreferenceStores(private val dirs: AppDirs) {
private val stores = ConcurrentHashMap<String, DataStore<Preferences>>()
operator fun get(name: String): DataStore<Preferences> = stores.computeIfAbsent(name) {
PreferenceDataStoreFactory.create(produceFile = { File(dirs.preferences, "$name.preferences_pb") })
}
}

View File

@@ -0,0 +1,4 @@
package ru.stenfredd.sleepyowl.core.common
/** Milliseconds of a clock that only goes forward; on Android it keeps counting while the device sleeps. */
expect fun monotonicMs(): Long

View File

@@ -0,0 +1,41 @@
package ru.stenfredd.sleepyowl.core.common
/**
* The app's log on every platform. Like Timber: [message] is a `String.format` pattern, formatted only when [args]
* are given, and the tag is the calling class. Android writes through Timber, desktop to stderr.
*/
object Log {
const val DEBUG = 3
const val INFO = 4
const val WARN = 5
const val ERROR = 6
fun d(message: String, vararg args: Any?) = write(DEBUG, null, message, args)
fun d(t: Throwable?, message: String, vararg args: Any?) = write(DEBUG, t, message, args)
fun i(message: String, vararg args: Any?) = write(INFO, null, message, args)
fun i(t: Throwable?, message: String, vararg args: Any?) = write(INFO, t, message, args)
fun w(message: String, vararg args: Any?) = write(WARN, null, message, args)
fun w(t: Throwable?, message: String, vararg args: Any?) = write(WARN, t, message, args)
fun e(message: String, vararg args: Any?) = write(ERROR, null, message, args)
fun e(t: Throwable?, message: String, vararg args: Any?) = write(ERROR, t, message, args)
private fun write(priority: Int, t: Throwable?, message: String, args: Array<out Any?>) {
if (logEnabled()) platformLog(priority, callerTag(), t, message, args)
}
/**
* Simple name of the class that called [Log], without `$1` of lambdas — the tag Timber's DebugTree gave. Our own
* frames are told apart by the class name at run time, as Timber does: R8 may merge [Log] into another class.
*/
private fun callerTag(): String {
val own = javaClass.name
val caller = Throwable().stackTrace.firstOrNull { it.className != own } ?: return "SleepyOwl"
return caller.className.substringAfterLast('.').replace(ANONYMOUS_CLASS, "")
}
private val ANONYMOUS_CLASS = Regex("(\\$\\d+)+$")
}
internal expect fun logEnabled(): Boolean
internal expect fun platformLog(priority: Int, tag: String, t: Throwable?, message: String, args: Array<out Any?>)

View File

@@ -0,0 +1,10 @@
package ru.stenfredd.sleepyowl.core.common
import java.io.File
/** XDG places: settings in `~/.config/sleepyowl`, refetchable data in `~/.cache/sleepyowl`. */
fun desktopAppDirs(): AppDirs {
fun xdg(variable: String, default: String) =
File(System.getenv(variable)?.takeIf { it.isNotBlank() } ?: (System.getProperty("user.home") + "/" + default), "sleepyowl")
return AppDirs(preferences = xdg("XDG_CONFIG_HOME", ".config"), files = xdg("XDG_CACHE_HOME", ".cache"))
}

View File

@@ -0,0 +1,3 @@
package ru.stenfredd.sleepyowl.core.common
actual fun monotonicMs(): Long = System.nanoTime() / 1_000_000

View File

@@ -0,0 +1,18 @@
package ru.stenfredd.sleepyowl.core.common
import java.time.LocalTime
import java.time.format.DateTimeFormatter
private val TIME = DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
private val LEVELS = mapOf(Log.DEBUG to 'D', Log.INFO to 'I', Log.WARN to 'W', Log.ERROR to 'E')
internal actual fun logEnabled() = true
internal actual fun platformLog(priority: Int, tag: String, t: Throwable?, message: String, args: Array<out Any?>) {
val text = if (args.isEmpty()) message else message.format(*args)
val line = "${LocalTime.now().format(TIME)} ${LEVELS[priority]} $tag: $text"
synchronized(System.err) {
System.err.println(line)
t?.printStackTrace()
}
}