Reorganize and group modules

This commit is contained in:
MM20
2022-12-13 17:37:26 +01:00
parent bac24baad2
commit 3f8880a90a
995 changed files with 501 additions and 298 deletions
+1
View File
@@ -0,0 +1 @@
/build
+56
View File
@@ -0,0 +1,56 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.websites"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.browser)
implementation(libs.androidx.palette)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.okhttp)
implementation(libs.jsoup)
implementation(libs.koin.android)
implementation(libs.coil.core)
implementation(project(":core:base"))
implementation(project(":core:ktx"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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
@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
/
</manifest>
@@ -0,0 +1,96 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.content.ContextCompat
import coil.imageLoader
import coil.request.ImageRequest
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.websites.R
import java.util.concurrent.ExecutionException
data class Website(
override val label: String,
val url: String,
val description: String,
val image: String,
val favicon: String,
val color: Int,
override val labelOverride: String? = null,
) : SavableSearchable {
override val domain: String = Domain
override val key = "$domain://$url"
override val preferDetailsOverLaunch: Boolean = false
override fun overrideLabel(label: String): Website {
return this.copy(labelOverride = label)
}
override suspend fun loadIcon(
context: Context,
size: Int,
themed: Boolean,
): LauncherIcon? {
if (favicon.isEmpty()) return null
try {
val request = ImageRequest.Builder(context)
.data(favicon)
.size(size)
.allowHardware(false)
.build()
val icon = context.imageLoader.execute(request).drawable ?: return null
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = icon,
scale = 1f,
),
backgroundLayer = TransparentLayer
)
} catch (e: ExecutionException) {
return null
}
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val color = if (color != 0) color else 0xFFF76F8E.toInt()
if (label.isNotBlank()) {
return StaticLauncherIcon(
foregroundLayer = TextLayer(text = label[0].toString(), color = color),
backgroundLayer = ColorLayer(color)
)
}
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_website)!!,
scale = 0.5f,
color = color,
),
backgroundLayer = ColorLayer(color)
)
}
private fun getLaunchIntent(): Intent {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
companion object {
const val Domain = "web"
}
}
@@ -0,0 +1,8 @@
package de.mm20.launcher2.websites
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val websitesModule = module {
single<WebsiteRepository> { WebsiteRepositoryImpl(androidContext()) }
}
@@ -0,0 +1,114 @@
package de.mm20.launcher2.websites
import android.content.Context
import android.webkit.URLUtil
import androidx.core.graphics.toColorInt
import de.mm20.launcher2.search.data.Website
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.UncheckedIOException
import org.koin.core.component.KoinComponent
import java.io.IOException
import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
import java.util.concurrent.TimeUnit
interface WebsiteRepository {
fun search(query: String): Flow<Website?>
}
internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, KoinComponent {
private val httpClient = OkHttpClient
.Builder()
.connectTimeout(200, TimeUnit.MILLISECONDS)
.readTimeout(3000, TimeUnit.MILLISECONDS)
.writeTimeout(1000, TimeUnit.MILLISECONDS)
.build()
override fun search(query: String): Flow<Website?> = channelFlow {
send(null)
withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll()
}
if (query.isBlank()) return@channelFlow
val website = queryWebsite(query)
send(website)
}
private suspend fun queryWebsite(query: String): Website? {
val result = withContext(Dispatchers.IO) {
var url = query
val protocol = "https://"
if (!query.startsWith("https://") && !query.startsWith("http://")) url =
"$protocol$query"
if (!URLUtil.isValidUrl(url)) return@withContext null
try {
val request = Request.Builder()
.url(URL(url))
.get()
.tag("onlinesearch")
.build()
val response = httpClient.newCall(request).execute()
url = response.request.url.toString()
val body = response.body?.string() ?: return@withContext null
val doc = Jsoup.parse(body)
var title = doc.select("meta[property=og:title]").attr("content")
if (title.isBlank()) title = doc.title()
if (title.isBlank()) title = url
var description = doc.select("meta[property=og:description]").attr("content")
if (description.isBlank()) description =
doc.select("meta[name=description]").attr("content")
val color = try {
val colorString = doc.select("meta[name=theme-color]").attr("content")
if (colorString.isNotEmpty()) colorString.toColorInt()
else 0
} catch (e: IllegalArgumentException) {
0
}
var image = doc.select("meta[property=og:image]").attr("content")
var favicon = doc.select("link[rel=apple-touch-icon]").attr("href")
if (favicon.isBlank()) favicon =
doc.head().select("meta[itemprop=image]").attr("content")
if (favicon.isBlank()) favicon = doc.select("link[rel=icon]").attr("href")
if (favicon.isBlank()) favicon =
doc.head().select("link[href~=.*\\.(ico|png)]").attr("href")
if (favicon.isNotBlank()) favicon = resolveUrl(response.request.url, favicon)
if (image.isNotBlank()) image = resolveUrl(response.request.url, image)
return@withContext Website(
label = title,
url = url,
description = description,
image = image,
favicon = favicon,
color = color
)
} catch (e: IOException) {
//Ignore. Not a HTML page or no connection. No result for this query
} catch (e: UncheckedIOException) {
} catch (e: URISyntaxException) {
} catch (e: RuntimeException) {
} catch (e: IllegalArgumentException) {
}
return@withContext null
}
return result
}
private fun resolveUrl(url: HttpUrl, link: String): String {
return try {
URL(url.toUrl(), link).toString()
} catch (e: MalformedURLException) {
""
}
}
}
@@ -0,0 +1,39 @@
package de.mm20.launcher2.websites
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Website
import org.json.JSONObject
class WebsiteSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as Website
return jsonObjectOf(
"label" to searchable.label,
"url" to searchable.url,
"description" to searchable.description,
"image" to searchable.image,
"favicon" to searchable.favicon,
"color" to searchable.color
).toString()
}
override val typePrefix: String
get() = "website"
}
class WebsiteDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable? {
val json = JSONObject(serialized)
return Website(
label = json.getString("label"),
favicon = json.getString("favicon"),
image = json.getString("image"),
description = json.getString("description"),
url = json.getString("url"),
color = json.getInt("color")
)
}
}
@@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"/>
</vector>