This commit is contained in:
aminecmi
2022-03-22 15:35:23 +01:00
parent 77a87cc58d
commit c1040ab4d5
34 changed files with 1606 additions and 0 deletions

77
shared/build.gradle.kts Normal file
View File

@ -0,0 +1,77 @@
plugins {
kotlin("multiplatform")
id("com.android.library")
kotlin("plugin.serialization") version "1.4.10"
}
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
//iosSimulatorArm64() sure all ios dependencies support this target
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:1.6.7")
implementation("io.ktor:ktor-client-serialization:1.6.7")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jsoup:jsoup:1.14.3")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:1.6.7")
}
}
val androidTest by getting {
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2")
}
}
val iosX64Main by getting
val iosArm64Main by getting
//val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
//iosSimulatorArm64Main.dependsOn(this)
}
val iosX64Test by getting
val iosArm64Test by getting
//val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
dependencies {
implementation("io.ktor:ktor-client-ios:1.6.7")
}
//iosSimulatorArm64Test.dependsOn(this)
}
}
}
android {
compileSdk = 31
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
targetSdk = 31
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="bou.amine.apps.readerforselfossv2" />

View File

@ -0,0 +1,5 @@
package bou.amine.apps.readerforselfossv2
actual class Platform actual constructor() {
actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

View File

@ -0,0 +1,12 @@
package bou.amine.apps.readerforselfossv2
import org.junit.Assert.assertTrue
import org.junit.Test
class AndroidGreetingTest {
@Test
fun testExample() {
assertTrue("Check Android is mentioned", Greeting().greeting().contains("Android"))
}
}

View File

@ -0,0 +1,7 @@
package bou.amine.apps.readerforselfossv2
class Greeting {
fun greeting(): String {
return "Hello, ${Platform().platform}!"
}
}

View File

@ -0,0 +1,5 @@
package bou.amine.apps.readerforselfossv2
expect class Platform() {
val platform: String
}

View File

@ -0,0 +1,188 @@
package bou.amine.apps.readerforselfossv2.rest
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.text.Html
import bou.amine.apps.readerforselfossv2.rest.SelfossModel.SelfossModel.constructUrl
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmField
class SelfossApi {
/**
* TODO:
* Self signed certs
* Timeout + 408
* Auth digest/basic
* Loging
*/
val baseUrl = "http://10.0.2.2:8888"
val userName = ""
val password = ""
val client = HttpClient() {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
private fun url(path: String) =
"$baseUrl$path"
suspend fun login() =
client.get<String>(url("/login"))// Todo: params
suspend fun getItems(type: String,
items: Int,
offset: Int,
tag: String? = "",
source: Long? = null,
search: String? = "",
updatedSince: String? = ""): List<SelfossModel.Item> =
client.get(url("/items")) {
parameter("username", userName)
parameter("password", password)
parameter("type", type)
parameter("tag", tag)
parameter("source", source)
parameter("search", search)
parameter("updatedsince", updatedSince)
parameter("items", items)
parameter("offset", offset)
}
suspend fun stats(): SelfossModel.Stats =
client.get(url("/stats")) {
parameter("username", userName)
parameter("password", password)
}
suspend fun tags(): List<SelfossModel.Tag> =
client.get(url("/tags")) {
parameter("username", userName)
parameter("password", password)
}
suspend fun update(): String =
client.get(url("/update")) {
parameter("username", userName)
parameter("password", password)
}
suspend fun spouts(): Map<String, SelfossModel.Spout> =
client.get(url("/sources/spouts")) {
parameter("username", userName)
parameter("password", password)
}
suspend fun sources(): List<SelfossModel.Source> =
client.get(url("/sources/list")) {
parameter("username", userName)
parameter("password", password)
}
suspend fun version(): SelfossModel.ApiVersion =
client.get(url("/api/about"))
suspend fun markAsRead(id: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/mark/$id"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
},
encodeInQuery = true
)
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/unmark/$id"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
},
encodeInQuery = true
)
suspend fun starr(id: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/starr/$id"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
},
encodeInQuery = true
)
suspend fun unstarr(id: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/unstarr/$id"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
},
encodeInQuery = true
)
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/mark"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
append("ids[]", ids.joinToString(","))
},
encodeInQuery = true
)
suspend fun createSource(title: String, url: String, spout: String, tags: String, filter: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/source"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
append("title", title)
append("url", url)
append("spout", spout)
append("tags", tags)
append("filter", filter)
},
encodeInQuery = true
)
suspend fun createSource2(title: String, url: String, spout: String, tags: String, filter: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/source"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
append("title", title)
append("url", url)
append("spout", spout)
append("tags[]", tags)
append("filter", filter)
},
encodeInQuery = true
)
suspend fun deleteSource(id: String) =
client.delete<SelfossModel.SuccessResponse>(url("/source/$id")) {
parameter("username", userName)
parameter("password", password)
}
}

View File

@ -0,0 +1,168 @@
package bou.amine.apps.readerforselfossv2.rest
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.text.Html
import kotlinx.serialization.Serializable
import java.util.*
import kotlin.collections.ArrayList
import kotlin.jvm.JvmField
import org.jsoup.Jsoup
import java.util.Locale.US
class SelfossModel {
@Serializable
data class Tag(
val tag: String,
val color: String,
val unread: Int
) {
fun getTitleDecoded(): String {
return Html.fromHtml(tag).toString()
}
}
@Serializable
class SuccessResponse(val success: Boolean) {
val isSuccess: Boolean
get() = success
}
@Serializable
class Stats(
val total: Int,
val unread: Int,
val starred: Int
)
@Serializable
data class Spout(
val name: String,
val description: String
)
@Serializable
data class ApiVersion(
val version: String?,
val apiversion: String?
) {
fun getApiMajorVersion() : Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
}
@Serializable
data class Source(
val id: String,
val title: String,
val tags: String,
val spout: String,
val error: String,
val icon: String
) {
fun getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
}
@Serializable
data class Item(
val id: String,
val datetime: String,
val title: String,
val content: String,
var unread: Int,
var starred: Int,
val thumbnail: String?,
val icon: String?,
val link: String,
val sourcetitle: String,
val tags: String
) {
fun getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun getThumbnail(baseUrl: String): String {
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
fun getImages() : ArrayList<String> {
val allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(US).contains(".jpg") ||
url.lowercase(US).contains(".jpeg") ||
url.lowercase(US).contains(".png") ||
url.lowercase(US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
fun getSourceTitle(): String {
return Html.fromHtml(sourcetitle).toString()
}
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
}
companion object SelfossModel {
private fun String?.isEmptyOrNullOrNullString(): Boolean =
this == null || this == "null" || this.isEmpty()
fun constructUrl(baseUrl: String, path: String, file: String?): String {
return if (file.isEmptyOrNullOrNullString()) {
""
} else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
}
}
}
}

View File

@ -0,0 +1,12 @@
package bou.amine.apps.readerforselfossv2
import kotlin.test.Test
import kotlin.test.assertTrue
class CommonGreetingTest {
@Test
fun testExample() {
assertTrue(Greeting().greeting().contains("Hello"), "Check 'Hello' is mentioned")
}
}

View File

@ -0,0 +1,7 @@
package bou.amine.apps.readerforselfossv2
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

View File

@ -0,0 +1,12 @@
package bou.amine.apps.readerforselfossv2
import kotlin.test.Test
import kotlin.test.assertTrue
class IosGreetingTest {
@Test
fun testExample() {
assertTrue(Greeting().greeting().contains("iOS"), "Check iOS is mentioned")
}
}