diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..0aabf4f
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+ToS;DR
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..c0c3586
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/discord.xml b/.idea/discord.xml
new file mode 100644
index 0000000..d8e9561
--- /dev/null
+++ b/.idea/discord.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..a2d7c21
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..8509a5c
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..e1eea1d
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..6c63d20
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..80808db
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Terms of Service; Didn’t Read
+
+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.
\ No newline at end of file
diff --git a/README.MD b/README.MD
new file mode 100644
index 0000000..6ff59a8
--- /dev/null
+++ b/README.MD
@@ -0,0 +1,30 @@
+# ToS;DR for Android
+
+[](https://play.google.com/store/apps/details?id=xyz.ptgms.tosdr&pli=1)
+
+This is a simple app that allows you to use ToS;DR on your Android device.
+
+## How to use
+
+1. Install the app from the [Play Store](https://play.google.com/store/apps/details?id=xyz.ptgms.tosdr&pli=1), download the APK from the [releases page](https://github.com/tosdr/tosdr-android/releases), or build it yourself.
+2. Share a link of a Website to the App or search for it in the search tab.
+3. Done! You are not viewing the ToS;DR page for the website!
+
+## How to build
+
+1. Clone the repository
+2. Open the project in Android Studio
+3. Build the project
+4. Done!
+
+## How to contribute
+
+Feel free to open a pull request or an issue if you want to contribute to the project. If you want to add a new language, you can use the [CrowdIn](https://crowdin.com/project/tosdr-android).
+
+## Is the app going to be on F-Droid?
+
+Yes, it will. I am going to make a new branch without any Google dependencies and then I will submit it to F-Droid.
+
+## Notice
+
+Google Play and the Google Play logo are trademarks of Google LLC.
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..6a99982
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,81 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'com.mikepenz.aboutlibraries.plugin'
+}
+
+android {
+ namespace 'xyz.ptgms.tosdr'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId 'xyz.ptgms.tosdr'
+ minSdk 24
+ targetSdk 33
+ versionCode 21
+ versionName "1.5"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.3.2'
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ implementation "com.mikepenz:aboutlibraries-core:10.5.1"
+ implementation 'com.android.billingclient:billing:5.1.0'
+ implementation("com.android.billingclient:billing-ktx:5.1.0")
+ implementation 'androidx.core:core-ktx:1.9.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
+ implementation 'androidx.activity:activity-compose:1.6.1'
+ implementation platform('androidx.compose:compose-bom:2022.11.00')
+ implementation 'androidx.compose.ui:ui:1.4.0-alpha02'
+ implementation 'androidx.compose.ui:ui-graphics:1.4.0-alpha02'
+ implementation 'androidx.compose.ui:ui-tooling-preview:1.4.0-alpha02'
+ implementation 'androidx.compose.material3:material3:1.1.0-alpha02'
+ implementation "io.coil-kt:coil-compose:2.2.2"
+ implementation 'com.beust:klaxon:5.6'
+ implementation "com.deepl.api:deepl-java:0.2.1"
+ implementation('ch.acra:acra-mail:5.9.7')
+ implementation('ch.acra:acra-dialog:5.9.7')
+ implementation('androidx.navigation:navigation-compose:2.6.0-alpha04')
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.4'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
+ androidTestImplementation platform('androidx.compose:compose-bom:2022.11.00')
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.4.0-alpha02'
+ debugImplementation 'androidx.compose.ui:ui-tooling:1.4.0-alpha02'
+ debugImplementation 'androidx.compose.ui:ui-test-manifest:1.4.0-alpha02'
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..39b24a3
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,37 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# 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
+
+# Needed for OKHttp
+-dontwarn org.bouncycastle.jsse.BCSSLParameters
+-dontwarn org.bouncycastle.jsse.BCSSLSocket
+-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
+-dontwarn org.conscrypt.Conscrypt$Version
+-dontwarn org.conscrypt.Conscrypt
+-dontwarn org.conscrypt.ConscryptHostnameVerifier
+-dontwarn org.openjsse.javax.net.ssl.SSLParameters
+-dontwarn org.openjsse.javax.net.ssl.SSLSocket
+-dontwarn org.openjsse.net.ssl.OpenJSSE
+# Klaxon
+-keep public class kotlin.reflect.jvm.internal.impl.** { public *; }
+-keep class com.beust.klaxon.** { *; }
+-keep interface com.beust.klaxon.** { *; }
+-keep class kotlin.Metadata { *; }
\ No newline at end of file
diff --git a/app/release/app-release.aab b/app/release/app-release.aab
new file mode 100644
index 0000000..3b545d3
Binary files /dev/null and b/app/release/app-release.aab differ
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
new file mode 100644
index 0000000..636b5a5
Binary files /dev/null and b/app/release/app-release.apk differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
new file mode 100644
index 0000000..287f7d4
--- /dev/null
+++ b/app/release/output-metadata.json
@@ -0,0 +1,20 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "xyz.ptgms.tosdr",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 16,
+ "versionName": "1.2",
+ "outputFile": "app-release.apk"
+ }
+ ],
+ "elementType": "File"
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3914229
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..2a66f9a
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/xyz/ptgms/tosdr/Application.kt b/app/src/main/java/xyz/ptgms/tosdr/Application.kt
new file mode 100644
index 0000000..1efb847
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/Application.kt
@@ -0,0 +1,37 @@
+package xyz.ptgms.tosdr
+
+import android.app.Application
+import android.content.Context
+import android.util.Log
+import com.android.billingclient.BuildConfig
+import org.acra.config.dialog
+import org.acra.config.mailSender
+import org.acra.data.StringFormat
+import org.acra.ktx.initAcra
+
+class Application : Application() {
+ override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+
+ Log.i("ACRA Init", "Initialised!")
+
+ initAcra {
+ //core configuration:
+ buildConfigClass = BuildConfig::class.java
+ reportFormat = StringFormat.JSON
+
+ dialog {
+ text = getString(R.string.crash_text)
+ title = getString(R.string.crash_title)
+ commentPrompt = getString(R.string.crash_comment)
+ resTheme = android.R.style.Theme_Material_Dialog
+ resIcon = android.R.drawable.stat_sys_warning
+ }
+
+ mailSender {
+ mailTo = "me@ptgms.space"
+ reportFileName = "Crash.txt"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/MainActivity.kt b/app/src/main/java/xyz/ptgms/tosdr/MainActivity.kt
new file mode 100644
index 0000000..df75660
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/MainActivity.kt
@@ -0,0 +1,389 @@
+package xyz.ptgms.tosdr
+
+import android.app.Activity
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Face
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Divider
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalDrawerSheet
+import androidx.compose.material3.ModalNavigationDrawer
+import androidx.compose.material3.NavigationDrawerItem
+import androidx.compose.material3.NavigationDrawerItemDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowCompat
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navDeepLink
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ConsumeParams
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryProductDetailsParams
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.tools.data.Billing
+import xyz.ptgms.tosdr.tools.data.NavigationItem
+import xyz.ptgms.tosdr.ui.theme.ToSDRTheme
+import xyz.ptgms.tosdr.ui.views.ToSDRAboutView
+import xyz.ptgms.tosdr.ui.views.ToSDRDetailView.ToSDRDetailView
+import xyz.ptgms.tosdr.ui.views.ToSDRDonateView
+import xyz.ptgms.tosdr.ui.views.ToSDRHomeView
+import xyz.ptgms.tosdr.ui.views.ToSDRLicensesView
+import xyz.ptgms.tosdr.ui.views.ToSDRSearchView
+import xyz.ptgms.tosdr.ui.views.ToSDRSettingsView
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Allow app to be drawn behind the status bar
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ billingSetup()
+
+ Log.i("Main Activity", "Welcome! Running with locale ${Locale.current.language}")
+
+ setContent {
+ ToSDRTheme {
+ DrawerLayout(billingClient)
+ }
+ }
+ }
+
+ private lateinit var billingClient: BillingClient
+
+ private fun billingSetup() {
+ Log.d("Billing", "Setting up billing client")
+ billingClient = BillingClient.newBuilder(this)
+ .enablePendingPurchases()
+ .setListener(purchasesUpdatedListener)
+ .build()
+
+ billingClient.startConnection(object : BillingClientStateListener {
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+ Log.d("Billing", "Billing setup finished")
+ queryProductDetails()
+ } else {
+ Log.e("Billing", "Billing setup failed")
+ Log.e("Billing", billingResult.debugMessage)
+ }
+ }
+
+ override fun onBillingServiceDisconnected() {
+ Log.d("Billing", "Billing service disconnected")
+
+ billingClient.startConnection(this)
+ }
+ })
+ }
+
+ private fun queryProductDetails() {
+ val skuList = listOf("1euro_donation", "5euro_donation", "10euro_donation")
+ skuList.forEach {
+ val query = QueryProductDetailsParams.newBuilder()
+ .setProductList(
+ listOf(
+ QueryProductDetailsParams.Product.newBuilder()
+ .setProductId(it)
+ .setProductType(BillingClient.ProductType.INAPP)
+ .build()
+ )
+ )
+ .build()
+
+ billingClient.queryProductDetailsAsync(query) { billingResult,
+ productDetailsList ->
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
+ Log.d("Billing", "Product details query finished")
+
+ for (product in productDetailsList) {
+ Log.d("Billing", "Product: ${product.title}")
+ }
+ Billing.products.add(productDetailsList[0])
+ } else {
+ Log.d(
+ "Billing",
+ "Product details query failed with code ${billingResult.responseCode}"
+ )
+ Log.d("Billing", billingResult.debugMessage)
+ }
+ }
+ }
+ }
+
+ private val purchasesUpdatedListener =
+ PurchasesUpdatedListener { billingResult, purchases ->
+ if (billingResult.responseCode ==
+ BillingClient.BillingResponseCode.OK
+ && purchases != null
+ ) {
+ for (purchase in purchases) {
+ Toast.makeText(
+ this,
+ getString(R.string.donation_thanks),
+ Toast.LENGTH_SHORT
+ ).show()
+ // Consume the purchase
+ billingClient.consumeAsync(ConsumeParams.newBuilder()
+ .setPurchaseToken(purchase.purchaseToken).build()) { billingConsumeResult, _ ->
+ if (billingConsumeResult.responseCode ==
+ BillingClient.BillingResponseCode.OK
+ ) {
+ Log.d("Billing", "Purchase consumed")
+ } else {
+ Log.d("Billing", "Purchase not consumed")
+ }
+ }
+ }
+ } else if (billingResult.responseCode ==
+ BillingClient.BillingResponseCode.USER_CANCELED
+ ) {
+ Toast.makeText(
+ this,
+ getString(R.string.donation_cancelled),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ this,
+ getString(R.string.donation_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun DrawerLayout(billingClient: BillingClient) {
+ val navController = rememberNavController()
+ val drawerState = rememberDrawerState(DrawerValue.Closed)
+ val scope = rememberCoroutineScope()
+
+ val items: List = listOf(
+ NavigationItem(stringResource(R.string.home), Icons.Default.Home, "home"),
+ NavigationItem(stringResource(R.string.search), Icons.Default.Search, "search"),
+ //NavigationItem("Test Search", Icons.Default.Search, "details/182/E"),
+ )
+
+ val settingItems: List = listOf(
+ NavigationItem(stringResource(R.string.settings), Icons.Default.Settings, "settings"),
+ NavigationItem(stringResource(R.string.about), Icons.Default.Face, "about"),
+ NavigationItem(stringResource(R.string.donate), Icons.Default.Favorite, "donate"),
+ )
+
+ val selectedItem = remember { mutableStateOf(items[0]) }
+ var title by rememberSaveable { mutableStateOf("Home") }
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet {
+ Column(
+ modifier = Modifier
+ //.fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ // Display App icon and name
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.app_icon),
+ contentDescription = stringResource(id = R.string.app_name),
+ modifier = Modifier
+ .size(48.dp)
+ .clip(RoundedCornerShape(8.dp))
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = stringResource(id = R.string.app_name),
+ style = MaterialTheme.typography.titleLarge
+ )
+ }
+ //Spacer(Modifier.height(12.dp))
+ Divider(modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding))
+ Spacer(Modifier.height(12.dp))
+ Text(
+ text = stringResource(R.string.navigation),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item.icon, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ scope.launch { drawerState.close() }
+ selectedItem.value = item
+ navController.navigate(item.page) {
+ popUpTo(0)
+ }
+ title = item.name
+ },
+ modifier = Modifier
+ .padding(NavigationDrawerItemDefaults.ItemPadding)
+ .fillMaxWidth()
+ )
+ }
+ Spacer(Modifier.height(12.dp))
+ Divider(modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding))
+ Spacer(Modifier.height(12.dp))
+ Text(
+ text = stringResource(id = R.string.settings),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ Spacer(Modifier.height(12.dp))
+ settingItems.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item.icon, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ scope.launch { drawerState.close() }
+ selectedItem.value = item
+ navController.navigate(item.page) {
+ popUpTo(0)
+ }
+ title = item.name
+ },
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ }
+ }
+ }
+ },
+ content = {
+ MainView(
+ drawerState,
+ scope,
+ navController = navController,
+ billingClient
+ )
+ }
+ )
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun MainView(
+ drawerState: DrawerState,
+ scope: CoroutineScope,
+ navController: NavHostController,
+ billingClient: BillingClient,
+ activity: Activity = this
+ ) {
+ Scaffold { padding ->
+ Log.d("MainView", "Padding: $padding")
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ NavHost(navController = navController, startDestination = "home") {
+ composable("home") {
+ ToSDRHomeView.ToSDRHomeView(modifier = Modifier, scope, drawerState)
+ }
+ composable("search") {
+ ToSDRSearchView.ToSDRView(
+ modifier = Modifier,
+ scope = scope,
+ drawerState = drawerState,
+ navHostController = navController
+ )
+ }
+ composable("settings") {
+ ToSDRSettingsView.ToSDRSettingsView(modifier = Modifier, scope, drawerState, navController)
+ }
+ composable("about") {
+ ToSDRAboutView.ToSDRAboutView(modifier = Modifier, scope = scope, drawerState)
+ }
+ composable("licenses") {
+ ToSDRLicensesView.ToSDRLicensesView(modifier = Modifier, scope = scope, navController)
+ }
+ composable("donate") {
+ ToSDRDonateView.ToSDRDonateView(
+ modifier = Modifier,
+ billingClient = billingClient,
+ activity = activity,
+ scope = scope,
+ drawerState = drawerState
+ )
+ }
+ composable("details/{id}/{grade}") { backStackEntry ->
+ ToSDRDetailView(
+ modifier = Modifier,
+ page = backStackEntry.arguments?.getString("id") ?: "",
+ grade = backStackEntry.arguments?.getString("grade") ?: "None",
+ navController = navController
+ )
+ }
+
+ composable(
+ "details/{id}/{grade}",
+ deepLinks = listOf(navDeepLink {
+ uriPattern = "tosdr://xyz.ptgms.space/{id}/{grade}"
+ }),
+ ) { backStackEntry ->
+ ToSDRDetailView(
+ modifier = Modifier,
+ page = backStackEntry.arguments?.getString("id") ?: "",
+ grade = backStackEntry.arguments?.getString("grade") ?: "None",
+ navController = navController
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ToSShareActivity.kt b/app/src/main/java/xyz/ptgms/tosdr/ToSShareActivity.kt
new file mode 100644
index 0000000..a31f203
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ToSShareActivity.kt
@@ -0,0 +1,93 @@
+package xyz.ptgms.tosdr
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.app.PendingIntent
+import android.app.TaskStackBuilder
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.core.net.toUri
+import xyz.ptgms.tosdr.tools.data.api.API.searchPage
+import kotlin.concurrent.thread
+
+class ToSShareActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ handleIntent(intent)
+ }
+
+ private fun handleIntent(intent: Intent) {
+ val appLinkAction = intent.action
+ intent.data
+ Log.i("ToSShareActivity", "Mime type: ${intent.type}")
+ if (Intent.ACTION_SEND == appLinkAction && "text/plain" == intent.type) {
+ handleSendText(intent)
+ }
+ }
+
+ private fun handleSendText(intent: Intent) {
+ intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
+ // Parse it into URL object and get the domain without subdomains.
+ val url = Uri.parse(it)
+ val domain = url.host?.split(".")?.takeLast(2)?.joinToString(".")
+
+ if (domain == null) {
+ finish()
+ return
+ }
+
+ val dialog = AlertDialog.Builder(this)
+
+ dialog.setTitle(getString(R.string.dialog_open_question))
+ dialog.setMessage(getString(R.string.dialog_open_website).format(domain))
+ dialog.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
+ // Get shared preferences
+ val prefs = getSharedPreferences("settings", Context.MODE_PRIVATE)
+ thread {
+ val searchResult = searchPage(domain, prefs.getBoolean("hideGrade", false), prefs.getBoolean("hideNotReviewed", false))
+
+ // Set the report in the UI thread
+ runOnUiThread {
+ if (searchResult.isNotEmpty()) {
+ val id = searchResult[0].page
+ val grade = searchResult[0].grade
+ val deepLinkIntent = Intent(
+ Intent.ACTION_VIEW,
+ "tosdr://xyz.ptgms.space/$id/$grade".toUri(),
+ this,
+ MainActivity::class.java
+ )
+
+ val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(this).run {
+ addNextIntentWithParentStack(deepLinkIntent)
+ getPendingIntent(0, PendingIntent.FLAG_MUTABLE)
+ }
+
+ deepLinkPendingIntent?.send()
+ finish()
+ } else {
+ Toast.makeText(this, getString(R.string.dialog_open_noresults), Toast.LENGTH_SHORT).show()
+ finish()
+ }
+ }
+ }
+ }
+
+ dialog.setNegativeButton(getString(android.R.string.cancel)) { _, _ ->
+ finish()
+ }
+
+ dialog.setOnDismissListener {
+ finish()
+ }
+
+ dialog.show()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/tools/data/Billing.kt b/app/src/main/java/xyz/ptgms/tosdr/tools/data/Billing.kt
new file mode 100644
index 0000000..a8033b9
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/tools/data/Billing.kt
@@ -0,0 +1,7 @@
+package xyz.ptgms.tosdr.tools.data
+
+import com.android.billingclient.api.ProductDetails
+
+object Billing {
+ var products: MutableList = mutableListOf()
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/tools/data/TosDR.kt b/app/src/main/java/xyz/ptgms/tosdr/tools/data/TosDR.kt
new file mode 100644
index 0000000..0c7d0b6
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/tools/data/TosDR.kt
@@ -0,0 +1,49 @@
+package xyz.ptgms.tosdr.tools.data
+
+import androidx.annotation.Keep
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.graphics.vector.ImageVector
+
+object Locale {
+ // A list of all supported languages for translation APIs
+ val languages = mutableListOf(" ", "de", "pl", "nl")
+ val languages_libre = mutableListOf("ar", "az", "zh", "cs", "da", "nl", "eo", "fi", "fr", "de",
+ "el", "he", "hi", "hu", "id", "ga", "it", "ja", "ko", "fa", "pl", "pt", "ru", "sk", "es",
+ "sv", "tr", "uk")
+}
+
+@Keep
+data class TosDR(
+ val name: String,
+ val id: String,
+ val icon: String,
+ val grade: String,
+ var points: MutableList,
+ val reviewed: Boolean,
+ val urls: List,
+)
+
+@Keep
+data class Point(
+ var title: MutableState,
+ var tlDr: MutableState,
+ var description: MutableState,
+ val quote: String,
+ val type: String,
+ val links: String,
+ var translated: Boolean = false
+)
+
+@Keep
+data class SearchResult(
+ val name: String,
+ val page: String,
+ val grade: String
+)
+
+@Keep
+data class NavigationItem(
+ val name: String,
+ val icon: ImageVector,
+ val page: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/tools/data/api/API.kt b/app/src/main/java/xyz/ptgms/tosdr/tools/data/api/API.kt
new file mode 100644
index 0000000..da59ac7
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/tools/data/api/API.kt
@@ -0,0 +1,252 @@
+package xyz.ptgms.tosdr.tools.data.api
+
+import android.os.StrictMode
+import android.util.Log
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.text.intl.Locale
+import com.deepl.api.ConnectionException
+import com.deepl.api.Formality
+import com.deepl.api.QuotaExceededException
+import com.deepl.api.TextResult
+import com.deepl.api.TextTranslationOptions
+import com.deepl.api.Translator
+import xyz.ptgms.tosdr.tools.data.ParseJSON
+import xyz.ptgms.tosdr.tools.data.ParseJSON.parseLibreTranslateJson
+import xyz.ptgms.tosdr.tools.data.Point
+import xyz.ptgms.tosdr.tools.data.SearchResult
+import xyz.ptgms.tosdr.tools.data.TosDR
+import java.net.URL
+
+object API {
+ fun deepLTranslation(original: Point, apiKey: String): Point {
+ // Return some default values to not waste API Calls
+ if (original.title.value == "Generated through the annotate view")
+ original.title.value = ""
+
+ // Get the language code
+ val locale = Locale.current.language
+
+ // No need to translate english / If the language code is not supported, return original to not waste API Calls
+ if (locale.startsWith("en") || !xyz.ptgms.tosdr.tools.data.Locale.languages.contains(locale)) {
+ return original
+ }
+
+ val translationQueue = mutableListOf()
+ val translatedStrings = mutableListOf()
+
+ val translateFinal = mutableListOf("", "", "")
+
+ if (original.title.value != "") {
+ Log.d("DeepL", original.title.value)
+ translationQueue.add(original.title.value)
+ translatedStrings.add(0)
+ }
+
+ if (original.description.value != "") {
+ Log.d("DeepL", original.title.value)
+ translationQueue.add(original.description.value)
+ translatedStrings.add(1)
+ }
+
+ // We do not translate quotes
+ // 1: Saves API Calls
+ // 2: Display quotes in the original language
+
+ val translator = Translator(apiKey)
+ try {
+ val results: List = translator.translateText(
+ translationQueue,
+ null,
+ locale,
+ TextTranslationOptions().setFormality(Formality.Less)
+ )
+
+ results.forEach { result ->
+ translatedStrings.forEach {
+ when (it) {
+ 0 -> translateFinal[0] = result.text
+ 1 -> translateFinal[1] = result.text
+ }
+ }
+ }
+ original.title.value = translateFinal[0]
+ original.description.value = translateFinal[1]
+
+ original.translated = true
+
+ return original
+ } catch (e: IllegalArgumentException) {
+ Log.e("DeepL", "Error while translating: \"${e.message}\"")
+ Log.e("DeepL", "Original: ${original.title.value}")
+ Log.e("DeepL", "\"${original.description.value}\"")
+ Log.e("DeepL", "\"${original.tlDr.value}\"")
+ return original
+ } catch (e: ConnectionException) {
+ Log.e("DeepL", "Error while translating: \"${e.message}\"")
+ return original
+ } catch (e: QuotaExceededException) {
+ return original
+ }
+ }
+
+ fun libreTranslate(original: Point): Point {
+ // Return some default values to not waste API Calls
+ if (original.title.value == "Generated through the annotate view")
+ original.title.value = ""
+
+ // Get the language code
+ val locale = Locale.current.language
+
+ // No need to translate english
+ if (locale.startsWith("en"))
+ return original
+
+ if (!xyz.ptgms.tosdr.tools.data.Locale.languages_libre.contains(locale)) {
+ return original
+ }
+
+ val translationQueue = mutableListOf()
+ val translatedStrings = mutableListOf()
+
+ val translateFinal = mutableListOf("", "", "")
+
+ if (original.title.value != "") {
+ translationQueue.add(original.title.value)
+ translatedStrings.add(0)
+ }
+
+ if (original.description.value != "") {
+ translationQueue.add(original.description.value)
+ translatedStrings.add(1)
+ }
+
+ // We do not translate quotes
+ // 1: Saves API Calls
+ // 2: Display quotes in the original language
+
+ try {
+ val results: MutableList = mutableListOf()
+
+ translationQueue.forEach {
+ // Translate logic
+ results.add(parseLibreTranslateJson(locale, it))
+ }
+
+ results.forEach { result ->
+ translatedStrings.forEach {
+ when (it) {
+ 0 -> translateFinal[0] = result
+ 1 -> translateFinal[1] = result
+ }
+ }
+ }
+ original.title.value = translateFinal[0]
+ original.description.value = translateFinal[1]
+
+ original.translated = true
+
+ return original
+ } catch (e: Exception) {
+ Log.e("LibreTranslate", "Error while translating: \"${e.message}\"")
+ Log.e("LibreTranslate", "Original: ${original.title.value}")
+ Log.e("LibreTranslate", "\"${original.description.value}\"")
+ Log.e("LibreTranslate", "\"${original.tlDr.value}\"")
+ return original
+ }
+ }
+ fun getToSDR(service: String, grade: String, localisePoints: Boolean): TosDR?{
+ // Request: https://api.tosdr.org/service/v1/
+ // Parameter: service: String
+ val policy = StrictMode.ThreadPolicy.Builder().permitNetwork().build()
+ StrictMode.setThreadPolicy(policy)
+ val url = "https://tosdr.org/api/v1/service/$service.json"
+ val response = URL(url).readText()
+ Log.d("Api", response)
+ val json = ParseJSON.parseJSON(response)
+ if ((json.error ?: 256).toInt() != 256) {
+ return null
+ }
+ val name = json.name ?: "Unknown"
+ val points = json.pointsData
+ val pointList = json.points
+ val pointsList = mutableListOf()
+ if (pointList != null && points != null) {
+ //Log.d("Api", "Points: $pointList")
+ //Log.d("Api", "Points: $points")
+ points.forEach { point ->
+ //val point = points[i.toString()] ?: continue
+ val title = point.value.title
+ val tldr =
+ if (point.value.tosdr.tldr != "Generated through the annotate view") point.value.tosdr.tldr else ""
+ val description = point.value.tosdr.case
+ val quote = point.value.quoteText
+ val link = point.value.discussion
+ pointsList.add(
+ Point(
+ mutableStateOf(title),
+ mutableStateOf(tldr),
+ mutableStateOf(description),
+ quote ?: "",
+ point.value.tosdr.point,
+ link,
+ Locale.current.language=="en" || !localisePoints
+ )
+ )
+ }
+ }
+
+ // Check if type of serviceClass is boolean
+ val rating: String = if (json.serviceClass is Boolean) {
+ "N/A"
+ } else {
+ (json.serviceClass ?: "N/A").toString()
+ }
+ //val rating: String = (json.serviceClass ?: "N/A") as String
+ return TosDR(
+ name,
+ service,
+ json.image ?: "",
+ grade,
+ pointsList,
+ rating != "N/A",
+ json.urls ?: listOf(),
+ )
+ }
+
+ fun searchPage(
+ query: String,
+ hideGrade: Boolean,
+ hideNotReviewed: Boolean
+ ): List {
+ // Request: https://api.tosdr.org/search/v4/?query=
+ // Parameter: query: String
+ val policy = StrictMode.ThreadPolicy.Builder().permitNetwork().build()
+ StrictMode.setThreadPolicy(policy)
+ val url = "https://api.tosdr.org/search/v4/?query=$query"
+ val response = URL(url).readText()
+ Log.d("Api", response)
+ val json = ParseJSON.parseSearch(response)
+
+ if (json.error.toInt() != 256) {
+ return mutableListOf()
+ }
+
+ val searchResults = mutableListOf()
+
+ for (i in json.parameters.services.indices) {
+ if (!json.parameters.services[i].isComprehensivelyReviewed && hideNotReviewed) {
+ continue
+ }
+ val service = json.parameters.services[i]
+ val name = service.name
+ val grade = service.rating.letter
+ if (grade == "N/A" && hideGrade) {
+ continue
+ }
+ searchResults.add(SearchResult(name, service.id.toInt().toString(), grade))
+ }
+
+ return searchResults
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/tools/data/gradeToHumanReadable.kt b/app/src/main/java/xyz/ptgms/tosdr/tools/data/gradeToHumanReadable.kt
new file mode 100644
index 0000000..b3131b2
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/tools/data/gradeToHumanReadable.kt
@@ -0,0 +1,40 @@
+package xyz.ptgms.tosdr.tools.data
+
+import androidx.compose.ui.graphics.Color
+
+object GradeToHumanReadable {
+
+ fun gradeToHuman(grade: String): String {
+ return when (grade) {
+ "A" -> "Good"
+ "B" -> "Okay"
+ "C" -> "Bad"
+ "D" -> "Very Bad"
+ "E" -> "Horrible"
+ else -> "Unknown"
+ }
+ }
+
+ fun gradeBackground(grade: String): Color {
+ return when (grade) {
+ "A" -> Color(0xFF46A546)
+ "B" -> Color(0xFF61CF61)
+ "B-" -> Color(0xFF70E670)
+ "C" -> Color(0xFFF89406)
+ "D" -> Color(0xFFC43C35)
+ "E" -> Color(0xFF9E342E)
+ else -> Color(0xFF999999)
+ }
+ }
+
+ fun gradeForeground(grade: String): Color {
+ return when (grade) {
+ "A" -> Color(0xFFFFFFFF)
+ "B" -> Color(0xFFFFFFFF)
+ "C" -> Color(0xFF000000)
+ "D" -> Color(0xFFFFFFFF)
+ "E" -> Color(0xFFFFFFFF)
+ else -> Color(0xFFFFFFFF)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/tools/data/parseJSON.kt b/app/src/main/java/xyz/ptgms/tosdr/tools/data/parseJSON.kt
new file mode 100644
index 0000000..43ef82e
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/tools/data/parseJSON.kt
@@ -0,0 +1,175 @@
+package xyz.ptgms.tosdr.tools.data
+
+import androidx.annotation.Keep
+import com.beust.klaxon.Json
+import com.beust.klaxon.Klaxon
+import java.net.HttpURLConnection
+import java.net.URL
+
+object ParseJSON {
+ fun parseJSON(json: String): Service.Service {
+ val klaxon = Klaxon()
+ return klaxon.parse(json)!!
+ }
+
+ fun parseSearch(json: String): Search.Search {
+ val klaxon = Klaxon()
+ return klaxon.parse(json)!!
+ }
+
+ fun parseLibreTranslateJson(language: String, source: String): String {
+ val request = TranslateRequest(q = source, source = "en", target = language)
+ val json = Klaxon().toJsonString(request)
+
+ // Make request to translate.ptgms.space.
+ val url = URL("https://translate.ptgms.space/translate")
+ val connection = url.openConnection() as HttpURLConnection
+ connection.requestMethod = "POST"
+ connection.setRequestProperty("Content-Type", "application/json")
+ connection.doOutput = true
+ connection.outputStream.write(json.toByteArray())
+
+ val response = connection.inputStream.bufferedReader().readText()
+ val responseJson = Klaxon().parse(response)!!
+ return responseJson.translatedText ?: source
+ }
+}
+
+
+@Keep
+data class LibreTranslateResponse (
+ val error: String? = null,
+ val translatedText: String? = null
+)
+
+
+@Keep
+data class TranslateRequest(
+ val q: String,
+ val source: String,
+ val target: String,
+ val format: String = "text"
+)
+
+@Keep
+class Service {
+ @Keep
+ data class Service (
+ val id: Long? = null,
+ val name: String? = null,
+ val slug: String? = null,
+ val image: String? = null,
+
+ @Json(name = "class")
+ val serviceClass: Any? = null,
+
+ @Json(ignored = true)
+ val links: Any? = null,
+ val points: List? = null,
+ val pointsData: Map? = null,
+ val urls: List? = null,
+ val error: Long? = null,
+ val message: String? = null,
+ val parameters: List? = null
+ )
+
+ @Keep
+ data class PointsDatum (
+ val discussion: String,
+ val id: Long,
+ val needsModeration: Boolean,
+ val quoteDoc: String? = null,
+ val quoteText: String? = null,
+ val services: List,
+ val set: String,
+ val slug: Any? = null,
+ val title: String,
+ val topics: List,
+ val tosdr: Tosdr
+ )
+
+ @Keep
+ data class Tosdr (
+ val binding: Boolean,
+ val case: String,
+ val point: String,
+ val score: Long,
+ val tldr: String
+ )
+}
+
+@Keep
+object Search {
+ @Keep
+ data class Search(
+ val error: Long,
+ val message: String,
+ val parameters: Parameters
+ )
+
+ @Keep
+ data class Parameters(
+ val services: List
+ )
+
+ @Keep
+ data class Service(
+ val id: Long,
+
+ @Json(name = "is_comprehensively_reviewed")
+ val isComprehensivelyReviewed: Boolean,
+
+ val urls: List,
+ val name: String,
+ val status: Any? = null,
+
+ @Json(name = "updated_at")
+ val updatedAt: String,
+
+ @Json(name = "created_at")
+ val createdAt: String,
+
+ val slug: String,
+ val wikipedia: String,
+ val rating: Rating,
+ val links: Links
+ )
+
+ @Keep
+ data class Links(
+ val phoenix: Phoenix,
+ val crisp: Crisp
+ )
+
+ @Keep
+ data class Crisp(
+ val api: String,
+ val service: String,
+ val badge: Badge
+ )
+
+ @Keep
+ data class Badge(
+ val svg: String,
+ val png: String
+ )
+
+ @Keep
+ data class Phoenix(
+ val service: String,
+ val documents: String,
+
+ @Json(name = "new_comment")
+ val newComment: String,
+
+ val edit: String
+ )
+
+ @Keep
+ data class Rating(
+ val hex: Long,
+ val human: String,
+ val letter: String
+ )
+
+}
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Color.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Color.kt
new file mode 100644
index 0000000..97ce5f7
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package xyz.ptgms.tosdr.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Theme.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Theme.kt
new file mode 100644
index 0000000..12add5c
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Theme.kt
@@ -0,0 +1,65 @@
+package xyz.ptgms.tosdr.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun ToSDRTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+// val view = LocalView.current
+// if (!view.isInEditMode) {
+// SideEffect {
+// val window = (view.context as Activity).window
+// window.statusBarColor = colorScheme.primary.toArgb()
+// WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+// }
+// }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Type.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Type.kt
new file mode 100644
index 0000000..be491b1
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package xyz.ptgms.tosdr.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRAboutView.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRAboutView.kt
new file mode 100644
index 0000000..6d2262d
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRAboutView.kt
@@ -0,0 +1,94 @@
+package xyz.ptgms.tosdr.ui.views
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.R
+import xyz.ptgms.tosdr.ui.views.elements.ExpandableCard
+import xyz.ptgms.tosdr.ui.views.elements.TextWithSource
+
+object ToSDRAboutView {
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun ToSDRAboutView(modifier: Modifier, scope: CoroutineScope, drawerState: DrawerState) {
+ Scaffold(topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(id = R.string.about),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = {
+ scope.launch { drawerState.open() }
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = stringResource(R.string.menu)
+ )
+ }
+ }
+ )
+ }) { padding ->
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(padding)
+ ) {
+ Text(
+ text = stringResource(R.string.about_tos_dr),
+ style = MaterialTheme.typography.headlineLarge,
+ modifier = Modifier.padding(8.dp)
+ )
+ Text(
+ text = stringResource(R.string.about_tosdr_description),
+ modifier = Modifier.padding(8.dp)
+ )
+
+ ExpandableCard(
+ title = stringResource(R.string.about_card_about_title),
+ description = TextWithSource(
+ stringResource(R.string.about_card_about_description),
+ "https://github.com/tosdr/tosdr-android",
+ stringResource(
+ R.string.visit_source
+ )
+ ),
+ modifier = Modifier.padding(8.dp)
+ )
+
+ ExpandableCard(
+ title = stringResource(R.string.about_tos_dr), description = TextWithSource(
+ stringResource(R.string.about_card_tosdr_description),
+ "https://tosdr.org/en/about",
+ stringResource(
+ R.string.visit_website
+ )
+ ), modifier = Modifier.padding(8.dp)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRDetailView.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRDetailView.kt
new file mode 100644
index 0000000..7d483d9
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRDetailView.kt
@@ -0,0 +1,529 @@
+package xyz.ptgms.tosdr.ui.views
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.rounded.Check
+import androidx.compose.material.icons.rounded.List
+import androidx.compose.material.icons.rounded.Share
+import androidx.compose.material.icons.rounded.Warning
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ElevatedSuggestionChip
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SuggestionChipDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat.startActivity
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
+import coil.compose.rememberAsyncImagePainter
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.R
+import xyz.ptgms.tosdr.tools.data.api.API
+import xyz.ptgms.tosdr.tools.data.GradeToHumanReadable.gradeBackground
+import xyz.ptgms.tosdr.tools.data.GradeToHumanReadable.gradeForeground
+import xyz.ptgms.tosdr.tools.data.GradeToHumanReadable.gradeToHuman
+import xyz.ptgms.tosdr.tools.data.Point
+import xyz.ptgms.tosdr.tools.data.TosDR
+import xyz.ptgms.tosdr.ui.views.elements.ReportCard
+import kotlin.concurrent.thread
+
+object ToSDRDetailView : ViewModel() {
+
+
+ // Localise each point in mutable so it updates over time :)
+ private fun localisePoints(
+ original: MutableList,
+ report: MutableState,
+ apiKey: String,
+ navController: NavHostController,
+ deepLSelected: Boolean
+ ) {
+ // Go through points with index
+ thread {
+ for (i in original.indices) {
+ if (navController.currentBackStackEntry?.destination?.route?.startsWith("details/") == false) {
+ Log.w("Translation", "Popped back, cancel translation!")
+ return@thread
+ }
+ // Get translation in background thread to slowly update
+ var translatedPoint: Point
+ if (deepLSelected && apiKey != "") {
+ translatedPoint = API.deepLTranslation(
+ original[i], apiKey = apiKey
+ )
+ viewModelScope.launch(Dispatchers.Main) {
+ report.value.points[i] = translatedPoint
+ }
+ } else if (!deepLSelected) {
+ translatedPoint = API.libreTranslate(
+ original[i]
+ )
+ viewModelScope.launch(Dispatchers.Main) {
+ report.value.points[i] = translatedPoint
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun ToSDRDetailView(
+ modifier: Modifier,
+ page: String,
+ grade: String,
+ navController: NavHostController,
+ preview: Boolean = false
+ ) {
+ val sharedPreference =
+ LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ val localisePoints = sharedPreference.getBoolean("localisePoints", false)
+ val deepLSelected = sharedPreference.getBoolean("deepLSelected", false)
+
+ val topAppBarState = rememberTopAppBarState()
+ val topAppBarBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
+
+ val gradeMode = remember { mutableStateOf(true) }
+
+ val openDialog = remember { mutableStateOf(false) }
+ val verifiedDialog = remember { mutableStateOf(false) }
+ val unverifiedDialog = remember { mutableStateOf(false) }
+
+ val context = LocalContext.current
+
+ val points = remember {
+ mutableListOf(
+ )
+ }
+
+ val report = remember {
+ mutableStateOf(
+ TosDR(
+ name = "",
+ id = "",
+ icon = "",
+ grade = "",
+ points = points,
+ reviewed = false,
+ urls = listOf()
+ )
+ )
+ }
+
+ if (preview)
+ report.value = TosDR(
+ name = "Example",
+ id = "example",
+ icon = "https://tosdr.org/images/services/1.png",
+ grade = "A",
+ points = mutableListOf(
+ Point(
+ title = remember { mutableStateOf("Example Title") },
+ description = remember { mutableStateOf("Example Point") },
+ quote = "This is an example quote",
+ tlDr = remember { mutableStateOf("Example tldr") },
+ type = "good",
+ links = ""
+ ),
+ Point(
+ title = remember { mutableStateOf("Example Title") },
+ description = remember { mutableStateOf("Example Point") },
+ quote = "This is an example quote",
+ tlDr = remember { mutableStateOf("Example tldr") },
+ type = "blocker",
+ links = ""
+ )
+ ),
+ reviewed = true,
+ urls = listOf("https://tosdr.org")
+ )
+
+ Dialog(
+ Icons.Rounded.Check, stringResource(R.string.detail_review_true_title), stringResource(
+ R.string.detail_review_true_description
+ ), Color.Green, verifiedDialog
+ )
+ Dialog(
+ Icons.Rounded.Warning,
+ stringResource(R.string.detail_review_false_title),
+ stringResource(
+ R.string.detail_review_false_description
+ ),
+ Color.Red,
+ unverifiedDialog
+ )
+
+ // Alert dialog
+ if (openDialog.value) {
+ AlertDialog(
+ onDismissRequest = { openDialog.value = false },
+ title = { Text(stringResource(R.string.details_known_urls).format(report.value.name)) },
+ text = {
+ Column(modifier.fillMaxWidth()) {
+ var text = ""
+ report.value.urls.forEach {
+ text += "$it\n"
+ }
+ Text(
+ text, modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .fillMaxWidth()
+ )
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = { openDialog.value = false }) {
+ Text(stringResource(id = android.R.string.ok))
+ }
+ },
+ )
+ }
+
+ val apiKey = sharedPreference.getString("deeplKey", "") ?: ""
+
+ thread {
+ try {
+ val data = API.getToSDR(page, grade, localisePoints)!!
+ // Set the report in the UI thread
+ viewModelScope.launch(Dispatchers.Main) {
+ report.value = data
+ if (localisePoints) {
+ localisePoints(
+ original = report.value.points,
+ report = report,
+ apiKey = apiKey,
+ navController= navController,
+ deepLSelected = deepLSelected
+ )
+ }
+ }
+
+ } catch (e: Exception) {
+ viewModelScope.launch(Dispatchers.Main) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.network_error),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ return@thread
+ }
+ }
+
+ Scaffold(
+ //modifier = Modifier.nestedScroll(TopAppBarDefaults.enterAlwaysScrollBehavior().nestedScrollConnection),
+ topBar = {
+ Column {
+ LargeTopAppBar(scrollBehavior = topAppBarBehavior,
+ title = {
+ if (report.value.name == "") {
+ Text(
+ text = stringResource(R.string.loading),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ } else {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = rememberAsyncImagePainter(report.value.icon),
+ contentDescription = stringResource(R.string.details_icon).format(
+ report.value.name
+ ),
+ modifier = Modifier
+ .size(50.dp)
+ .clip(shape = RoundedCornerShape(8.dp))
+ )
+ Text(
+ text = report.value.name,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+ }
+ },
+ navigationIcon = {
+ IconButton(onClick = {
+ navController.popBackStack()
+ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.back)
+ )
+ }
+ })
+
+ }
+ }) { padding ->
+ // Display report once it's loaded
+ if (report.value.name != "") {
+ LazyColumn(
+ modifier = modifier
+ //.verticalScroll(rememberScrollState())
+ .padding(padding)
+ .nestedScroll(topAppBarBehavior.nestedScrollConnection),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ item {
+ //val context = LocalContext.current
+ Column(
+ modifier = Modifier
+ ) {
+ Row(
+ modifier = Modifier
+ //.fillMaxWidth()
+ .horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Spacer(modifier = Modifier.size(12.dp))
+ if (report.value.reviewed) {
+ ElevatedSuggestionChip(
+ colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
+ containerColor = Color(0xFF00C853),
+ labelColor = Color.White,
+ iconContentColor = Color.White
+ ),
+ label = { Text(text = stringResource(R.string.details_reviewed)) },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Check,
+ contentDescription = stringResource(R.string.details_reviewed)
+ )
+ },
+ onClick = {
+ verifiedDialog.value = true
+ }
+ )
+ } else {
+ ElevatedSuggestionChip(
+ colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
+ containerColor = Color(0xFFC80000),
+ labelColor = Color.White,
+ iconContentColor = Color.White
+ ),
+ label = { Text(text = stringResource(R.string.details_not_reviewed)) },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Warning,
+ contentDescription = stringResource(R.string.details_not_reviewed)
+ )
+ },
+ onClick = {
+ unverifiedDialog.value = true
+ }
+ )
+ }
+ if (report.value.grade != "None") {
+ ElevatedSuggestionChip(
+ colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
+ containerColor = gradeBackground(report.value.grade),
+ labelColor = gradeForeground(report.value.grade),
+ iconContentColor = gradeForeground(report.value.grade)
+ ),
+ label = {
+ if (gradeMode.value) {
+ Text(
+ text = stringResource(R.string.details_grade).format(
+ report.value.grade
+ )
+ )
+ } else {
+ Text(text = gradeToHuman(report.value.grade))
+ }
+ },
+ onClick = {
+ gradeMode.value = !gradeMode.value
+ }
+ )
+ }
+ ElevatedSuggestionChip(
+ onClick = {},
+ label = {
+ Text(
+ text = stringResource(R.string.details_cases).format(
+ report.value.points.size
+ )
+ )
+ },
+ icon = {
+ Icon(
+ Icons.Rounded.Warning,
+ contentDescription = stringResource(R.string.details_cases).format(
+ report.value.points.size
+ )
+ )
+ }
+ )
+ ElevatedSuggestionChip(
+ onClick = {
+ val url = "https://tosdr.org/en/service/${report.value.id}"
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(url)
+ startActivity(context, intent, null)
+ },
+ label = { Text(text = stringResource(R.string.details_view_on_page)) },
+ icon = {
+ Icon(
+ Icons.Rounded.Share,
+ contentDescription = stringResource(R.string.details_view_on_page)
+ )
+ }
+ )
+ ElevatedSuggestionChip(
+ onClick = {
+ // Show alert with all the URLs
+ openDialog.value = true
+ },
+ label = {
+ Text(
+ text = stringResource(R.string.details_saved_urls).format(
+ report.value.urls.size
+ )
+ )
+ },
+ icon = {
+ Icon(
+ Icons.Rounded.List, contentDescription = "URLs"
+ )
+ }
+ )
+ Spacer(modifier = Modifier.size(12.dp))
+ }
+ Divider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ )
+ }
+ }
+ items(count = report.value.points.size) { index ->
+ val it = report.value.points[index]
+ ReportCard(
+ title = it.title.value,
+ description = it.description.value,
+ quote = it.quote,
+ type = it.type,
+ link = it.links,
+ translation = it.translated,
+ modifier = Modifier.padding(
+ top = 6.dp,
+ bottom = 6.dp,
+ start = 16.dp,
+ end = 16.dp
+ )
+ )
+ }
+ }
+ } else {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(padding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ CircularProgressIndicator()
+ Text(
+ text = stringResource(id = R.string.loading),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+
+ }
+
+ @Composable
+ fun Dialog(
+ icon: ImageVector,
+ title: String,
+ description: String,
+ tint: Color,
+ visible: MutableState
+ ) {
+ if (visible.value) {
+ AlertDialog(
+ icon = {
+ Icon(
+ tint = tint,
+ imageVector = icon,
+ modifier = Modifier.size(48.dp),
+ contentDescription = title
+ )
+ },
+ onDismissRequest = { visible.value = false },
+ title = { Text(title) },
+ text = { Text(description) },
+ confirmButton = {
+ TextButton(onClick = { visible.value = false }) {
+ Text(stringResource(android.R.string.ok))
+ }
+ }
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true, showSystemUi = true)
+@Composable
+fun Preview() {
+ val context = LocalContext.current
+ ToSDRDetailView.ToSDRDetailView(
+ modifier = Modifier,
+ page = "182",
+ grade = "E",
+ navController = NavHostController(context),
+ preview = true
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRDonateView.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRDonateView.kt
new file mode 100644
index 0000000..07750b1
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRDonateView.kt
@@ -0,0 +1,193 @@
+package xyz.ptgms.tosdr.ui.views
+
+import android.app.Activity
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.Button
+import androidx.compose.material3.Divider
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingFlowParams
+import com.android.billingclient.api.ProductDetails
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.R
+import xyz.ptgms.tosdr.tools.data.Billing
+import xyz.ptgms.tosdr.ui.views.elements.ExpandableCard
+
+object ToSDRDonateView {
+
+ private fun makePurchase(position: Int, billingClient: BillingClient, activity: Activity) {
+ Log.d("Billing", "Making purchase for ${Billing.products[position].productId}")
+
+ val products = mutableListOf("1euro_donation", "5euro_donation", "10euro_donation")
+
+ var product: ProductDetails? = null
+
+ Billing.products.forEach {
+ if (it.productId == products[position]) {
+ product = it
+ }
+ }
+
+ if (product == null) {
+ return
+ }
+
+ val productDetailsParamsList = listOf(
+ BillingFlowParams.ProductDetailsParams.newBuilder()
+ .setProductDetails(product!!)
+ .build()
+ )
+
+ val billingFlowParams = BillingFlowParams.newBuilder()
+ .setProductDetailsParamsList(productDetailsParamsList)
+ .build()
+
+ val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)
+
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
+ Toast.makeText(activity, activity.getString(R.string.donation_error), Toast.LENGTH_SHORT).show()
+ }
+
+ }
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun ToSDRDonateView(modifier: Modifier, billingClient: BillingClient, activity: Activity, scope: CoroutineScope, drawerState: DrawerState) {
+ val iapEnabled = Billing.products.isNotEmpty()
+
+ Scaffold(topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(id = R.string.donate),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = {
+ scope.launch { drawerState.open() }
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = stringResource(R.string.menu)
+ )
+ }
+ }
+ )
+ }) { padding ->
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(padding)
+ ) {
+ Text(
+ stringResource(R.string.donation_info),
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(8.dp)
+ )
+
+ ExpandableCard(
+ title = stringResource(R.string.donation_info_card_title),
+ description = stringResource(
+ R.string.donation_info_card_description
+ ),
+ modifier = Modifier.padding(8.dp)
+ )
+
+ Divider(modifier = Modifier.padding(8.dp))
+
+ ElevatedCard(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(8.dp),
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text("1€", style = MaterialTheme.typography.titleLarge)
+ Text(
+ stringResource(R.string.donation_thanks_1euro),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ Button(onClick = {
+ makePurchase(0, billingClient, activity)
+ }, enabled = iapEnabled) {
+ Text(stringResource(R.string.donate))
+ }
+ }
+ }
+ ElevatedCard(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(8.dp),
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text("5€", style = MaterialTheme.typography.titleLarge)
+ Text(
+ stringResource(R.string.donation_thanks_5euro),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ Button(onClick = {
+ makePurchase(1, billingClient, activity)
+ }, enabled = iapEnabled) {
+ Text(stringResource(R.string.donate))
+ }
+ }
+ }
+ ElevatedCard(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(8.dp),
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text("10€", style = MaterialTheme.typography.titleLarge)
+ Text(
+ stringResource(R.string.donation_thanks_10euro),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ Button(onClick = {
+ makePurchase(2, billingClient, activity)
+ }, enabled = iapEnabled) {
+ Text(stringResource(R.string.donate))
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRHomeView.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRHomeView.kt
new file mode 100644
index 0000000..4c8e4f6
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRHomeView.kt
@@ -0,0 +1,105 @@
+package xyz.ptgms.tosdr.ui.views
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.R
+import xyz.ptgms.tosdr.ui.views.elements.ExpandableCard
+import xyz.ptgms.tosdr.ui.views.elements.TextWithSource
+
+object ToSDRHomeView {
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun ToSDRHomeView(modifier: Modifier, scope: CoroutineScope, drawerState: DrawerState) {
+ val topAppBarState = rememberTopAppBarState()
+ val topAppBarBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
+ Scaffold(topBar = {
+ TopAppBar(
+ scrollBehavior = topAppBarBehavior,
+ title = {
+ Text(
+ text = stringResource(id = R.string.home),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = {
+ scope.launch { drawerState.open() }
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = stringResource(R.string.menu)
+ )
+ }
+ }
+ )
+ }) { padding ->
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .nestedScroll(topAppBarBehavior.nestedScrollConnection)
+ .padding(padding)
+ ) {
+ Text(
+ text = stringResource(R.string.home_welcome),
+ style = MaterialTheme.typography.headlineLarge,
+ modifier = Modifier.padding(8.dp)
+ )
+ ExpandableCard(
+ title = stringResource(R.string.home_card_what_is_this_title),
+ description = stringResource(
+ R.string.home_card_what_is_this_description
+ ),
+ modifier = Modifier.padding(8.dp)
+ )
+ ExpandableCard(
+ title = stringResource(R.string.home_card_how_to_use_title),
+ description = stringResource(
+ R.string.home_card_how_to_use_description
+ ),
+ modifier = Modifier.padding(8.dp)
+ )
+ ExpandableCard(
+ title = stringResource(R.string.home_card_like_title),
+ description = stringResource(
+ R.string.home_card_like_description
+ ),
+ modifier = Modifier.padding(8.dp)
+ )
+ ExpandableCard(
+ title = stringResource(R.string.home_card_legal_title),
+ description = TextWithSource(
+ stringResource(R.string.home_card_legal_description),
+ "https://tosdr.org/legal",
+ null
+ ),
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRLicensesView.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRLicensesView.kt
new file mode 100644
index 0000000..6479aa4
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRLicensesView.kt
@@ -0,0 +1,120 @@
+package xyz.ptgms.tosdr.ui.views
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ElevatedSuggestionChip
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import androidx.navigation.NavHostController
+import com.mikepenz.aboutlibraries.Libs
+import com.mikepenz.aboutlibraries.util.withContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.R
+
+object ToSDRLicensesView {
+
+ // This is important to prevent a Play Store warning or ban
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun ToSDRLicensesView(
+ modifier: Modifier,
+ scope: CoroutineScope,
+ navController: NavHostController
+ ) {
+
+ val topAppBarState = rememberTopAppBarState()
+ val topAppBarBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
+
+ val context = LocalContext.current
+ Scaffold(topBar = {
+ TopAppBar(scrollBehavior = topAppBarBehavior,
+ title = {
+ Text(
+ text = stringResource(R.string.open_source_licenses),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = {
+ scope.launch { navController.popBackStack() }
+ }) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.back)
+ )
+ }
+ }
+ )
+ }) { padding ->
+ LazyColumn(
+ modifier = modifier
+ .padding(padding)
+ .nestedScroll(topAppBarBehavior.nestedScrollConnection),
+ ) {
+ val libs = Libs.Builder()
+ .withContext(context)
+ .build()
+ // We take libraries but filter out duplicates
+ val libraries = libs.libraries.distinctBy { Pair(it.name, it.description) }
+
+ items(libraries.size) { index ->
+ ElevatedCard(modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)) {
+ Text(text = libraries[index].name, style = TextStyle(fontSize = 20.sp), modifier = Modifier.padding(8.dp))
+ Spacer(modifier = Modifier.height(8.dp))
+ if (libraries[index].developers.isNotEmpty()) {
+ Text(text = libraries[index].developers[0].name ?: "No developer", modifier = Modifier.padding(8.dp))
+ }
+ Divider()
+ Text(text = libraries[index].description ?: "No description", modifier = Modifier.padding(8.dp))
+
+ Row(modifier = Modifier.padding(8.dp)) {
+ libraries[index].licenses.forEach {
+ ElevatedSuggestionChip(
+ modifier = Modifier.padding(4.dp),
+ label = { Text(text = it.name) },
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(it.url)
+ ContextCompat.startActivity(context, intent, null)
+ }
+ )
+ }
+ }
+ }
+
+ Spacer(modifier= Modifier.height(10.dp))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRSearchView.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRSearchView.kt
new file mode 100644
index 0000000..b6c6ded
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRSearchView.kt
@@ -0,0 +1,210 @@
+package xyz.ptgms.tosdr.ui.views
+
+import android.content.Context
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.R
+import xyz.ptgms.tosdr.tools.data.api.API.searchPage
+import xyz.ptgms.tosdr.tools.data.GradeToHumanReadable.gradeBackground
+import xyz.ptgms.tosdr.tools.data.GradeToHumanReadable.gradeForeground
+import xyz.ptgms.tosdr.tools.data.SearchResult
+import java.lang.Exception
+import kotlin.concurrent.thread
+
+object ToSDRSearchView : ViewModel() {
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun ToSDRView(modifier: Modifier, navHostController: NavHostController, scope: CoroutineScope, drawerState: DrawerState) {
+ val context: Context = LocalContext.current
+
+ val topAppBarState = rememberTopAppBarState()
+ val topAppBarBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
+
+ val sharedPreference =
+ LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ val hideGrade = sharedPreference.getBoolean("hideNoGrade", false)
+ val hideNotReviewed = sharedPreference.getBoolean("hideNotReviewed", false)
+ var value by rememberSaveable { mutableStateOf("") }
+ var status by rememberSaveable { mutableStateOf("") }
+ val searchResult = remember { mutableStateListOf() }
+ var progress by remember { mutableStateOf(0.0f) }
+ Scaffold(topBar = {
+ TopAppBar(
+ scrollBehavior = topAppBarBehavior,
+ title = {
+ Text(
+ text = stringResource(id = R.string.search),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = {
+ scope.launch { drawerState.open() }
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = stringResource(R.string.menu)
+ )
+ }
+ }
+ )
+ }) { padding ->
+ LazyColumn(
+ modifier = modifier
+ .fillMaxSize()
+ //.verticalScroll(rememberScrollState())
+ .padding(padding)
+ .nestedScroll(topAppBarBehavior.nestedScrollConnection)
+ ) {
+ // Website name input
+ //LinearProgressIndicator(progress = progress, modifier = Modifier.padding(8.dp).fillMaxWidth())
+ item { Row(modifier = Modifier.fillMaxWidth()) {
+ OutlinedTextField(
+ value = value,
+ onValueChange = { value = it },
+ label = { Text(stringResource(R.string.search_label)) },
+ placeholder = { Text(stringResource(R.string.search_label_placeholder)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Filled.Search,
+ contentDescription = stringResource(R.string.search)
+ )
+ },
+ trailingIcon = {
+ if (value != "") {
+ if (progress != 0f)
+ CircularProgressIndicator()
+ else
+ IconButton(onClick = { value = "" }) {
+ Icon(
+ painter = painterResource(id = R.drawable.round_backspace_24),
+ contentDescription = stringResource(R.string.clear)
+ )
+ }
+ } else Spacer(modifier = Modifier)
+ },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(onSearch = {
+ status = context.getString(R.string.loading)
+ progress = 0.1f
+ searchResult.clear()
+ thread {
+ progress = 0.3f
+ try {
+ val data = searchPage(value, hideGrade, hideNotReviewed)
+ progress = 0.7f
+
+ // Set the report in the UI thread
+ viewModelScope.launch(Dispatchers.Main) {
+ searchResult.addAll(data)
+ progress = 0f
+ status = context.getString(R.string.done)
+ }
+ } catch (e: Exception) {
+ viewModelScope.launch(Dispatchers.Main) {
+ Toast.makeText(context, context.getString(R.string.network_error), Toast.LENGTH_LONG).show()
+ progress = 0f
+ status = context.getString(R.string.done)
+ }
+ Log.e("ToSDRSearchView", "Error while searching", e)
+ return@thread
+ }
+ }
+ }),
+ maxLines = 1
+ )
+ }
+ }
+
+ items(searchResult.size) {index ->
+ SearchCard(searchResult[index], navHostController)
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ private fun SearchCard(SearchResult: SearchResult, navController: NavHostController) {
+ ElevatedCard(modifier = Modifier.padding(8.dp), shape = CardDefaults.shape, onClick = {
+ if (SearchResult.grade == "N/A") {
+ navController.navigate("details/${SearchResult.page}/None")
+ } else {
+ navController.navigate("details/${SearchResult.page}/${SearchResult.grade}")
+ }
+
+ }) {
+ Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = SearchResult.name, fontSize = 20.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier
+ .weight(1f)
+ .padding(12.dp))
+ Card(
+ colors = CardDefaults.cardColors(containerColor = gradeBackground(SearchResult.grade), contentColor = gradeForeground(SearchResult.grade)),
+ modifier = Modifier
+ .width(65.dp)
+ .padding(12.dp)
+ ) {
+ Text(text = SearchResult.grade, color = gradeForeground(SearchResult.grade), textAlign = TextAlign.Center, modifier = Modifier
+ .fillMaxWidth()
+ .padding(4.dp))
+ }
+
+ }
+ }
+ //ReportCard(title = SearchResult.Name, grade = SearchResult.Grade, page = SearchResult.Page, modifier = Modifier.padding(8.dp))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRSettingsView.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRSettingsView.kt
new file mode 100644
index 0000000..df37b6f
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/ToSDRSettingsView.kt
@@ -0,0 +1,256 @@
+package xyz.ptgms.tosdr.ui.views
+
+import android.content.Context
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavHostController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import xyz.ptgms.tosdr.R
+
+object ToSDRSettingsView {
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun ToSDRSettingsView(modifier: Modifier, scope: CoroutineScope, drawerState: DrawerState, navController: NavHostController) {
+ val context = LocalContext.current
+ val sharedPreference = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+
+ val hideNoGrade = remember { mutableStateOf(sharedPreference.getBoolean("hideNoGrade", false)) }
+ val hideNotReviewed = remember { mutableStateOf(sharedPreference.getBoolean("hideNotReviewed", false)) }
+ val localisePoints = remember { mutableStateOf(sharedPreference.getBoolean("localisePoints", false)) }
+ val deepLSelected = remember { mutableStateOf(sharedPreference.getBoolean("deepLSelected", false)) }
+ //val showSeverity = remember { mutableStateOf(sharedPreference.getBoolean("showSeverity", false)) }
+
+ val deepLkey = remember { mutableStateOf(sharedPreference.getString("deeplKey", "")) }
+
+ Scaffold(topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(id = R.string.settings),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = {
+ scope.launch { drawerState.open() }
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Menu,
+ contentDescription = stringResource(R.string.menu)
+ )
+ }
+ }
+ )
+ }) { padding ->
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(padding)
+ ) {
+ Text(
+ text = stringResource(R.string.settings_search),
+ modifier = Modifier.padding(16.dp)
+ )
+ ElevatedCard(modifier = Modifier
+ .padding(4.dp)
+ .fillMaxWidth(), onClick = {
+ hideNoGrade.value = !hideNoGrade.value
+ sharedPreference.edit().putBoolean("hideNoGrade", hideNoGrade.value).apply()
+ }) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.settings_search_hidenograde),
+ modifier = Modifier.weight(1f)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Switch(checked = hideNoGrade.value, onCheckedChange = {
+ hideNoGrade.value = it
+ sharedPreference.edit().putBoolean("hideNoGrade", it).apply()
+ })
+ }
+ }
+ ElevatedCard(modifier = Modifier
+ .padding(4.dp)
+ .fillMaxWidth(), onClick = {
+ hideNotReviewed.value = !hideNotReviewed.value
+ sharedPreference.edit().putBoolean("hideNotReviewed", hideNotReviewed.value)
+ .apply()
+ }) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.settings_search_hidenoreview),
+ modifier = Modifier.weight(1f)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Switch(checked = hideNotReviewed.value, onCheckedChange = {
+ hideNotReviewed.value = it
+ sharedPreference.edit().putBoolean("hideNotReviewed", it).apply()
+ })
+ }
+ }
+ if (!Locale.current.language.startsWith("en")) {
+ Text(
+ text = stringResource(R.string.settings_localisation),
+ modifier = Modifier.padding(16.dp)
+ )
+ ElevatedCard(modifier = Modifier
+ .padding(4.dp)
+ .fillMaxWidth(), onClick = {
+ localisePoints.value = !localisePoints.value
+ sharedPreference.edit().putBoolean("localisePoints", localisePoints.value)
+ .apply()
+ }) {
+ Column {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(text = stringResource(R.string.settings_localise_points_in_services))
+ if (deepLSelected.value) {
+ Text(
+ text = stringResource(R.string.settings_deepl_description),
+ fontStyle = FontStyle.Italic,
+ fontSize = 12.sp
+ )
+ } else {
+ Text(
+ text = stringResource(R.string.settings_libretranslate_description),
+ fontStyle = FontStyle.Italic,
+ fontSize = 12.sp
+ )
+ }
+
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Switch(checked = localisePoints.value, onCheckedChange = {
+ localisePoints.value = it
+ sharedPreference.edit().putBoolean("localisePoints", it).apply()
+ })
+ }
+ if (localisePoints.value) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .selectable(
+ selected = deepLSelected.value,
+ onClick = { deepLSelected.value = true },
+ role = Role.RadioButton
+ )
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = (deepLSelected.value),
+ onClick = null // null recommended for accessibility with screenreaders
+ )
+ Text(
+ text = stringResource(R.string.settings_deepl),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(start = 16.dp)
+ )
+ }
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .selectable(
+ selected = !deepLSelected.value,
+ onClick = { deepLSelected.value = false },
+ role = Role.RadioButton
+ )
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = (!deepLSelected.value),
+ onClick = null // null recommended for accessibility with screenreaders
+ )
+ Text(
+ text = stringResource(R.string.settings_libretranslate),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(start = 16.dp)
+ )
+ }
+ if (deepLSelected.value) {
+ TextField(
+ value = deepLkey.value ?: "",
+ onValueChange = {
+ deepLkey.value = it
+ sharedPreference.edit().putString("deepLkey", it).apply()
+ },
+ label = { Text(text = stringResource(R.string.settings_deepl_key)) },
+ modifier = Modifier.fillMaxWidth(),
+ maxLines = 1
+ )
+ }
+ }
+ }
+ }
+ }
+ ElevatedCard(modifier = Modifier
+ .padding(4.dp)
+ .fillMaxWidth(),
+ onClick = {
+ navController.navigate("licenses")
+ }) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.open_source_licenses),
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/elements/ExpandableCard.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/elements/ExpandableCard.kt
new file mode 100644
index 0000000..d1dd16e
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/elements/ExpandableCard.kt
@@ -0,0 +1,149 @@
+package xyz.ptgms.tosdr.ui.views.elements
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Shapes
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat.startActivity
+import xyz.ptgms.tosdr.R
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ExpandableCard(
+ modifier: Modifier = Modifier,
+ title: String,
+ titleFontSize: TextUnit = MaterialTheme.typography.titleLarge.fontSize,
+ titleFontWeight: FontWeight = FontWeight.Bold,
+ description: Any,
+ descriptionFontSize: TextUnit = MaterialTheme.typography.bodyMedium.fontSize,
+ descriptionFontWeight: FontWeight = FontWeight.Normal,
+ descriptionMaxLines: Int = 10,
+ shape: CornerBasedShape = Shapes().medium,
+ padding: Dp = 12.dp
+) {
+ val expandedState = remember { mutableStateOf(false) }
+ val rotationState = animateFloatAsState(
+ targetValue = if (expandedState.value) 180f else 0f
+ )
+
+ ElevatedCard(
+ modifier = modifier
+ .fillMaxWidth()
+ .animateContentSize(
+ animationSpec = TweenSpec(
+ durationMillis = 300,
+ easing = LinearOutSlowInEasing
+ )
+ ),
+ shape = shape,
+ onClick = {
+ expandedState.value = !expandedState.value
+ }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(padding)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier.weight(6f),
+ text = title,
+ fontSize = titleFontSize,
+ fontWeight = titleFontWeight,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ IconButton(
+ modifier = Modifier
+ .alpha(0.5f)
+ .weight(1f)
+ .rotate(rotationState.value),
+ onClick = {
+ expandedState.value = !expandedState.value
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = stringResource(R.string.drop_down_arrow)
+ )
+ }
+ }
+ if (expandedState.value) {
+ Divider()
+ if (description is String) {
+ Text(
+ modifier = Modifier.padding(top = 8.dp),
+ text = description,
+ fontSize = descriptionFontSize,
+ fontWeight = descriptionFontWeight,
+ maxLines = descriptionMaxLines,
+ overflow = TextOverflow.Ellipsis
+ )
+ } else if (description is TextWithSource) {
+ val context = LocalContext.current
+ Column {
+ Text(
+ modifier = Modifier.padding(top = 8.dp),
+ text = description.text,
+ fontSize = descriptionFontSize,
+ fontWeight = descriptionFontWeight,
+ maxLines = descriptionMaxLines,
+ overflow = TextOverflow.Ellipsis
+ )
+ Button(onClick = {
+ val browserIntent =
+ Intent(Intent.ACTION_VIEW, Uri.parse("https://tosdr.org/legal"))
+ startActivity(context, browserIntent, null)
+ }, modifier = Modifier
+ .padding(top = 8.dp)
+ .fillMaxWidth()) {
+ Text(text = description.buttonText?: stringResource(id = R.string.visit_source))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+data class TextWithSource(
+ val text: String,
+ val source: String,
+ val buttonText: String?
+)
\ No newline at end of file
diff --git a/app/src/main/java/xyz/ptgms/tosdr/ui/views/elements/ReportCard.kt b/app/src/main/java/xyz/ptgms/tosdr/ui/views/elements/ReportCard.kt
new file mode 100644
index 0000000..1efa390
--- /dev/null
+++ b/app/src/main/java/xyz/ptgms/tosdr/ui/views/elements/ReportCard.kt
@@ -0,0 +1,188 @@
+package xyz.ptgms.tosdr.ui.views.elements
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Shapes
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat.startActivity
+import xyz.ptgms.tosdr.R
+import xyz.ptgms.tosdr.tools.data.GradeToHumanReadable.gradeBackground
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ReportCard(
+ modifier: Modifier = Modifier,
+ title: String,
+ titleFontSize: TextUnit = MaterialTheme.typography.titleMedium.fontSize,
+ titleFontWeight: FontWeight = FontWeight.Bold,
+ description: String,
+ quote: String,
+ link: String,
+ type: String? = null,
+ translation: Boolean = true,
+ descriptionFontSize: TextUnit = MaterialTheme.typography.bodyMedium.fontSize,
+ descriptionFontWeight: FontWeight = FontWeight.Normal,
+ descriptionMaxLines: Int = 10,
+ shape: CornerBasedShape = Shapes().medium,
+ padding: Dp = 12.dp
+) {
+ val expandedState = remember { mutableStateOf(false) }
+ val rotationState = animateFloatAsState(
+ targetValue = if (expandedState.value) 180f else 0f
+ )
+
+ ElevatedCard(
+ modifier = modifier
+ .fillMaxWidth()
+ .animateContentSize(
+ animationSpec = TweenSpec(
+ durationMillis = 300,
+ easing = LinearOutSlowInEasing
+ )
+ ),
+ shape = shape,
+ onClick = {
+ expandedState.value = !expandedState.value
+ }
+ ) {
+ Column {
+ val context = LocalContext.current
+ if (!translation)
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(padding)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ GetIcon(type?:"")
+ if ((type ?: "") != "") {
+ Spacer(modifier = Modifier.padding(4.dp))
+ }
+ Text(
+ modifier = Modifier.weight(6f),
+ text = title,
+ fontSize = titleFontSize,
+ fontWeight = titleFontWeight,
+ maxLines = 7,
+ overflow = TextOverflow.Ellipsis
+ )
+ IconButton(
+ modifier = Modifier
+ .alpha(0.5f)
+ .weight(1f)
+ .rotate(rotationState.value),
+ onClick = {
+ expandedState.value = !expandedState.value
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = stringResource(id = R.string.drop_down_arrow)
+ )
+ }
+ }
+ if (expandedState.value) {
+ Divider(modifier = Modifier.padding(vertical = 8.dp))
+ if (description.isNotEmpty() && description != "Generated through the annotate view") {
+ Text(
+ modifier = Modifier,
+ text = description,
+ fontSize = descriptionFontSize,
+ fontWeight = descriptionFontWeight,
+ maxLines = descriptionMaxLines,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ if (quote.isNotEmpty()) {
+ Text(
+ modifier = Modifier.padding(top = 8.dp),
+ text = stringResource(R.string.quote_from_policies),
+ fontSize = descriptionFontSize,
+ fontWeight = FontWeight.Bold,
+ maxLines = descriptionMaxLines,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ modifier = Modifier.padding(top = 8.dp),
+ text = "\"$quote\"",
+ fontSize = descriptionFontSize,
+ fontWeight = descriptionFontWeight,
+ maxLines = descriptionMaxLines,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ if (link.isNotEmpty() && link.startsWith("http")) {
+ Button(
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .fillMaxWidth(),
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
+ startActivity(context, intent, null)
+ }
+ ) {
+ Text(text = stringResource(id = R.string.visit_source))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun GetIcon(type: String) {
+ return when (type) {
+ "good" -> Icon(painter = painterResource(id = R.drawable.good), "Good",
+ tint = gradeBackground("A"), modifier = Modifier.size(36.dp))
+ "neutral" -> Icon(painter = painterResource(id = R.drawable.neutral), "Neutral",
+ tint = Color.LightGray, modifier = Modifier.size(36.dp))
+ "bad" -> Icon(painter = painterResource(id = R.drawable.bad), "Bad",
+ tint = gradeBackground("C"), modifier = Modifier.size(36.dp))
+ "blocker" -> Icon(painter = painterResource(id = R.drawable.blocker), "Blocker",
+ tint = gradeBackground("E"), modifier = Modifier.size(36.dp))
+ else -> Spacer(modifier = Modifier.padding(0.dp))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-mdpi/app_icon.png b/app/src/main/res/drawable-mdpi/app_icon.png
new file mode 100644
index 0000000..2a66f9a
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/app_icon.png differ
diff --git a/app/src/main/res/drawable/bad.xml b/app/src/main/res/drawable/bad.xml
new file mode 100644
index 0000000..1ef781d
--- /dev/null
+++ b/app/src/main/res/drawable/bad.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/blocker.xml b/app/src/main/res/drawable/blocker.xml
new file mode 100644
index 0000000..223c6e7
--- /dev/null
+++ b/app/src/main/res/drawable/blocker.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/good.xml b/app/src/main/res/drawable/good.xml
new file mode 100644
index 0000000..f132ce8
--- /dev/null
+++ b/app/src/main/res/drawable/good.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..ce830f7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..cd3c1eb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/neutral.xml b/app/src/main/res/drawable/neutral.xml
new file mode 100644
index 0000000..ae3da20
--- /dev/null
+++ b/app/src/main/res/drawable/neutral.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/round_backspace_24.xml b/app/src/main/res/drawable/round_backspace_24.xml
new file mode 100644
index 0000000..8b38f9e
--- /dev/null
+++ b/app/src/main/res/drawable/round_backspace_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..50ec886
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..50ec886
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..b8a99e8
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b1e665e
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..47b867b
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..c4bb4c2
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dd42f57
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..582cac7
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..be359a4
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..93e22b0
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a64bc6e
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1c27bf7
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..65be9c0
--- /dev/null
+++ b/app/src/main/res/values-ar/strings.xml
@@ -0,0 +1,82 @@
+
+
+
+ زيارة المصدر
+ زيارة الموقع
+ جاري التحميل…
+ تم!
+ سهم عرض التفاصيل
+ اقتباس من السياسات:
+ تفريغ
+ القائمة
+ عودة
+
+ شكرا لتبرعك!
+ تم إلغاء الشراء
+ فشل الشراء
+ حدث خطأ ما، يرجى المحاولة مرة أخرى لاحقاً.
+ التبرعات ليست مطلوبة، لكنها مقدّرة كثيرا.
+ معلومات
+ حيث أن التطبيق يستخدم فقط البيانات المقدمة عن طريق ToS;DR، أتعهد بالتبرع بنسبة 50 في المائة من جميع التبرعات لمطوري API.
+ شكرا لتبرعك!
+ شكرا لتبرعك الكبير!
+ شكرا لتبرعك الضخم!
+ تبرّع
+
+ الإعدادت
+ إعدادات البحث
+ إخفاء المواقع بدون درجة
+ إخفاء المواقع التي لم يتم مراجعتها بشكل شامل
+ الاعدادات الأقليمية
+ ملاحظة: قد تكون غير دقيقة وبطيئة بشكل لا يصدق.\nغير مدعوم: العربية
+ مفتاح API DeepL
+ نقاط التوطين في الخدمات
+ ملاحظة: يمكن أن يكون غير دقيق للغاية\nقد يكون غير متصل
+
+ "تراخيص المصدر المفتوح "
+ حول التطبيق
+ حول ToS;DR
+ ToS;DR للأندرويد هو عميل لموقع ويب وملحقات شعبية تسمى \"ToS;DR\"
+ حول هذا التطبيق
+ تم برمجة هذا التطبيق من قبل صناعات ptgms ، وهو مفتوح المصدر بالكامل.
+ لمشاهدة قائمة المساهمين للموقع على الشبكة والمكانة الإضافية، يرجى زيارة الموقع من الزر أدناه.
+
+ البحث
+ أدخل اسم الموقع
+ جوجل، فيسبوك، …
+
+ تم مراجعة هذه الخدمة!
+ وحصلت هذه الخدمة على قدر من الاستعراضات جيد ومن ثم يمكن إظهار درجة دقيقة.
+ هذه الخدمة غير مراجعة!
+ ليس هناك ما يكفي من الاستعراضات والحالات للحصول على درجة دقيقة.
+ عناوين URL المعروفة لـ %s
+ أيقونة ل %s
+ مُراجَع
+ لم يتم مراجعته
+ الدرجة: %s
+ عناوين URL المحفوظة: %s
+ الحالات: %s
+ عرض في ToS;DR
+
+ مرحبا بكم في ToS;DR!
+ ما هذا؟
+ يتيح لك هذا التطبيق عرض نسخة قصيرة ومختصرة من شروط الخدمة وسياسة الخصوصية لمواقع محددة.
+ طريقة الاستخدام
+ يمكنك إما \"مشاركة\" موقع ويب لهذا التطبيق أو استخدام وظيفة البحث في التطبيق.
+ هل بعجبك ما تراه؟
+ تأكد من التحقق من قسم \"حول التطبيق\" و \"التبرع\".
+ إخلاء المسؤولية القانونية
+
+ فتح صفحة ToS;DR ؟
+ الموقع: %s
+ لم يتم العثور على أي نتائج
+ لا ينبغي اعتبار أي شيء هنا مشورة قانونية، ونحن نعرب عن رأينا بدون ضمان، ولا نؤيد أي خدمة بأي شكل من الأشكال. يرجى الرجوع إلى محام مؤهل للحصول على المشورة القانونية. قراءة ToS;DR ليست بأي حال من الأحوال بديلاً لقراءة المصطلحات التي تلتزم بها.
+
+ التصفح
+ الصفحة الرئيسية
+
+ الرجاء إرسال تقرير عن الأعطال حتى يمكنني إصلاح هذه المشكلة. أنا أقدر خصوصيتك، لا تتردد في معاينة وتحرير الأشياء في السجل قبل إرسالها.
+ أوه لا، تعطل التطبيق!
+ أضف تعليقاً, خطوات لاستنساخ أو، إذا كان ذلك ممكناً, الموقع الذي فتحته.
+ خطأ! هل أنت متصل بالإنترنت؟
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000..b520b3d
--- /dev/null
+++ b/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,72 @@
+
+
+ Quelle anzeigen
+ Website anzeigen
+ Laden…
+ Fertig!
+ Pfeil nach unten
+ Zitat aus den Richtlinien:
+ Danke für deine Spende!
+ Kauf abgebrochen
+ Kauf fehlgeschlagen
+ Es ist ein Fehler aufgetreten, bitte versuche es später noch einmal.
+ Spenden sind nicht erforderlich, werden aber sehr geschätzt.
+ Info
+ Da die App nur Daten verwendet, die von ToS;DR zur Verfügung gestellt werden, verspreche ich, dass 50% aller Spenden an die Entwickler der API gespendet werden.
+ Danke für deine Spende!
+ Danke für deine große Spende!
+ Danke für deine gigantische Spende!
+ Spenden
+ Einstellungen
+ Such-Einstellungen
+ Verstecke Seiten ohne Note
+ Verstecke Seiten, die nicht ausführlich bewertet wurden.
+ Über
+ Über ToS;DR
+ ToS;DR für Android ist ein Client für die beliebte Website und Erweiterung \"ToS;DR\".
+ Über diese App
+ Diese App wurde von ptgms Industries programmiert. Sie ist vollständig Open-Source.
+ Um die Liste der Mitwirkenden an der Website und der Erweiterung einzusehen, besuche bitte die Website über die Schaltfläche unten.
+ Suche
+ Website-Namen eingeben
+ Google, Facebook, …
+ Dieser Dienst wurde überprüft!
+ Dieser Dienst hat eine große Anzahl von Bewertungen erhalten, so dass eine korrekte Note angegeben werden kann.
+ Dieser Dienst wurde nicht überprüft!
+ Es gibt nicht genügend Bewertungen und Fälle, um eine genaue Note zu erhalten.
+ Bekannte URLs für %s
+ Symbol für %s
+ Überprüft
+ Nicht überprüft
+ Note: %s
+ Gespeicherte URLs: %s
+ Willkommen bei ToS;DR!
+ Was ist das?
+ Mit dieser App kannst du eine kurze und übersichtliche Version der Nutzungsbedingungen und der Datenschutzrichtlinie einer bestimmten Website anzeigen lassen.
+ Verwendung
+ Du kannst entweder eine Website für diese App \"freigeben\" oder die Suchfunktion in der App verwenden.
+ Dir gefällst, was du siehst?
+ Stelle sicher die \"Über\" und \"Spenden\" Seite aufzurufen.
+ Rechtlicher Hinweis
+ ToS;DR Seite öffnen?
+ Website: %s
+ Keine Resultate gefunden
+ Nichts in dieser App sollte als Rechtsberatung angesehen werden. Wir geben unsere Meinung ohne jegliche Garantie ab und unterstützen keine Dienstleistung in irgendeiner Weise. Bitte wende dich für eine Rechtsberatung an einen qualifizierten Anwalt. ToS;DR ersetzt in keiner Weise das Lesen der vollständigen Bedingungen, an der du gebunden bist.
+ Navigation
+ Home
+ Leeren
+ Bitte sende einen Absturzbericht damit ich das Problem beheben kann. Ich respektiere deine Privatsphäre, schaue dir gerne den Bericht an und schau, was abgeschickt wird oder entferne deine Daten bevor du den Bericht abschickst.
+ Oh nein, die App ist abgestürtzt!
+ Füge einen Kommentar, Schritte zur Reproduktion oder, falls zutreffend, den Seitennamen ein.
+ Lokalisierung
+ Info: Ist eventuell inakkurat und extremst langsam.\nInkompatibel: Arabisch
+ DeepL API-Schlüssel
+ Fälle: %s
+ Auf ToS;DR ansehen
+ Menü
+ Zurück
+ Lokalisiere Punkte in Websiten
+ Info: Kann sehr inakkurat sein.\nIst eventuell offline.
+ Fehler! Bist du mit dem Internet verbunden?
+ Open-Source Lizensen
+
\ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000..c027e2c
--- /dev/null
+++ b/app/src/main/res/values-nl/strings.xml
@@ -0,0 +1,82 @@
+
+
+
+ Bezoek Bron
+ Bezoek de Website
+ Bezig met laden…
+ Gereed!
+ Naar beneden Pijl
+ Citaat uit beleid:
+ Wissen
+ Menu
+ Terug
+
+ Hartelijk dank voor uw donatie!
+ Aankoop geannuleerd
+ Aankoop mislukt
+ Er is iets fout gegaan. Probeer het later opnieuw.
+ Donaties zijn niet vereist, maar wel zeer gewaardeerd.
+ Info
+ De app maakt zorgvuldig gebruik van data door ToS;DR, ik beloof dat 50% van alle donaties zal worden gedoneerd aan de ontwikkelaars van de API.
+ Hartelijk dank voor de donatie!
+ Hartelijk dank voor de grote donatie!
+ Hartelijk dank voor de reusachtige donatie!
+ Doneren
+
+ Instellingen
+ Zoekinstellingen
+ Verberg websites zonder beoordeling
+ Sites die niet volledig zijn gereviewd verbergen
+ Lokalisatie
+ Let op: kan onnauwkeurig en ongelooflijk langzaam zijn.\nNiet ondersteund: Arabisch
+ DeepL API-sleutel
+ Lokaliseer punten in diensten
+ Let op: Kan extreem onnauwkeurig zijn\n Is mogelijk niet online
+
+ Over
+ Over ToS;DR
+ ToS;DR voor Android is een client voor de populaire Website en Extensie genaamd \"ToS;DR\"
+ Over deze app
+ Deze app was geprogrammeerd door Ptgms Industries. Het is volledig open source.
+ Om de lijst van bijdragers voor de Website en Extensie te zien, bezoek alstublieft de Website met de onderstaande knop.
+
+ Zoeken
+ Voer het adres van de website in
+ Google, Facebook, …
+
+ Deze Dienst is beoordeeld!
+ Deze service heeft een begrijpelijke hoeveelheid beoordelingen ontvangen en kan dus een nauwkeurige beoordeling geven.
+ Deze Dienst is nog niet beoordeeld!
+ Er zijn niet genoeg beoordelingen en gevallen om een nauwkeurige beoordeling te krijgen.
+ Bekende URL\'s voor %s
+ Pictogram voor %s
+ Beoordeeld
+ Niet beoordeeld
+ Beoordeling: %s
+ Opgeslagen URL\'s: %s
+ Gevallen: %s
+ Bekijk op ToS;DR
+
+ Welkom bij ToS;DR!
+ Wat is dit?
+ Met deze app kunt u een korte en ingetrokken versie bekijken van een specifieke websites gebruiksvoorwaarden en privacybeleid.
+ Gebruiksaanwijzing
+ U kunt een website naar deze app \"Delen\" of de zoekfunctie in de app gebruiken.
+ Vind je het leuk wat je ziet?
+ Bezoek het tabblad \"Over\" en \"Doneren\" een keer.
+ Wettelijk disclaimer
+
+ Open de ToS;DR Pagina?
+ Website: %s
+ Geen zoekresultaten gevonden
+ Wij geven onze mening zonder wettelijke garantie en staan op geen enkele manier achter een dienst. Raadpleeg alstublieft een gekwalificeerde advocaat voor juridisch advies. Lezen van ToS;DR is in geen geval een vervanging voor het lezen van de volledige termen waaraan u gebonden bent.
+
+ Navigatie
+ Startpagina
+
+ Stuur alstublieft een crashrapport zodat ik dit probleem kan verhelpen. Ik hecht waarde aan uw privacy, voel vrij om dingen te bekijken en te redigeren in het logboek voordat je ze verstuurt.
+ Oeps, de app is vastgelopen!
+ Voeg een opmerking toe, stappen om te reproduceren of, indien van toepassing, de site die je hebt geopend.
+ Er is iets fout gegaan. Bent u verbonden met het internet?
+ Opensourcelicenties
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3a09068
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..3a09068
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d243bd1
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,94 @@
+
+ ToS;DR
+
+
+ Visit Source
+ Visit Website
+ Loading…
+ Done!
+ Drop Down Arrow
+ Quote from policies:
+ Clear
+ Menu
+ Back
+
+
+ Thank you for your donation!
+ Purchase cancelled
+ Purchase failed
+ Something went wrong, please try again later.
+ Donations are not required, but greatly appreciated.
+ Info
+ As the App only uses Data thoughtfully provided by ToS;DR, I pledge that 50% of all Donations will be donated onto the developers of the API.
+ Thank you for your Donation!
+ Thank you for your big Donation!
+ Thank you for your huge Donation!
+ Donate
+
+
+ Settings
+ Search Settings
+ Hide sites with no grade
+ Hide sites that aren\'t comprehensively reviewed
+ Localisation
+ DeepL
+ Notice: May be inaccurate and incredibly slow.\nUnsupported: Arabic
+ DeepL API key
+ Localise points in services
+ LibreTranslate
+ Notice: Can be extremely inaccurate\nMight be offline
+ Open Source Licenses
+
+
+ About
+ About ToS;DR
+ ToS;DR for Android is an client for popular Website and Extension named \"ToS;DR\"
+ About this App
+ This app has been programmed by ptgms Industries. It is fully open-source.
+ To see the contributor list for the Website and Extension, please visit the Website with the Button below.
+
+ Search
+ Enter website name
+ Google, Facebook, …
+
+
+ This Service is reviewed!
+ This service received a comprehensible amount of reviews and thus an accurate grade can be shown.
+ This Service is unreviewed!
+ There are not enough reviews and cases to have an accurate grade.
+ Known URLs for %s
+ Icon for %s
+ Reviewed
+ Not Reviewed
+ Grade: %s
+ Saved URLs: %s
+ Cases: %s
+ View on ToS;DR
+
+
+ Welcome to ToS;DR!
+ What is this?
+ This app allows you to view a short and abridged version of a specific websites\' Terms of Service and Privacy Policy.
+ How to use
+ You can either \"Share\" a Website to this App or use the Search function in the app.
+ Like what you are seeing?
+ Make sure to check out the \"About\" and \"Donate\" tab.
+ Legal disclaimer
+
+
+ Open ToS;DR Page?
+ Website: %s
+ No results found
+ Nothing here should be considered legal advice. We express our opinion with no guarantee and we do not endorse any service in any way. Please refer to a qualified attorney for legal advice. Reading ToS;DR is in no way a replacement for reading the full terms to which you are bound.
+
+
+ Navigation
+ Home
+
+
+ Please send a crash report so I can fix this issue. I value your privacy, feel free to preview and redact things in the log before sending them.
+ Oh no, the App crashed!
+ Add a comment, steps to reproduce or, if applicable, the site you opened.
+ Error! Are you connected to the internet?
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..b43cdbf
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..6cebe05
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
new file mode 100644
index 0000000..b562b55
--- /dev/null
+++ b/app/src/main/res/xml/locales_config.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..4fc5d69
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,7 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '8.0.0-alpha09' apply false
+ id 'com.android.library' version '8.0.0-alpha09' apply false
+ id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
+ id "com.mikepenz.aboutlibraries.plugin" version "10.5.1"
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..7ee9b71
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..8aa9e47
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Nov 08 13:56:29 CET 2022
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/resources/google-play-badge.png b/resources/google-play-badge.png
new file mode 100644
index 0000000..9499b2d
Binary files /dev/null and b/resources/google-play-badge.png differ
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..f3640d3
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "ToS;DR"
+include ':app'