Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • justJanne/meteroid
1 result
Select Git revision
Show changes
Commits on Source (2)
Showing
with 130 additions and 68 deletions
......@@ -3,9 +3,11 @@
![logo](/metadata/en-US/images/featureGraphic.png)
### What
Small Android application to use with mete, the Matekasse of Chaosdorf.
### Where
* [Chaosdorf Wiki](https://wiki.chaosdorf.de/Meteroid)
* [Google Play](https://play.google.com/store/apps/details?id=de.chaosdorf.meteroid2)
* [F-Droid](https://f-droid.org/repository/browse/?fdid=de.chaosdorf.meteroid)
......@@ -20,6 +22,7 @@ Small Android application to use with mete, the Matekasse of Chaosdorf.
![your logs](/metadata/en-US/images/phoneScreenshots/3.png)
### License
The MIT License (MIT)
Copyright (c) 2013-2016 Chaosdorf e.V.
......@@ -46,17 +49,20 @@ THE SOFTWARE.
The meteroid app stores a few things permanently on your device
("permanently" meaning, until you uninstall the app or clear its data):
* the URL of your chosen mete instance
* your user ID (just a number that identifies you)
* your last five used items
Additionally, some data may be cached temporarily:
* your avatar
* the avatars of other users
* the images of drinks
The rest of the data is kept in your mete instance. Please check its terms
and make sure you trust its operators:
* your chosen name (this can just be a nickname)
* your e-mail address (optional)
* your usage history (optional)
......@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
@JvmInline
value class BarcodeId(val value: Long) {
override fun toString() = value.toString()
fun isValid() = value >= 0L
}
......@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
@JvmInline
value class DrinkId(val value: Long) {
override fun toString() = value.toString()
fun isValid() = value >= 0L
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.mete.model
@JvmInline
value class ServerId(val value: Long) {
override fun toString() = value.toString()
fun isValid() = value >= 0L
}
......@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
@JvmInline
value class TransactionId(val value: Long) {
override fun toString() = value.toString()
fun isValid() = value >= 0L
}
......@@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable
@JvmInline
value class UserId(val value: Long) {
override fun toString() = value.toString()
fun isValid() = value >= 0L
}
......@@ -9,6 +9,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:supportsRtl="true"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
......
......@@ -27,13 +27,14 @@ package de.chaosdorf.meteroid
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.get
import dagger.hilt.android.AndroidEntryPoint
import de.chaosdorf.meteroid.ui.AppRouter
import de.chaosdorf.meteroid.ui.AppViewModel
import de.chaosdorf.meteroid.ui.theme.MeteroidTheme
import de.chaosdorf.meteroid.ui.MeteroidRouter
import de.chaosdorf.meteroid.theme.MeteroidTheme
@AndroidEntryPoint
......@@ -41,15 +42,19 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModelProvider = ViewModelProvider(this)
val viewModel = viewModelProvider.get<AppViewModel>()
val viewModel = viewModelProvider.get<MeteroidViewModel>()
installSplashScreen().setKeepOnScreenCondition {
viewModel.initialBackStack.value == null
viewModel.initialAccount.value == null
}
setContent {
val initialAccount by viewModel.initialAccount.collectAsState()
MeteroidTheme {
AppRouter(viewModel)
if (initialAccount != null) {
MeteroidRouter(initialAccount!!)
}
}
}
}
......
......@@ -22,32 +22,36 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui.servers
package de.chaosdorf.meteroid
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import de.chaosdorf.meteroid.sync.SyncManager
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ServerListViewModel @Inject constructor(
class MeteroidViewModel @Inject constructor(
accountPreferences: AccountPreferences,
serverRepository: ServerRepository,
private val preferences: AccountPreferences
syncManager: SyncManager
) : ViewModel() {
val servers: StateFlow<List<Server>> = serverRepository.getAllFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val initialAccount: StateFlow<AccountPreferences.State?> = accountPreferences.state
.filterNotNull()
.take(1)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
fun selectServer(serverId: ServerId) {
init {
viewModelScope.launch {
preferences.setServer(serverId)
serverRepository.getAllFlow().collectLatest { list ->
for (server in list) {
syncManager.sync(server, null, incremental = true)
}
}
}
}
}
......@@ -25,7 +25,7 @@
package de.chaosdorf.meteroid.storage
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.mete.model.ServerId
import kotlinx.coroutines.flow.Flow
interface AccountPreferences {
......
......@@ -30,7 +30,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.mete.model.ServerId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
......@@ -38,7 +38,6 @@ import javax.inject.Inject
class AccountPreferencesImpl @Inject constructor(
private val dataStore: DataStore<Preferences>
) : AccountPreferences {
override val state: Flow<AccountPreferences.State> =
dataStore.data.mapLatest {
val serverId = it[SERVER_KEY] ?: -1L
......
......@@ -24,13 +24,10 @@
package de.chaosdorf.meteroid.sync
import android.util.Log
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.AccountInfo
import de.chaosdorf.meteroid.model.PinnedUser
import de.chaosdorf.meteroid.model.PinnedUserRepository
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.model.*
import javax.inject.Inject
class AccountProvider @Inject constructor(
......@@ -49,8 +46,10 @@ class AccountProvider @Inject constructor(
suspend fun togglePin(serverId: ServerId, userId: UserId) {
if (pinnedUserRepository.isPinned(serverId, userId)) {
Log.e("DEBUG", "Unpinning $serverId, $userId")
pinnedUserRepository.delete(serverId, userId)
} else {
Log.e("DEBUG", "Pinning $serverId, $userId")
pinnedUserRepository.save(PinnedUser(serverId, userId))
}
}
......
......@@ -31,7 +31,7 @@ import de.chaosdorf.meteroid.MeteroidDatabase
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.meteroid.sync.base.BaseSyncHandler
import javax.inject.Inject
......
......@@ -31,7 +31,9 @@ import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.sync.base.SyncHandler
import de.chaosdorf.meteroid.util.newServer
import kotlinx.coroutines.flow.combine
import java.math.BigDecimal
import javax.inject.Inject
......@@ -42,6 +44,16 @@ class SyncManager @Inject constructor(
private val drinkSyncHandler: DrinkSyncHandler,
private val transactionSyncHandler: TransactionSyncHandler
) {
val syncState = combine(
userSyncHandler.state,
drinkSyncHandler.state,
transactionSyncHandler.state
) { states ->
states.find { it is SyncHandler.State.Error }
?: states.find { it == SyncHandler.State.Loading }
?: SyncHandler.State.Idle
}
suspend fun checkOffline(server: Server): Boolean {
val updated = factory.newServer(server.serverId, server.url)
return if (updated == null) {
......
......@@ -30,7 +30,7 @@ import de.chaosdorf.mete.model.TransactionId
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.MeteroidDatabase
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.meteroid.model.Transaction
import de.chaosdorf.meteroid.model.TransactionRepository
import de.chaosdorf.meteroid.sync.base.BaseIncrementalSyncHandler
......
......@@ -29,7 +29,7 @@ import de.chaosdorf.mete.model.MeteApiFactory
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.MeteroidDatabase
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.sync.base.BaseSyncHandler
......
......@@ -37,8 +37,8 @@ abstract class BaseIncrementalSyncHandler<Context, Entry, Key> :
}
override suspend fun syncIncremental(context: Context) {
if (syncState.compareAndSet(State.Idle, State.Loading) ||
syncState.compareAndSet(State.Error(), State.Loading)
if (syncState.compareAndSet(SyncHandler.State.Idle, SyncHandler.State.Loading) ||
syncState.compareAndSet(SyncHandler.State.Error(), SyncHandler.State.Loading)
) {
Log.w(this::class.simpleName, "Started incremental sync")
try {
......@@ -65,11 +65,11 @@ abstract class BaseIncrementalSyncHandler<Context, Entry, Key> :
}
}
}
syncState.value = State.Idle
syncState.value = SyncHandler.State.Idle
Log.w(this::class.simpleName, "Finished incremental sync")
} catch (e: Exception) {
Log.e(this::class.simpleName, "Error while syncing data", e)
syncState.value = State.Error("Error while syncing data: $e")
syncState.value = SyncHandler.State.Error("Error while syncing data: $e")
}
} else {
Log.w(this::class.simpleName, "Already syncing, disregarding sync request")
......
......@@ -29,22 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
abstract class BaseSyncHandler<Context, Entry, Key> : SyncHandler<Context> {
sealed class State {
data object Idle : State()
data object Loading : State()
data class Error(val message: String = "") : State() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
}
abstract suspend fun <T> withTransaction(block: suspend () -> T): T
abstract suspend fun loadCurrent(context: Context): List<Entry>
......@@ -55,12 +39,13 @@ abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> {
abstract suspend fun delete(key: Key)
abstract suspend fun store(entry: Entry)
protected val syncState = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = syncState
protected val syncState = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle)
val state: StateFlow<SyncHandler.State> = syncState
override suspend fun sync(context: Context) {
if (syncState.compareAndSet(State.Idle, State.Loading) ||
syncState.compareAndSet(State.Error(), State.Loading)) {
if (syncState.compareAndSet(SyncHandler.State.Idle, SyncHandler.State.Loading) ||
syncState.compareAndSet(SyncHandler.State.Error(), SyncHandler.State.Loading)
) {
Log.w(this::class.simpleName, "Started sync")
try {
val loadedEntries = loadCurrent(context)
......@@ -77,11 +62,11 @@ abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> {
store(loadedEntry)
}
}
syncState.value = State.Idle
syncState.value = SyncHandler.State.Idle
Log.w(this::class.simpleName, "Finished sync")
} catch (e: Exception) {
Log.e(this::class.simpleName, "Error while syncing data", e)
syncState.value = State.Error("Error while syncing data: $e")
syncState.value = SyncHandler.State.Error("Error while syncing data: $e")
}
} else {
Log.w(this::class.simpleName, "Already syncing, disregarding sync request")
......
......@@ -26,4 +26,20 @@ package de.chaosdorf.meteroid.sync.base
interface SyncHandler<Context> {
suspend fun sync(context: Context)
sealed class State {
data object Idle : State()
data object Loading : State()
data class Error(val message: String = "") : State() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
}
}