Skip to content
Snippets Groups Projects
Unverified Commit 803d14f4 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

wip: initial version with hilt

parent 489dc64a
No related branches found
No related tags found
No related merge requests found
......@@ -2,15 +2,27 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa",
"identityHash": "74b0d505a7abb27362a1ee0986b700e0",
"entities": [
{
"tableName": "Drink",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "drinkId",
"columnName": "drinkId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
......@@ -38,12 +50,6 @@
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
......@@ -90,19 +96,32 @@
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
"serverId",
"drinkId"
]
},
"indices": [],
"foreignKeys": []
"foreignKeys": [
{
"table": "Server",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
],
"referencedColumns": [
"serverId"
]
}
]
},
{
"tableName": "Server",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`serverId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true
},
......@@ -128,17 +147,104 @@
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
"serverId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` REAL NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "drinkId",
"columnName": "drinkId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "balance",
"columnName": "balance",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "audit",
"columnName": "audit",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "redirect",
"columnName": "redirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"serverId",
"drinkId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "Server",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
],
"referencedColumns": [
"serverId"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '74b0d505a7abb27362a1ee0986b700e0')"
]
}
}
\ No newline at end of file
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa",
"entities": [
{
"tableName": "Drink",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "volume",
"columnName": "volume",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "caffeine",
"columnName": "caffeine",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "price",
"columnName": "price",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "logoUrl",
"columnName": "logoUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoFileName",
"columnName": "logoFileName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoContentType",
"columnName": "logoContentType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoFileSize",
"columnName": "logoFileSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "logoUpdatedAt",
"columnName": "logoUpdatedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Server",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoUrl",
"columnName": "logoUrl",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')"
]
}
}
\ No newline at end of file
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "e8f8c6c1efa75d20c3aa764efc3037aa",
"entities": [
{
"tableName": "Drink",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `active` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "volume",
"columnName": "volume",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "caffeine",
"columnName": "caffeine",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "price",
"columnName": "price",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "logoUrl",
"columnName": "logoUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoFileName",
"columnName": "logoFileName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoContentType",
"columnName": "logoContentType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoFileSize",
"columnName": "logoFileSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "logoUpdatedAt",
"columnName": "logoUpdatedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Server",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "logoUrl",
"columnName": "logoUrl",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8f8c6c1efa75d20c3aa764efc3037aa')"
]
}
}
\ No newline at end of file
......@@ -29,21 +29,25 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.DrinkDao
import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerDao
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter
@Database(
version = 1,
entities = [
Drink::class,
Server::class
Server::class,
User::class
],
autoMigrations = [],
)
@TypeConverters(value = [KotlinDatetimeTypeConverter::class])
abstract class MeteroidDatabase : RoomDatabase() {
abstract fun drinks(): DrinkDao
abstract fun server(): ServerDao
abstract fun drinks(): DrinkRepository
abstract fun server(): ServerRepository
abstract fun users(): UserRepository
}
......@@ -26,25 +26,29 @@ package de.chaosdorf.meteroid.model
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import de.chaosdorf.mete.DrinkId
import de.chaosdorf.mete.v1.DrinkModelV1
import de.chaosdorf.meteroid.Repository
import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.Instant
@Entity
@Entity(
primaryKeys = ["serverId", "drinkId"],
foreignKeys = [
ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE)
]
)
data class Drink(
@PrimaryKey
val id: DrinkId,
val serverId: ServerId,
val drinkId: DrinkId,
val active: Boolean,
val name: String,
val volume: Double,
val caffeine: Int?,
val price: Double,
val active: Boolean,
val createdAt: Instant,
val updatedAt: Instant,
val logoUrl: String,
......@@ -54,13 +58,14 @@ data class Drink(
val logoUpdatedAt: Instant
) {
companion object {
fun fromModelV1(value: DrinkModelV1) = Drink(
fun fromModelV1(serverId: ServerId, value: DrinkModelV1) = Drink(
serverId,
value.id,
value.active,
value.name,
value.bottleSize,
value.caffeine,
value.price,
value.active,
value.createdAt,
value.updatedAt,
value.logoUrl,
......@@ -73,27 +78,25 @@ data class Drink(
}
@Dao
interface DrinkDao : Repository<DrinkId, Drink> {
override fun getKey(value: Drink): DrinkId = value.id
@Query("SELECT * FROM Drink WHERE id = :id LIMIT 1")
override suspend fun get(id: DrinkId): Drink?
interface DrinkRepository {
@Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1")
suspend fun get(serverId: ServerId, drinkId: DrinkId): Drink?
@Query("SELECT * FROM Drink WHERE id = :id LIMIT 1")
override fun getFlow(id: DrinkId): Flow<Drink?>
@Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1")
fun getFlow(serverId: ServerId, drinkId: DrinkId): Flow<Drink?>
@Query("SELECT * FROM Drink")
override suspend fun getAll(): List<Drink>
@Query("SELECT * FROM Drink WHERE serverId = :serverId")
suspend fun getAll(serverId: ServerId): List<Drink>
@Query("SELECT * FROM Drink")
override fun getAllFlow(): Flow<List<Drink>>
@Query("SELECT * FROM Drink WHERE serverId = :serverId")
fun getAllFlow(serverId: ServerId): Flow<List<Drink>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
override suspend fun save(drink: Drink)
suspend fun save(drink: Drink)
@Query("DELETE FROM Drink WHERE id = :id")
override suspend fun delete(id: DrinkId)
@Query("DELETE FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId")
suspend fun delete(serverId: ServerId, drinkId: DrinkId)
@Query("DELETE FROM Drink")
override suspend fun deleteAll()
@Query("DELETE FROM Drink WHERE serverId = :serverId")
suspend fun deleteAll(serverId: ServerId)
}
......@@ -30,8 +30,6 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import de.chaosdorf.mete.PwaManifest
import de.chaosdorf.meteroid.Repository
import kotlinx.coroutines.flow.Flow
@JvmInline
......@@ -40,34 +38,32 @@ value class ServerId(val value: Long)
@Entity
data class Server(
@PrimaryKey
val id: ServerId,
val serverId: ServerId,
val name: String?,
val url: String,
val logoUrl: String?
)
@Dao
interface ServerDao : Repository<ServerId, Server> {
override fun getKey(value: Server): ServerId = value.id
interface ServerRepository {
@Query("SELECT * FROM Server WHERE serverId = :id LIMIT 1")
suspend fun get(id: ServerId): Server?
@Query("SELECT * FROM Server WHERE id = :id LIMIT 1")
override suspend fun get(id: ServerId): Server?
@Query("SELECT * FROM Server WHERE id = :id LIMIT 1")
override fun getFlow(id: ServerId): Flow<Server?>
@Query("SELECT * FROM Server WHERE serverId = :id LIMIT 1")
fun getFlow(id: ServerId): Flow<Server?>
@Query("SELECT * FROM Server")
override suspend fun getAll(): List<Server>
suspend fun getAll(): List<Server>
@Query("SELECT * FROM Server")
override fun getAllFlow(): Flow<List<Server>>
fun getAllFlow(): Flow<List<Server>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
override suspend fun save(drink: Server)
suspend fun save(server: Server)
@Query("DELETE FROM Server WHERE id = :id")
override suspend fun delete(id: ServerId)
@Query("DELETE FROM Server WHERE serverId = :id")
suspend fun delete(id: ServerId)
@Query("DELETE FROM Server")
override suspend fun deleteAll()
suspend fun deleteAll()
}
......@@ -22,71 +22,74 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.di
package de.chaosdorf.meteroid.model
import de.chaosdorf.meteroid.Repository
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import de.chaosdorf.mete.DrinkId
import de.chaosdorf.mete.UserId
import de.chaosdorf.mete.v1.UserModelV1
import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.Instant
interface ServerSelectionViewModelFactory {
fun newInstance(
scope: CoroutineScope,
repository: Repository<ServerId, Server>,
hasSelectedServer: Boolean,
onAddServer: () -> Unit,
onSelect: (server: ServerId) -> Unit,
onClose: () -> Unit
) = ServerSelectionViewModelImpl(
scope,
repository,
hasSelectedServer,
onAddServer,
onSelect,
onClose
@Entity(
primaryKeys = ["serverId", "drinkId"],
foreignKeys = [
ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE)
]
)
data class User(
val serverId: ServerId,
val drinkId: UserId,
val active: Boolean,
val name: String,
val email: String,
val balance: Double,
val audit: Boolean,
val redirect: Boolean,
val createdAt: Instant,
val updatedAt: Instant,
) {
companion object {
fun fromModelV1(serverId: ServerId, value: UserModelV1) = User(
serverId,
value.id,
value.active,
value.name,
value.email,
value.balance,
value.audit,
value.redirect,
value.createdAt,
value.updatedAt
)
}
interface ServerSelectionViewModel {
val servers: StateFlow<List<Server>>
val hasSelectedServer: Boolean
fun addServer()
fun select(server: ServerId)
fun remove(server: ServerId)
fun close()
}
class ServerSelectionViewModelImpl(
private val scope: CoroutineScope,
private val repository: Repository<ServerId, Server>,
override val hasSelectedServer: Boolean,
private val onAddServer: () -> Unit,
private val onSelect: (server: ServerId) -> Unit,
private val onClose: () -> Unit
) : ServerSelectionViewModel {
override val servers: StateFlow<List<Server>> =
repository.getAllFlow().stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
@Dao
interface UserRepository {
@Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1")
suspend fun get(serverId: ServerId, drinkId: DrinkId): Drink?
override fun addServer() {
onAddServer()
}
@Query("SELECT * FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId LIMIT 1")
fun getFlow(serverId: ServerId, drinkId: DrinkId): Flow<Drink?>
override fun select(server: ServerId) {
onSelect(server)
}
@Query("SELECT * FROM Drink WHERE serverId = :serverId")
suspend fun getAll(serverId: ServerId): List<Drink>
override fun remove(server: ServerId) {
scope.launch {
repository.delete(server)
}
}
@Query("SELECT * FROM Drink WHERE serverId = :serverId")
fun getAllFlow(serverId: ServerId): Flow<List<Drink>>
override fun close() {
onClose()
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(drink: Drink)
@Query("DELETE FROM Drink WHERE serverId = :serverId AND drinkId = :drinkId")
suspend fun delete(serverId: ServerId, drinkId: DrinkId)
@Query("DELETE FROM Drink WHERE serverId = :serverId")
suspend fun deleteAll(serverId: ServerId)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment