Skip to content
Snippets Groups Projects
Verified Commit 3be9694f authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

feat: build initial demo

parent 9945a4f1
No related branches found
No related tags found
No related merge requests found
Showing
with 1575 additions and 0 deletions
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
[{*.mod, *.dtd, *.ent, *.elt}]
indent_style = space
indent_size = 2
[{*.jhm, *.rng, *.wsdl, *.fxml, *.xslt, *.jrxml, *.ant, *.xul, *.xsl, *.xsd, *.tld, *.jnlp, *.xml}]
indent_style = space
indent_size = 2
[*.json]
indent_style = space
indent_size = 2
[*.java]
indent_style = space
indent_size = 2
[{*.kts, *.kt}]
indent_style = space
indent_size = 2
[{*.yml, *.yaml}]
indent_style = space
indent_size = 2
*.iml
.gradle
/local.properties
/signing.properties
/.idea/*
!/.idea/copyright/
.DS_Store
/captures
build/
/reports/
/persistence/schemas/
image: "k8r.eu/justjanne/android-sdk:f918b1cd"
cache:
key: "$CI_PROJECT_NAME"
paths:
- ".gradle/caches"
before_script:
- "export GRADLE_USER_HOME=$(pwd)/.gradle"
- "chmod +x ./gradlew"
- "echo $SIGNING_KEYSTORE | base64 -d > /root/signing.keystore"
- "echo $SIGNING_PROPERTIES | base64 -d > signing.properties"
stages:
- "build"
- "deploy"
test:
stage: "build"
script:
- "./gradlew assembleRelease -x lintRelease -x lintVitalRelease"
- "cp app/build/outputs/apk/release/*.apk ."
- "./gradlew check -x connectedCheck --stacktrace"
artifacts:
paths:
- "*.apk"
- "*/build/test-results/**/TEST-*.xml"
- "*/build/reports/*.xml"
reports:
junit:
- "*/build/test-results/**/TEST-*.xml"
- "*/build/reports/*.xml"
rules:
- if: "$CI_COMMIT_BRANCH == 'main'"
when: on_success
version:
stage: "build"
script:
- "export VERSION_NAME=$(git describe --abbrev=0 --tags HEAD)"
- "export VERSION_CODE=$(git rev-list --count $VERSION_NAME)"
- "echo \"{\\\"name\\\":\\\"$VERSION_NAME\\\",\\\"code\\\":$VERSION_CODE}\" > version.json"
artifacts:
paths:
- "version.json"
rules:
- if: "$CI_COMMIT_BRANCH == 'main'"
when: on_success
deploy-local:
stage: "deploy"
image: "k8r.eu/justjanne/docker-s3cmd:latest"
cache: { }
dependencies:
- "test"
- "version"
script:
- "echo $S3_CONFIG | base64 -d > $HOME/.s3cfg"
- "export VERSION=$(ls *.apk)"
- "s3cmd put $VERSION s3://releases/quasseldroid-ng/$VERSION"
- "s3cmd put version.json s3://releases/quasseldroid-ng/version.json"
- "s3cmd cp s3://releases/quasseldroid-ng/$VERSION s3://releases/quasseldroid-ng/Quasseldroid-latest.apk"
rules:
- if: "$CI_COMMIT_BRANCH == 'main' && $S3_CONFIG != ''"
when: on_success
deploy-beta:
stage: "deploy"
image: "k8r.eu/justjanne/docker-fastlane:latest"
cache: { }
dependencies:
- "test"
script:
- "echo $FASTLANE_CONFIG | base64 -d > $HOME/key.json"
- "export VERSION=$(ls *.apk)"
- "fastlane supply --apk $VERSION --track beta --json_key $HOME/key.json --package_name com.iskrembilen.quasseldroid --skip_upload_metadata=true --skip_upload_images=true --skip_upload_screenshots=true"
rules:
- if: "$CI_COMMIT_BRANCH == 'main' && $FASTLANE_CONFIG != ''"
when: on_success
This diff is collapsed.
# [Quasseldroid]
[![Release Version](https://img.shields.io/github/release/justjanne/quasseldroid-ng/all.svg)](https://github.com/justjanne/Quasseldroid-ng/releases)
Quassel is a distributed, decentralized IRC client, written using C++ and Qt. Quasseldroid is a
pure-java client for the Quassel core, allowing you to connect to your Quassel core using your
Android™ phone.
[![Screenshot of Quasseldroid on Phone and Tablet](https://i.k8r.eu/2G2ToAh.png)](https://i.k8r.eu/2G2ToA.png)
## Build Requirements
Quasseldroid requires you to have the latest version of gradle installed, and a recent version of
the Android SDK installed (and configured via the environment variable ANDROID_HOME)
## Building
The build process uses gradle, `./gradlew assemble` builds all versions.
Unit tests are supported, and can be run with `./gradlew check`
To sign your releases, [generate a keypair] and create a file named `signing.properties` with the
following content to let gradle automatically sign your builds.
```
storeFile=/path/to/your/keystore/here.keystore
storePassword=passwordofyourkeystorehere
keyAlias=nameofyourkeyhere
keyPassword=passwordofyourkeyhere
```
## Authors
* **Janne Mareike Koschinski** (justJanne)
Rewrite, UI, Annotation Processors, Backend
## Acknowledgements
This project was inspired by and is based on [Quasseldroid Legacy] as well as [Quassel].
Authors of legacy Quasseldroid:
* **Frederik M. J. Vestre** (freqmod)
Initial qdatastream deserialization attempts
* **Martin "Java Sucks" Sandsmark** (sandsmark)
Legacy protocol implementation, (de)serializers, project (de)moralizer
* **Magnus Fjell** (magnuf)
Legacy UI
* **Ken Børge Viktil** (Kenji)
Legacy UI
## License
> This program is free software: you can redistribute it and/or modify it
> under the terms of the GNU General Public License version 3 as published
> by the Free Software Foundation.
> This program is distributed in the hope that it will be useful,
> but WITHOUT ANY WARRANTY; without even the implied warranty of
> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> GNU General Public License for more details.
> You should have received a copy of the GNU General Public License along
> with this program. If not, see &lt;<http://www.gnu.org/licenses/>&gt;.
[Quasseldroid]: https://quasseldroid.info/
[generate a keypair]: http://developer.android.com/tools/publishing/app-signing.html
[Quasseldroid Legacy]: https://github.com/sandsmark/quasseldroid
[Quassel]: https://quassel-irc.org/
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Mareike Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
plugins {
id("justjanne.android.signing")
id("justjanne.android.app")
}
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner = "de.kuschku.quasseldroid.util.TestRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
multiDexEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
}
getByName("debug") {
applicationIdSuffix = ".debug"
multiDexEnabled = true
}
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.get()
}
}
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.appcompat.resources)
implementation(libs.androidx.activity)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.compiler)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.navigation.compose)
implementation(libs.libquassel.client)
implementation(libs.compose.htmltext)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.ui.preview)
testImplementation(libs.androidx.compose.ui.test)
}
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/lib/android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# The project is GPL anyway, obfuscation is useless.
-dontobfuscate
# Keep our invokers
-keep class * implements de.kuschku.libquassel.quassel.syncables.interfaces.invokers.Invoker {
static ** INSTANCE;
}
-keep class * implements de.kuschku.libquassel.quassel.syncables.interfaces.invokers.InvokerRegistry {
static ** INSTANCE;
}
# remove unnecessary warnings
# Android HTTP Libs
-dontnote android.net.http.**
-dontnote org.apache.http.**
# Kotlin stuff
-dontnote kotlin.**
# Gson
-dontnote com.google.gson.**
# Dagger
-dontwarn com.google.errorprone.annotations.*
# Retrofit
-dontwarn retrofit2.**
# Annotation used by Retrofit on Java 8 VMs
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-dontwarn javax.annotation.concurrent.GuardedBy
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
# Okio
-dontwarn okio.**
-dontwarn org.conscrypt.**
# OkHttp3
-dontwarn okhttp3.**
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.justjanne.quasseldroid">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="irc" />
<data android:scheme="ircs" />
</intent-filter>
</activity>
<service
android:name=".service.QuasselService"
android:exported="false" />
</application>
</manifest>
package de.justjanne.quasseldroid
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import de.justjanne.quasseldroid.service.QuasselBackend
import de.justjanne.quasseldroid.ui.theme.QuasseldroidTheme
class MainActivity : ComponentActivity() {
private val backend = QuasselBackend()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
backend.onCreate(this)
setContent {
QuasseldroidTheme {
Surface(modifier = Modifier.fillMaxSize()) {
QuasseldroidRouter(backend = backend)
}
}
}
}
override fun onStart() {
super.onStart()
backend.onStart(this)
}
override fun onResume() {
super.onResume()
backend.onResume(this)
}
override fun onStop() {
super.onStop()
backend.onStop(this)
}
override fun onDestroy() {
super.onDestroy()
backend.onDestroy(this)
}
}
package de.justjanne.quasseldroid
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import de.justjanne.quasseldroid.service.QuasselBackend
import de.justjanne.quasseldroid.ui.CoreInfoRoute
import de.justjanne.quasseldroid.ui.HomeView
import de.justjanne.quasseldroid.ui.LoginRoute
@Composable
fun QuasseldroidRouter(backend: QuasselBackend) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "login") {
composable("login") { LoginRoute(backend, navController) }
composable("home") { HomeView(backend, navController) }
composable("coreInfo") { CoreInfoRoute(backend, navController) }
}
}
package de.justjanne.quasseldroid.sample
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import de.justjanne.libquassel.protocol.features.FeatureSet
import de.justjanne.libquassel.protocol.models.ConnectedClient
import org.threeten.bp.Instant
class SampleConnectedClientProvider : PreviewParameterProvider<ConnectedClient> {
override val values = sequenceOf(
ConnectedClient(
id = 5,
remoteAddress = "192.168.178.1",
location = "Kiel, Germany",
version = "Quasseldroid 1.5.3-gafff49c2",
versionDate = Instant.ofEpochSecond(1645656060L),
connectedSince = Instant.ofEpochSecond(1645656060L),
secure = true,
features = FeatureSet.all()
),
ConnectedClient(
id = 2,
remoteAddress = "2a01:c22:bd32:4000:c7f:2640:7fcd:ae9c",
location = "Kiel, Germany",
version = "Quasseldroid 1.5.3-gafff49c2",
versionDate = Instant.ofEpochSecond(1645656060L),
connectedSince = Instant.ofEpochSecond(1645656060L),
secure = false,
features = FeatureSet.all()
)
)
}
package de.justjanne.quasseldroid.sample
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import de.justjanne.libquassel.protocol.features.FeatureSet
import de.justjanne.libquassel.protocol.models.ConnectedClient
import de.justjanne.libquassel.protocol.syncables.state.CoreInfoState
import org.threeten.bp.Instant
class SampleCoreInfoProvider : PreviewParameterProvider<CoreInfoState> {
override val values = sequenceOf(
CoreInfoState(
version = "v0.14.0 (git-<a href=\"https://github.com/quassel/quassel/commit/da9c1c9fcf25f9dbd9acb96e6c8d1ff148e55986\">da9c1c9f</a>)",
versionDate = Instant.ofEpochSecond(1645656060L),
startTime = Instant.ofEpochSecond(1645656060L),
connectedClientCount = 2,
connectedClients = listOf(
ConnectedClient(
id = 5,
remoteAddress = "192.168.178.1",
location = "Kiel, Germany",
version = "Quasseldroid <a href=\"https://git.kuschku.de/justJanne/QuasselDroid-ng/-/commit/afff49c2ae4be7717fa75f8c466d4f84b13641b5\">1.5.3-gafff49c2</a>",
versionDate = Instant.ofEpochSecond(1645656060L),
connectedSince = Instant.ofEpochSecond(1645656060L),
secure = true,
features = FeatureSet.all()
),
ConnectedClient(
id = 2,
remoteAddress = "2a01:c22:bd32:4000:c7f:2640:7fcd:ae9c",
location = "Kiel, Germany",
version = "Quasseldroid <a href=\"https://git.kuschku.de/justJanne/QuasselDroid-ng/-/commit/afff49c2ae4be7717fa75f8c466d4f84b13641b5\">1.5.3-gafff49c2</a>",
versionDate = Instant.ofEpochSecond(1645656060L),
connectedSince = Instant.ofEpochSecond(1645656060L),
secure = false,
features = FeatureSet.all()
)
)
)
)
}
package de.justjanne.quasseldroid.service
import java.net.InetSocketAddress
data class ConnectionData(
val address: InetSocketAddress,
val username: String,
val password: String
)
package de.justjanne.quasseldroid.service
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import de.justjanne.libquassel.client.session.ClientSession
import de.justjanne.libquassel.protocol.util.StateHolder
import de.justjanne.libquassel.protocol.util.flatMap
import de.justjanne.quasseldroid.BuildConfig
import de.justjanne.quasseldroid.service.QuasselService.Companion.quasselService
import de.justjanne.quasseldroid.util.lifecycle.DefaultContextualLifecycleObserver
import de.justjanne.quasseldroid.util.lifecycle.LifecycleStatus
import kotlinx.coroutines.flow.MutableStateFlow
class QuasselBackend : DefaultContextualLifecycleObserver(), ServiceConnection,
StateHolder<ClientSession?> {
private var connectionData: ConnectionData? = null
override fun flow() = state.flatMap()
override fun state() = state.value?.state()
private val state = MutableStateFlow<QuasselBinder?>(null)
override fun onCreate(owner: Context) {
super.onCreate(owner)
connectionData?.let { (address, username, password) ->
Log.d("QuasselBackend", "Starting Quassel Service")
owner.startService(owner.quasselService(address, Pair(username, password)))
}
}
override fun onStart(owner: Context) {
super.onStart(owner)
connectionData?.let { (address, username, password) ->
Log.d("QuasselBackend", "Binding Quassel Service")
owner.bindService(owner.quasselService(address, Pair(username, password)), this, 0)
}
}
override fun onStop(owner: Context) {
super.onStop(owner)
Log.d("QuasselBackend", "Unbinding Quassel Service")
owner.unbindService(this)
}
fun login(context: Context, connectionData: ConnectionData): Boolean {
this.connectionData = connectionData
when (status) {
LifecycleStatus.CREATED -> {
val (address, username, password) = connectionData
Log.d("QuasselBackend", "Starting Quassel Service")
context.startService(
context.quasselService(address, Pair(username, password))
)
return true
}
LifecycleStatus.STARTED, LifecycleStatus.RESUMED -> {
val (address, username, password) = connectionData
Log.d("QuasselBackend", "Binding Quassel Service")
context.startService(
context.quasselService(address, Pair(username, password))
)
context.bindService(
context.quasselService(address, Pair(username, password)),
this,
0
)
return true
}
else -> {
Log.w("QuasselBackend", "Trying to log in but status is $status")
return false
}
}
}
fun disconnect(context: Context) {
context.stopService(context.quasselService())
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
if (name == quasselService && service is QuasselBinder) {
Log.d("QuasselBackend", "Quassel Service bound")
state.value = service
} else {
Log.w("QuasselBackend", "Unknown Service bound: $name")
}
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d("QuasselBackend", "Service unbound: $name")
state.value = null
}
companion object {
private val quasselService = ComponentName(
BuildConfig.APPLICATION_ID,
QuasselService::class.java.canonicalName!!
)
}
}
package de.justjanne.quasseldroid.service
import android.os.Binder
import de.justjanne.libquassel.client.session.ClientSession
import de.justjanne.libquassel.protocol.util.StateHolder
import kotlinx.coroutines.flow.StateFlow
class QuasselBinder(
private val state: StateFlow<ClientSession?>
) : Binder(), StateHolder<ClientSession?> {
constructor(runner: QuasselRunner) : this(runner.flow())
override fun flow() = state
override fun state() = state.value
}
package de.justjanne.quasseldroid.service
import android.util.Log
import de.justjanne.libquassel.client.session.ClientSession
import de.justjanne.libquassel.protocol.connection.ProtocolFeature
import de.justjanne.libquassel.protocol.connection.ProtocolMeta
import de.justjanne.libquassel.protocol.connection.ProtocolVersion
import de.justjanne.libquassel.protocol.features.FeatureSet
import de.justjanne.libquassel.protocol.io.CoroutineChannel
import de.justjanne.libquassel.protocol.util.StateHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.io.Closeable
import java.net.InetSocketAddress
import javax.net.ssl.SSLContext
class QuasselRunner(
private val address: InetSocketAddress,
private val auth: Pair<String, String>
) : Thread("Quassel Runner"), Closeable, StateHolder<ClientSession?> {
private val channel = CoroutineChannel()
override fun state(): ClientSession? = state.value
override fun flow(): StateFlow<ClientSession?> = state
private val state = MutableStateFlow<ClientSession?>(null)
init {
start()
}
override fun run() {
runBlocking(Dispatchers.IO) {
Log.d("QuasselRunner", "Resolving URL")
val address = InetSocketAddress(address.hostString, address.port)
Log.d("QuasselRunner", "Connecting")
channel.connect(address)
Log.d("QuasselRunner", "Handshake")
val session = ClientSession(
channel,
ProtocolFeature.all,
listOf(
ProtocolMeta(
ProtocolVersion.Datastream,
0x0000u
)
),
SSLContext.getDefault()
).also { state.value = it }
session.handshakeHandler.init(
"Quasseltest v0.1",
"2022-02-24",
FeatureSet.all()
)
val (username, password) = auth
Log.d("QuasselRunner", "Authenticating")
session.handshakeHandler.login(username, password)
Log.d("QuasselRunner", "Waiting for init")
session.baseInitHandler.waitForInitDone()
Log.d("QuasselRunner", "Init Done")
}
}
override fun close() {
Log.d("QuasselRunner", "Stopping Quassel Runner")
runBlocking(Dispatchers.IO) {
withTimeout(2000L) {
channel.close()
}
}
}
}
package de.justjanne.quasseldroid.service
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.util.Log
import java.net.InetSocketAddress
class QuasselService : Service() {
private var runner: QuasselRunner? = null
private fun newRunner(intent: Intent): QuasselRunner {
Log.w("QuasselService", "Creating new quassel runner")
val address = InetSocketAddress.createUnresolved(
requireNotNull(intent.getStringExtra("host")) {
"Required argument 'host' missing"
},
intent.getIntExtra("port", 4242),
)
val auth = Pair(
requireNotNull(intent.getStringExtra("username")) {
"Required argument 'username' missing"
},
requireNotNull(intent.getStringExtra("password")) {
"Required argument 'password' missing"
},
)
return QuasselRunner(address, auth)
}
override fun onCreate() {
Log.d("QuasselService", "Service created")
}
override fun onDestroy() {
runner?.close()
super.onDestroy()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("QuasselService", "Start Command received")
super.onStartCommand(intent, flags, startId)
if (intent != null && this.runner == null) {
this.runner = newRunner(intent)
} else if (this.runner == null) {
Log.e("QuasselService", "Could not start runner, intent missing")
}
return START_STICKY
}
override fun onBind(intent: Intent): IBinder {
Log.d("QuasselService", "Binding")
return QuasselBinder(this.runner ?: newRunner(intent).also { runner = it })
}
companion object {
fun Context.quasselService(
address: InetSocketAddress,
auth: Pair<String, String>
) = quasselService().apply {
putExtra("host", address.hostString)
putExtra("port", address.port)
val (username, password) = auth
putExtra("username", username)
putExtra("password", password)
}
fun Context.quasselService() = Intent(applicationContext, QuasselService::class.java)
}
}
package de.justjanne.quasseldroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import de.charlex.compose.HtmlText
import de.justjanne.libquassel.protocol.models.ConnectedClient
import de.justjanne.quasseldroid.R
import de.justjanne.quasseldroid.sample.SampleConnectedClientProvider
import de.justjanne.quasseldroid.ui.theme.Insecure
import de.justjanne.quasseldroid.ui.theme.Secure
import de.justjanne.quasseldroid.ui.theme.Typography
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.FormatStyle
private val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
@Preview(name = "Connected Client")
@Composable
fun ConnectedClientCard(
@PreviewParameter(SampleConnectedClientProvider::class)
client: ConnectedClient,
modifier: Modifier = Modifier
) {
val secureResource = painterResource(
if (client.secure) R.drawable.ic_lock
else R.drawable.ic_no_encryption
)
val tint = if (client.secure) Secure else Insecure
Card(modifier = modifier) {
Row(modifier = Modifier.padding(16.dp)) {
Column(modifier = Modifier.weight(1.0f)) {
HtmlText(
text = client.version,
style = Typography.body1
)
Text(
client.remoteAddress,
style = Typography.body2
)
Text(
client.connectedSince
.atZone(ZoneId.systemDefault())
.format(formatter),
style = Typography.body2
)
}
Spacer(modifier = Modifier.width(16.dp))
Icon(secureResource, tint = tint, contentDescription = "")
}
}
}
package de.justjanne.quasseldroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import de.justjanne.libquassel.protocol.util.flatMap
import de.justjanne.quasseldroid.service.QuasselBackend
import de.justjanne.quasseldroid.util.mapNullable
import de.justjanne.quasseldroid.util.rememberFlow
@Composable
fun CoreInfoRoute(backend: QuasselBackend, navController: NavController) {
val coreInfo = rememberFlow(null) {
backend.flow()
.flatMap()
.mapNullable { it.coreInfo }
.flatMap()
}
Column(Modifier.padding(16.dp)) {
Button(onClick = { navController.navigate("home") }) {
Text("Back")
}
if (coreInfo == null) {
Text("No data available")
} else {
CoreInfoView(coreInfo)
}
}
}
package de.justjanne.quasseldroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.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 androidx.compose.ui.unit.dp
import de.charlex.compose.HtmlText
import de.justjanne.libquassel.protocol.models.ConnectedClient
import de.justjanne.libquassel.protocol.syncables.state.CoreInfoState
import de.justjanne.quasseldroid.sample.SampleCoreInfoProvider
import de.justjanne.quasseldroid.ui.theme.Typography
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.FormatStyle
private val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
@Preview(name = "Core Info", showBackground = true)
@Composable
fun CoreInfoView(
@PreviewParameter(SampleCoreInfoProvider::class)
coreInfo: CoreInfoState
) {
Column(modifier = Modifier.padding(8.dp)) {
Column(modifier = Modifier.padding(8.dp)) {
HtmlText(
text = coreInfo.version,
style = Typography.body1
)
Text(
text = coreInfo.versionDate
?.atZone(ZoneId.systemDefault())
?.format(formatter)
?: "Unknown",
style = Typography.body2
)
Text(
coreInfo.startTime
.atZone(ZoneId.systemDefault())
.format(formatter),
style = Typography.body2
)
}
LazyColumn {
items(coreInfo.connectedClients, key = ConnectedClient::id) {
ConnectedClientCard(
it,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment