├── .idea
├── .name
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── kotlinc.xml
├── vcs.xml
├── migrations.xml
├── misc.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── appInsightsSettings.xml
├── runConfigurations.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── locales_config.xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_service_placeholder.xml
│ │ │ │ ├── ic_rounded_api_24.xml
│ │ │ │ ├── ic_rounded_block_24.xml
│ │ │ │ ├── ic_twitter.xml
│ │ │ │ ├── ic_rounded_warning_24.xml
│ │ │ │ ├── ic_rounded_shield_24.xml
│ │ │ │ ├── ic_github.xml
│ │ │ │ ├── logo_transparent.xml
│ │ │ │ ├── ic_rounded_open_in_browser_24.xml
│ │ │ │ ├── ic_rounded_content_copy_24.xml
│ │ │ │ ├── ic_rounded_info_24.xml
│ │ │ │ ├── ic_rounded_thumb_up_24.xml
│ │ │ │ ├── ic_rounded_thumb_down_24.xml
│ │ │ │ ├── ic_rounded_home_storage_24.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ ├── ic_mastodon.xml
│ │ │ │ ├── ic_rounded_format_quote_24.xml
│ │ │ │ ├── ic_rounded_school_24.xml
│ │ │ │ ├── ic_rounded_celebration_24.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── values-zh-rCN
│ │ │ │ └── strings.xml
│ │ │ ├── values-ja-rJP
│ │ │ │ └── strings.xml
│ │ │ ├── values-ru-rRU
│ │ │ │ └── strings.xml
│ │ │ └── values-pl-rPL
│ │ │ │ └── strings.xml
│ │ ├── java
│ │ │ └── xyz
│ │ │ │ └── ptgms
│ │ │ │ └── tosdr
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── ToSDRColorScheme.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── data
│ │ │ │ ├── room
│ │ │ │ │ ├── ServiceEntity.kt
│ │ │ │ │ ├── ServiceDao.kt
│ │ │ │ │ └── ToSDRDatabase.kt
│ │ │ │ ├── BillingManagerInterface.kt
│ │ │ │ └── DatabaseUpdater.kt
│ │ │ │ ├── components
│ │ │ │ ├── settings
│ │ │ │ │ ├── SettingsTitle.kt
│ │ │ │ │ ├── SettingsGroup.kt
│ │ │ │ │ └── SettingsRow.kt
│ │ │ │ ├── lists
│ │ │ │ │ └── RoundedLists.kt
│ │ │ │ └── points
│ │ │ │ │ ├── PointsGroup.kt
│ │ │ │ │ └── PointsRow.kt
│ │ │ │ ├── api
│ │ │ │ ├── ToSDRApi.kt
│ │ │ │ ├── models
│ │ │ │ │ ├── TeamModels.kt
│ │ │ │ │ └── Models.kt
│ │ │ │ ├── ToSDRRepository.kt
│ │ │ │ └── ApiClient.kt
│ │ │ │ ├── navigation
│ │ │ │ └── NavGraph.kt
│ │ │ │ ├── ShareActivity.kt
│ │ │ │ ├── screens
│ │ │ │ ├── about
│ │ │ │ │ ├── ServicesExplainedScreen.kt
│ │ │ │ │ ├── LibrariesScreen.kt
│ │ │ │ │ ├── GradesExplainedScreen.kt
│ │ │ │ │ ├── PointsExplainedScreen.kt
│ │ │ │ │ └── AboutScreen.kt
│ │ │ │ ├── PointView.kt
│ │ │ │ └── TeamScreen.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── viewmodels
│ │ │ │ └── ToSDRViewModel.kt
│ │ └── AndroidManifest.xml
│ ├── google
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── xyz
│ │ │ └── ptgms
│ │ │ └── tosdr
│ │ │ └── data
│ │ │ └── BillingManager.kt
│ ├── test
│ │ └── java
│ │ │ └── xyz
│ │ │ └── ptgms
│ │ │ └── tosdr
│ │ │ └── ExampleUnitTest.kt
│ ├── foss
│ │ └── java
│ │ │ └── xyz
│ │ │ └── ptgms
│ │ │ └── tosdr
│ │ │ └── data
│ │ │ └── BillingManager.kt
│ └── androidTest
│ │ └── java
│ │ └── xyz
│ │ └── ptgms
│ │ └── tosdr
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
├── schemas
│ └── xyz.ptgms.tosdr.data.room.ToSDRDatabase
│ │ └── 1.json
└── build.gradle.kts
├── metadata
└── en-US
│ ├── short_description.txt
│ ├── changelogs
│ └── 35.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── hero.png
│ │ ├── learn.png
│ │ ├── ondevice.png
│ │ └── settings.png
│ └── full_description.txt
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── settings.gradle.kts
├── LICENSE
├── gradle.properties
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | ToS;DR
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/metadata/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Short summaries on Terms of Conditions
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/35.txt:
--------------------------------------------------------------------------------
1 | Added variant for FOSS builds
2 | Fixed bug in display of points
--------------------------------------------------------------------------------
/metadata/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/metadata/en-US/images/icon.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/metadata/en-US/images/phoneScreenshots/hero.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/learn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/metadata/en-US/images/phoneScreenshots/learn.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/ondevice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/metadata/en-US/images/phoneScreenshots/ondevice.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/metadata/en-US/images/phoneScreenshots/settings.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tosdr/tosdr-android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/google/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jan 23 20:23:11 CET 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /app/release
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/test/java/xyz/ptgms/tosdr/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/data/room/ServiceEntity.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.data.room
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Keep
8 | @Entity(tableName = "services")
9 | data class ServiceEntity(
10 | @PrimaryKey val id: Int,
11 | val name: String,
12 | val url: String,
13 | val rating: String,
14 | val lastUpdate: Long = System.currentTimeMillis()
15 | )
--------------------------------------------------------------------------------
/metadata/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | "I agree to the Terms and Conditions of this Website".
2 | The biggest lie of the internet. No one wants to spend an hour reading a wall of text. So we click "Accept" and move on, without knowing what we just agreed.
3 | Let's make that a thing of the past!
4 | With this app, you can search for Websites or Share an Website into it to get a Report of what is in the policies and it displays a Grade on Privacy, so you will actually know what you are agreeing on!
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_service_placeholder.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/components/settings/SettingsTitle.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.components.settings
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 |
10 | @Composable
11 | fun SettingsTitle(text: String) {
12 | Text(
13 | text = text,
14 | style = MaterialTheme.typography.titleMedium,
15 | modifier = Modifier.padding(start = 8.dp, top = 12.dp, bottom = 4.dp)
16 | )
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/ui/theme/ToSDRColorScheme.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | object ToSDRColorScheme {
6 | val gradeA = Color(0xFF2ECC71)
7 | val gradeB = Color(0xFF27AE60)
8 | val gradeC = Color(0xFFF1C40F)
9 | val gradeD = Color(0xFFE67E22)
10 | val gradeE = Color(0xFFE74C3C)
11 | val gradeNA = Color(0xFF8F8F94)
12 | }
13 |
14 | object BadgeColors {
15 | val red = Color(0xFFFF3B30)
16 | val green = Color(0xFF34C759)
17 | val blue = Color(0xFF007AFF)
18 | val orange = Color(0xFFFF9500)
19 | val gray = Color(0xFF8F8F94)
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/components/lists/RoundedLists.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.components.lists
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Shape
6 | import androidx.compose.ui.unit.dp
7 |
8 | @Composable
9 | fun getAdaptiveRoundedCornerShape(index: Int, lastIndex: Int): Shape {
10 | return RoundedCornerShape(
11 | topStart = if (index == 0) 24.dp else 8.dp,
12 | topEnd = if (index == 0) 24.dp else 8.dp,
13 | bottomStart = if (index == lastIndex) 24.dp else 8.dp,
14 | bottomEnd = if (index == lastIndex) 24.dp else 8.dp
15 | )
16 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | maven("https://jitpack.io")
13 | }
14 | }
15 | dependencyResolutionManagement {
16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
17 | repositories {
18 | google()
19 | mavenCentral()
20 | maven("https://jitpack.io")
21 | }
22 | }
23 |
24 | rootProject.name = "ToS;DR"
25 | include(":app")
26 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/api/ToSDRApi.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.api
2 |
3 | import retrofit2.http.GET
4 | import retrofit2.http.Header
5 | import retrofit2.http.Query
6 | import xyz.ptgms.tosdr.api.models.AppDbEntry
7 | import xyz.ptgms.tosdr.api.models.SearchResponse
8 | import xyz.ptgms.tosdr.api.models.ServiceDetail
9 |
10 | interface ToSDRApi {
11 | @GET("search/v5")
12 | suspend fun searchServices(@Query("query") query: String): SearchResponse
13 |
14 | @GET("service/v3")
15 | suspend fun getServiceDetails(
16 | @Query("id") id: Int,
17 | @Query("lang") lang: String? = null
18 | ): ServiceDetail
19 |
20 | @GET("appdb/version/v2")
21 | suspend fun getAppDb(@Header("apikey") apiKey: String): List
22 | }
--------------------------------------------------------------------------------
/app/src/foss/java/xyz/ptgms/tosdr/data/BillingManager.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.data
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.StateFlow
7 |
8 | class BillingManager(context: Context) : BillingManagerInterface {
9 | private val _purchaseState = MutableStateFlow(
10 | BillingManagerInterface.PurchaseState.IDLE
11 | )
12 | override val purchaseState: StateFlow = _purchaseState
13 | override fun launchBillingFlow(
14 | activity: Activity,
15 | product: BillingManagerInterface.ProductInfo
16 | ) {
17 | // No-op in FOSS version
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/data/BillingManagerInterface.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.data
2 |
3 | import kotlinx.coroutines.flow.StateFlow
4 | import android.app.Activity
5 |
6 | interface BillingManagerInterface {
7 | val purchaseState: StateFlow
8 |
9 | fun launchBillingFlow(activity: Activity, product: ProductInfo)
10 |
11 | sealed class PurchaseState {
12 | object IDLE : PurchaseState()
13 | data class ProductsAvailable(val products: List) : PurchaseState()
14 | object PurchaseSuccessful : PurchaseState()
15 | data class Error(val message: String) : PurchaseState()
16 | }
17 |
18 | data class ProductInfo(
19 | val id: String,
20 | val name: String,
21 | val price: String
22 | )
23 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/xyz/ptgms/tosdr/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("xyz.ptgms.tosdr", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/navigation/NavGraph.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.navigation
2 |
3 | sealed class Screen(val route: String) {
4 | object Search : Screen("search")
5 | object About : Screen("about")
6 | object Donate : Screen("donate")
7 | object Team : Screen("team")
8 | object Settings : Screen("settings")
9 | object ServiceDetails : Screen("service/{serviceId}") {
10 | fun createRoute(serviceId: Int) = "service/$serviceId"
11 | }
12 | object PointView : Screen("point/{pointId}") {
13 | fun createRoute(pointId: Int) = "point/$pointId"
14 | }
15 | object GradesExplained : Screen("about/grades")
16 | object PointsExplained : Screen("about/points")
17 | object ServicesExplained : Screen("about/services")
18 | object Libraries : Screen("about/libraries")
19 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_api_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/api/models/TeamModels.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.api.models
2 |
3 | import androidx.annotation.Keep
4 |
5 | @Keep
6 | data class TeamMemberLinks(
7 | val email: String?,
8 | val github: String?,
9 | val twitter: String?,
10 | val website: String?,
11 | val mastodon: String?
12 | ) {
13 | val isEmpty: Boolean
14 | get() = email == null && github == null && twitter == null && website == null && mastodon == null
15 | }
16 |
17 | @Keep
18 | data class TeamMember(
19 | val photo: String,
20 | val name: String,
21 | val title: String,
22 | val description: String,
23 | val links: TeamMemberLinks
24 | )
25 |
26 | @Keep
27 | data class Team(
28 | val founders: List,
29 | val current: List,
30 | val past: List
31 | )
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/data/room/ServiceDao.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.data.room
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.*
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | @Dao
8 | @Keep
9 | interface ServiceDao {
10 | @Query("SELECT * FROM services WHERE name LIKE :query OR url LIKE :query")
11 | fun searchServices(query: String): Flow>
12 |
13 | @Query("SELECT MAX(lastUpdate) FROM services")
14 | suspend fun getLastUpdateTime(): Long?
15 |
16 | @Query("SELECT COUNT(*) FROM services")
17 | suspend fun getCount(): Int
18 |
19 | @Transaction
20 | @Insert(onConflict = OnConflictStrategy.REPLACE)
21 | suspend fun insertAll(services: List)
22 |
23 | @Query("DELETE FROM services")
24 | suspend fun clearAll()
25 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_block_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_twitter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/components/settings/SettingsGroup.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.components.settings
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material3.Surface
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 |
12 | @Composable
13 | fun SettingsGroup(
14 | modifier: Modifier = Modifier,
15 | content: @Composable ColumnScope.() -> Unit
16 | ) {
17 | Surface(
18 | shape = RoundedCornerShape(24.dp),
19 | modifier = modifier
20 | ) {
21 | Column(
22 | verticalArrangement = Arrangement.spacedBy(2.dp),
23 | content = content
24 | )
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_warning_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/data/room/ToSDRDatabase.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.data.room
2 |
3 | import android.content.Context
4 | import androidx.annotation.Keep
5 | import androidx.room.Database
6 | import androidx.room.Room
7 | import androidx.room.RoomDatabase
8 |
9 | @Keep
10 | @Database(entities = [ServiceEntity::class], version = 1)
11 | abstract class ToSDRDatabase : RoomDatabase() {
12 | abstract fun serviceDao(): ServiceDao
13 |
14 | companion object {
15 | @Volatile
16 | private var INSTANCE: ToSDRDatabase? = null
17 |
18 | fun getDatabase(context: Context): ToSDRDatabase {
19 | return INSTANCE ?: synchronized(this) {
20 | Room.databaseBuilder(
21 | context.applicationContext,
22 | ToSDRDatabase::class.java,
23 | "tosdr_database"
24 | ).build().also { INSTANCE = it }
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_shield_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_github.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/logo_transparent.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_open_in_browser_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_content_copy_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Terms of Service; Didn’t Read
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_info_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_thumb_up_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_thumb_down_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_home_storage_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_mastodon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/api/ToSDRRepository.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.api
2 |
3 | import xyz.ptgms.tosdr.api.models.AppDbEntry
4 | import xyz.ptgms.tosdr.api.models.ServiceBasic
5 | import xyz.ptgms.tosdr.api.models.ServiceDetail
6 |
7 | class ToSDRRepository {
8 | private var api = ApiClient.api
9 |
10 | init {
11 | ApiClient.addBaseUrlChangeListener {
12 | api = ApiClient.api
13 | }
14 | }
15 |
16 | suspend fun searchServices(query: String): Result> = try {
17 | Result.success(api.searchServices(query).services)
18 | } catch (e: Exception) {
19 | Result.failure(e)
20 | }
21 |
22 | suspend fun getServiceDetails(id: Int): Result = try {
23 | var lang: String?
24 | val deviceLanguage = java.util.Locale.getDefault().language
25 | lang = when (deviceLanguage) {
26 | "de", "nl", "es", "fr" -> deviceLanguage
27 | else -> null
28 | }
29 | Result.success(api.getServiceDetails(id, lang))
30 | } catch (e: Exception) {
31 | Result.failure(e)
32 | }
33 |
34 | suspend fun getAppDb(apiKey: String): Result> = try {
35 | Result.success(api.getAppDb(apiKey))
36 | } catch (e: Exception) {
37 | Result.failure(e)
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_format_quote_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_school_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rounded_celebration_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/api/ApiClient.kt:
--------------------------------------------------------------------------------
1 | import okhttp3.OkHttpClient
2 | import retrofit2.Retrofit
3 | import retrofit2.converter.gson.GsonConverterFactory
4 | import xyz.ptgms.tosdr.api.ToSDRApi
5 | import android.content.Context
6 |
7 | object ApiClient {
8 | var defaultUrl = "https://api.tosdr.org/"
9 |
10 | private var currentBaseUrl = defaultUrl
11 | private val listeners = mutableListOf<() -> Unit>()
12 |
13 | fun addBaseUrlChangeListener(listener: () -> Unit) {
14 | listeners.add(listener)
15 | }
16 |
17 | fun removeBaseUrlChangeListener(listener: () -> Unit) {
18 | listeners.remove(listener)
19 | }
20 |
21 | fun initialize(context: Context) {
22 | val prefs = context.getSharedPreferences("tosdr_prefs", Context.MODE_PRIVATE)
23 | val savedUrl = prefs.getString("base_url", defaultUrl) ?: defaultUrl
24 | updateBaseUrl(savedUrl)
25 | }
26 |
27 | fun updateBaseUrl(newUrl: String) {
28 | currentBaseUrl = if (newUrl.endsWith("/")) newUrl else "$newUrl/"
29 | retrofit = createRetrofit()
30 | api = retrofit.create(ToSDRApi::class.java)
31 | listeners.forEach { it() }
32 | }
33 |
34 | private fun createRetrofit(): Retrofit {
35 | return Retrofit.Builder()
36 | .baseUrl(currentBaseUrl)
37 | .addConverterFactory(GsonConverterFactory.create())
38 | .client(OkHttpClient.Builder().build())
39 | .build()
40 | }
41 |
42 | private var retrofit = createRetrofit()
43 | var api: ToSDRApi = retrofit.create(ToSDRApi::class.java)
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/data/DatabaseUpdater.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.data
2 |
3 | import androidx.annotation.Keep
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.flow
6 | import xyz.ptgms.tosdr.data.room.ServiceEntity
7 | import xyz.ptgms.tosdr.data.room.ToSDRDatabase
8 | import xyz.ptgms.tosdr.viewmodels.ToSDRViewModel
9 |
10 | object DatabaseUpdater {
11 | private const val SEVEN_DAYS_IN_MILLIS = 7 * 24 * 60 * 60 * 1000L
12 | private const val API_KEY = "congrats on getting the key :P"
13 |
14 | suspend fun shouldUpdate(database: ToSDRDatabase): Boolean {
15 | val lastUpdate = database.serviceDao().getLastUpdateTime() ?: 0L
16 | val currentTime = System.currentTimeMillis()
17 | return lastUpdate == 0L || currentTime - lastUpdate >= SEVEN_DAYS_IN_MILLIS
18 | }
19 |
20 | @Keep
21 | fun updateDatabase(
22 | viewModel: ToSDRViewModel,
23 | database: ToSDRDatabase
24 | ): Flow>> = flow {
25 | viewModel.getAppDb(API_KEY).collect { result ->
26 | result.onSuccess { entries ->
27 | val serviceEntities = entries.map { entry ->
28 | ServiceEntity(
29 | id = entry.id,
30 | name = entry.name,
31 | url = entry.url,
32 | rating = entry.rating
33 | )
34 | }
35 | database.serviceDao().insertAll(serviceEntities)
36 | emit(Result.success(serviceEntities))
37 | }.onFailure {
38 | emit(Result.failure(it))
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/components/points/PointsGroup.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.components.points
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Surface
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 | import xyz.ptgms.tosdr.api.models.Point
15 |
16 | @Composable
17 | fun PointsGroup(
18 | modifier: Modifier = Modifier,
19 | title: String,
20 | points: List,
21 | onPointClick: (Point) -> Unit = {},
22 | original: Boolean = false
23 | ) {
24 | Column(modifier = modifier) {
25 | Text(
26 | text = title,
27 | style = MaterialTheme.typography.titleMedium,
28 | modifier = Modifier.padding(start = 8.dp, top = 12.dp, bottom = 4.dp)
29 | )
30 | Surface(
31 | shape = RoundedCornerShape(24.dp),
32 | modifier = Modifier.fillMaxWidth()
33 | ) {
34 | Column(
35 | verticalArrangement = Arrangement.spacedBy(2.dp),
36 | ) {
37 | points.forEach { point ->
38 | PointsRow(
39 | point = point,
40 | onClick = { onPointClick(point) },
41 | original = original
42 | )
43 | }
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/api/models/Models.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.api.models
2 |
3 | import androidx.annotation.Keep
4 |
5 | @Keep
6 | data class SearchResponse(
7 | val services: List
8 | )
9 |
10 | @Keep
11 | data class ServiceBasic(
12 | val id: Int,
13 | val is_comprehensively_reviewed: Boolean,
14 | val urls: List,
15 | val name: String,
16 | val updated_at: String,
17 | val created_at: String,
18 | val slug: String,
19 | val rating: String
20 | )
21 |
22 | @Keep
23 | data class ServiceDetail(
24 | val id: Int,
25 | val is_comprehensively_reviewed: Boolean,
26 | val name: String,
27 | val updated_at: String,
28 | val created_at: String,
29 | val slug: String,
30 | val rating: String,
31 | val urls: List,
32 | val image: String,
33 | val documents: List,
34 | val points: List
35 | )
36 |
37 | @Keep
38 | data class Document(
39 | val id: Int,
40 | val name: String,
41 | val url: String,
42 | val updated_at: String,
43 | val created_at: String
44 | )
45 |
46 | @Keep
47 | data class Point(
48 | val id: Int,
49 | val title: String,
50 | val source: String,
51 | val status: String,
52 | val analysis: String,
53 | val case: Case,
54 | val document_id: Int,
55 | val updated_at: String,
56 | val created_at: String
57 | )
58 |
59 | @Keep
60 | data class Case(
61 | val id: Int,
62 | val weight: Int,
63 | val title: String,
64 | val localized_title: String?,
65 | val description: String,
66 | val updated_at: String,
67 | val created_at: String,
68 | val topic_id: Int,
69 | val classification: String
70 | )
71 |
72 | @Keep
73 | data class AppDbEntry(
74 | val id: Int,
75 | val name: String,
76 | val url: String,
77 | val rating: String
78 | )
--------------------------------------------------------------------------------
/app/schemas/xyz.ptgms.tosdr.data.room.ToSDRDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "fe6f8e047205ca74fadf4f4e7d3525b0",
6 | "entities": [
7 | {
8 | "tableName": "services",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `rating` TEXT NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "name",
19 | "columnName": "name",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "url",
25 | "columnName": "url",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "rating",
31 | "columnName": "rating",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "lastUpdate",
37 | "columnName": "lastUpdate",
38 | "affinity": "INTEGER",
39 | "notNull": true
40 | }
41 | ],
42 | "primaryKey": {
43 | "autoGenerate": false,
44 | "columnNames": [
45 | "id"
46 | ]
47 | },
48 | "indices": [],
49 | "foreignKeys": []
50 | }
51 | ],
52 | "views": [],
53 | "setupQueries": [
54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe6f8e047205ca74fadf4f4e7d3525b0')"
56 | ]
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F),
32 | */
33 | )
34 |
35 | @Composable
36 | fun ToSDRTheme(
37 | darkTheme: Boolean = isSystemInDarkTheme(),
38 | // Dynamic color is available on Android 12+
39 | dynamicColor: Boolean = true,
40 | content: @Composable () -> Unit
41 | ) {
42 | val colorScheme = when {
43 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
44 | val context = LocalContext.current
45 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46 | }
47 |
48 | darkTheme -> DarkColorScheme
49 | else -> LightColorScheme
50 | }
51 |
52 | MaterialTheme(
53 | colorScheme = colorScheme,
54 | typography = Typography,
55 | content = content
56 | )
57 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/components/settings/SettingsRow.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.components.settings
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.defaultMinSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material3.Surface
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.unit.dp
17 |
18 | @Composable
19 | fun SettingsRow(
20 | modifier: Modifier = Modifier,
21 | leading: (@Composable () -> Unit)? = null,
22 | title: @Composable () -> Unit,
23 | trailing: (@Composable () -> Unit)? = null,
24 | onClick: (() -> Unit)? = null,
25 | content: (@Composable () -> Unit)? = null
26 | ) {
27 | Surface(
28 | onClick = { onClick?.invoke() },
29 | modifier = modifier
30 | .fillMaxWidth()
31 | .defaultMinSize(minHeight = 58.dp),
32 | enabled = onClick != null,
33 | shape = RoundedCornerShape(8.dp),
34 | tonalElevation = 4.dp
35 | ) {
36 | Row(
37 | modifier = Modifier
38 | .fillMaxWidth()
39 | .padding(horizontal = 16.dp, vertical = 8.dp),
40 | horizontalArrangement = Arrangement.SpaceBetween,
41 | verticalAlignment = Alignment.CenterVertically
42 | ) {
43 | Row(
44 | modifier = Modifier.weight(1f),
45 | horizontalArrangement = Arrangement.spacedBy(16.dp),
46 | verticalAlignment = Alignment.CenterVertically
47 | ) {
48 | leading?.invoke()
49 |
50 | Box(modifier = Modifier.weight(1f)) {
51 | title()
52 | }
53 | }
54 |
55 | Spacer(modifier = Modifier.width(4.dp))
56 |
57 | trailing?.invoke()
58 | }
59 |
60 | content?.invoke()
61 | }
62 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.compose)
5 | alias(libs.plugins.ksp)
6 | id("com.mikepenz.aboutlibraries.plugin")
7 | }
8 |
9 | android {
10 | namespace = "xyz.ptgms.tosdr"
11 | compileSdk = 35
12 |
13 | defaultConfig {
14 | applicationId = "xyz.ptgms.tosdr"
15 | minSdk = 24
16 | targetSdk = 36
17 | versionCode = 42
18 | versionName = "2.1.2"
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = true
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | signingConfig = signingConfigs.getByName("debug")
31 | }
32 | debug {
33 | applicationIdSuffix = ".debug"
34 | }
35 | }
36 | compileOptions {
37 | sourceCompatibility = JavaVersion.VERSION_11
38 | targetCompatibility = JavaVersion.VERSION_11
39 | }
40 | kotlinOptions {
41 | jvmTarget = "11"
42 | }
43 | buildFeatures {
44 | compose = true
45 | buildConfig = true
46 | }
47 |
48 | dependenciesInfo {
49 | includeInApk = false
50 | }
51 |
52 | flavorDimensions += "billing"
53 | productFlavors {
54 | create("google") {
55 | dimension = "billing"
56 | buildConfigField("String", "FLAVOR", "\"google\"")
57 | }
58 | create("foss") {
59 | dimension = "billing"
60 | buildConfigField("String", "FLAVOR", "\"foss\"")
61 | }
62 | }
63 | }
64 |
65 | dependencies {
66 | ksp(libs.androidx.room.compiler)
67 |
68 | implementation(libs.androidx.core.ktx)
69 | implementation(libs.androidx.lifecycle.runtime.ktx)
70 | implementation(libs.androidx.activity.compose)
71 | implementation(platform(libs.androidx.compose.bom))
72 | implementation(libs.androidx.ui)
73 | implementation(libs.androidx.ui.graphics)
74 | implementation(libs.androidx.ui.tooling.preview)
75 | implementation(libs.androidx.material3)
76 | implementation(libs.androidx.navigation.compose)
77 | implementation(libs.retrofit)
78 | implementation(libs.converter.gson)
79 | implementation(libs.kotlinx.coroutines.android)
80 | implementation(libs.androidx.lifecycle.viewmodel.compose)
81 | implementation(libs.coil.compose)
82 | implementation(libs.androidx.room.runtime)
83 | implementation(libs.androidx.room.ktx)
84 | implementation(libs.compose.markdown)
85 | implementation(libs.aboutlibraries.core)
86 |
87 | testImplementation(libs.junit)
88 | androidTestImplementation(libs.androidx.junit)
89 | androidTestImplementation(libs.androidx.espresso.core)
90 | androidTestImplementation(platform(libs.androidx.compose.bom))
91 | androidTestImplementation(libs.androidx.ui.test.junit4)
92 | debugImplementation(libs.androidx.ui.tooling)
93 | debugImplementation(libs.androidx.ui.test.manifest)
94 |
95 | runtimeOnly(libs.aboutlibraries)
96 |
97 | "googleImplementation"(libs.billing)
98 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/ShareActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr
2 |
3 | import android.app.Activity
4 | import android.app.AlertDialog
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Bundle
8 | import android.util.Log
9 | import android.widget.Toast
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.launch
13 |
14 | class ShareActivity : Activity() {
15 | private val coroutineScope = CoroutineScope(Dispatchers.Main)
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | handleIntent(intent)
20 | }
21 |
22 | private fun handleIntent(intent: Intent) {
23 | val appLinkAction = intent.action
24 | Log.i("ShareActivity", "Mime type: ${intent.type}")
25 | if (Intent.ACTION_SEND == appLinkAction && "text/plain" == intent.type) {
26 | handleSendText(intent)
27 | }
28 | }
29 |
30 | private fun handleSendText(intent: Intent) {
31 | intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
32 | val url = Uri.parse(it)
33 | val domain = url.host?.split(".")?.takeLast(2)?.joinToString(".")
34 |
35 | if (domain == null) {
36 | finish()
37 | return
38 | }
39 |
40 | val dialog = AlertDialog.Builder(this)
41 |
42 | dialog.setTitle(getString(R.string.share_question))
43 | dialog.setMessage(getString(R.string.share_question_desc, domain))
44 | dialog.setPositiveButton(getString(android.R.string.ok)) { _, _ ->
45 | searchAndOpenService(domain)
46 | }
47 |
48 | dialog.setNegativeButton(getString(android.R.string.cancel)) { _, _ ->
49 | finish()
50 | }
51 |
52 | dialog.setOnDismissListener {
53 | finish()
54 | }
55 |
56 | dialog.show()
57 | }
58 | }
59 |
60 | private fun searchAndOpenService(domain: String) {
61 | coroutineScope.launch {
62 | try {
63 | val response = ApiClient.api.searchServices(domain)
64 | if (response.services.isNotEmpty()) {
65 | val serviceId = response.services[0].id
66 | val intent = Intent(this@ShareActivity, MainActivity::class.java).apply {
67 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
68 | data = Uri.parse("tosdr://service/$serviceId")
69 | }
70 | startActivity(intent)
71 | } else {
72 | Toast.makeText(
73 | this@ShareActivity,
74 | getString(R.string.share_no_results),
75 | Toast.LENGTH_SHORT
76 | ).show()
77 | }
78 | } catch (_: Exception) {
79 | Toast.makeText(
80 | this@ShareActivity,
81 | getString(R.string.share_no_results),
82 | Toast.LENGTH_SHORT
83 | ).show()
84 | } finally {
85 | finish()
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/components/points/PointsRow.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.components.points
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.defaultMinSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Surface
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.res.painterResource
18 | import androidx.compose.ui.unit.dp
19 | import xyz.ptgms.tosdr.R
20 | import xyz.ptgms.tosdr.api.models.Point
21 | import xyz.ptgms.tosdr.ui.theme.BadgeColors
22 |
23 | @Composable
24 | fun PointsRow(
25 | modifier: Modifier = Modifier,
26 | point: Point,
27 | onClick: (() -> Unit)? = null,
28 | content: (@Composable () -> Unit)? = null,
29 | original: Boolean = false
30 | ) {
31 | Surface(
32 | onClick = { onClick?.invoke() },
33 | modifier = modifier
34 | .fillMaxWidth()
35 | .defaultMinSize(minHeight = 58.dp),
36 | enabled = onClick != null,
37 | shape = RoundedCornerShape(8.dp),
38 | tonalElevation = 4.dp,
39 | ) {
40 | Row(
41 | modifier = Modifier
42 | .fillMaxWidth()
43 | .padding(horizontal = 16.dp),
44 | horizontalArrangement = Arrangement.SpaceBetween,
45 | verticalAlignment = Alignment.CenterVertically
46 | ) {
47 | Row(
48 | modifier = Modifier.weight(1f),
49 | horizontalArrangement = Arrangement.spacedBy(16.dp),
50 | verticalAlignment = Alignment.CenterVertically
51 | ) {
52 | Icon(
53 | painter = painterResource(when(point.case.classification) {
54 | "blocker" -> R.drawable.ic_rounded_block_24
55 | "bad" -> R.drawable.ic_rounded_thumb_down_24
56 | "good" -> R.drawable.ic_rounded_thumb_up_24
57 | else -> R.drawable.ic_rounded_info_24
58 | }),
59 | contentDescription = null,
60 | tint = when(point.case.classification) {
61 | "blocker" -> BadgeColors.red
62 | "bad" -> BadgeColors.orange
63 | "good" -> BadgeColors.green
64 | else -> BadgeColors.gray
65 | }
66 | )
67 |
68 | Box(modifier = Modifier.weight(1f)) {
69 | Text(
70 | modifier = Modifier.padding(vertical = 4.dp),
71 | text = if (!original && point.case.localized_title != null) {
72 | point.case.localized_title
73 | } else {
74 | point.title
75 | },
76 | style = MaterialTheme.typography.bodyLarge
77 | )
78 | }
79 | }
80 | }
81 |
82 | content?.invoke()
83 | }
84 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | aboutlibraries = "11.5.0"
3 | aboutlibrariesCore = "11.5.0"
4 | agp = "8.9.0"
5 | billing = "7.1.1"
6 | coilCompose = "2.7.0"
7 | composeMarkdown = "0.5.6"
8 | converterGson = "2.11.0"
9 | kotlin = "2.0.21"
10 | coreKtx = "1.15.0"
11 | junit = "4.13.2"
12 | junitVersion = "1.2.1"
13 | espressoCore = "3.6.1"
14 | kotlinxCoroutinesAndroid = "1.10.1"
15 | lifecycleRuntimeKtx = "2.8.7"
16 | activityCompose = "1.10.0"
17 | composeBom = "2025.01.00"
18 | retrofit = "2.11.0"
19 | navigation-compose = "2.8.5"
20 | roomKtx = "2.6.1"
21 | ksp = "2.0.21-1.0.28"
22 |
23 | [libraries]
24 | aboutlibraries = { module = "com.mikepenz:aboutlibraries", version.ref = "aboutlibraries" }
25 | aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibrariesCore" }
26 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
27 | androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
28 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomKtx" }
29 | androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
30 | androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" }
31 | billing = { module = "com.android.billingclient:billing", version.ref = "billing" }
32 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
33 | compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" }
34 | converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
35 | junit = { group = "junit", name = "junit", version.ref = "junit" }
36 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
37 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
38 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
39 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
40 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
41 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
42 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
43 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
44 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
45 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
46 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
47 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
48 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
49 | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
50 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
51 |
52 | [plugins]
53 | android-application = { id = "com.android.application", version.ref = "agp" }
54 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
55 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
56 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
57 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/screens/about/ServicesExplainedScreen.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.screens.about
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
7 | import androidx.compose.material.icons.rounded.Check
8 | import androidx.compose.material3.*
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 | import androidx.navigation.NavController
16 | import xyz.ptgms.tosdr.R
17 | import xyz.ptgms.tosdr.components.settings.SettingsGroup
18 | import xyz.ptgms.tosdr.components.settings.SettingsTitle
19 | import xyz.ptgms.tosdr.ui.theme.BadgeColors
20 |
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | @Composable
23 | fun ServicesExplainedScreen(navController: NavController) {
24 | Scaffold(
25 | topBar = {
26 | TopAppBar(
27 | navigationIcon = {
28 | IconButton(onClick = { navController.navigateUp() }) {
29 | Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
30 | }
31 | },
32 | title = { Text(stringResource(R.string.services_title)) }
33 | )
34 | }
35 | ) { padding ->
36 | LazyColumn(
37 | modifier = Modifier
38 | .fillMaxSize()
39 | .padding(padding)
40 | .padding(horizontal = 16.dp)
41 | ) {
42 | item {
43 | SettingsTitle(text = stringResource(R.string.services_badges))
44 | SettingsGroup {
45 | Surface(
46 | modifier = Modifier.fillMaxWidth(),
47 | color = BadgeColors.green,
48 | shape = MaterialTheme.shapes.medium
49 | ) {
50 | Row(
51 | modifier = Modifier
52 | .fillMaxWidth()
53 | .padding(16.dp),
54 | verticalAlignment = Alignment.CenterVertically
55 | ) {
56 | Column {
57 | Row(
58 | verticalAlignment = Alignment.CenterVertically,
59 | horizontalArrangement = Arrangement.spacedBy(8.dp)
60 | ) {
61 | Icon(
62 | Icons.Rounded.Check,
63 | contentDescription = null,
64 | tint = Color.White
65 | )
66 | Text(
67 | text = stringResource(R.string.services_review_status),
68 | style = MaterialTheme.typography.titleLarge,
69 | color = Color.White
70 | )
71 | }
72 | Text(
73 | text = stringResource(R.string.services_review_desc),
74 | style = MaterialTheme.typography.bodySmall,
75 | color = Color.White
76 | )
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
83 | item {
84 | SettingsTitle(text = stringResource(R.string.services_contents))
85 | SettingsGroup {
86 | Text(
87 | text = stringResource(R.string.services_contents_desc),
88 | style = MaterialTheme.typography.bodyMedium,
89 | modifier = Modifier.padding(16.dp)
90 | )
91 | }
92 | }
93 |
94 | item {
95 | Spacer(modifier = Modifier.height(16.dp))
96 | }
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/screens/about/LibrariesScreen.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.screens.about
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.lazy.LazyColumn
13 | import androidx.compose.foundation.lazy.items
14 | import androidx.compose.material.icons.Icons
15 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
16 | import androidx.compose.material3.Button
17 | import androidx.compose.material3.ExperimentalMaterial3Api
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.IconButton
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.Scaffold
22 | import androidx.compose.material3.Surface
23 | import androidx.compose.material3.Text
24 | import androidx.compose.material3.TopAppBar
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.graphics.Shape
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.unit.dp
31 | import androidx.navigation.NavController
32 | import com.mikepenz.aboutlibraries.Libs
33 | import com.mikepenz.aboutlibraries.entity.Library
34 | import com.mikepenz.aboutlibraries.util.withContext
35 | import xyz.ptgms.tosdr.R
36 | import xyz.ptgms.tosdr.components.lists.getAdaptiveRoundedCornerShape
37 |
38 |
39 | @OptIn(ExperimentalMaterial3Api::class)
40 | @Composable
41 | fun LibrariesScreen(navController: NavController) {
42 | Scaffold(
43 | topBar = {
44 | TopAppBar(
45 | navigationIcon = {
46 | IconButton(onClick = { navController.navigateUp() }) {
47 | Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
48 | }
49 | },
50 | title = { Text(stringResource(R.string.libraries_title)) }
51 | )
52 | }
53 | ) { padding ->
54 | val context = LocalContext.current
55 | LazyColumn(
56 | modifier = Modifier
57 | .fillMaxSize()
58 | .padding(padding)
59 | .padding(horizontal = 16.dp),
60 | verticalArrangement = Arrangement.spacedBy(2.dp)
61 | ) {
62 | val libs = Libs.Builder()
63 | .withContext(context)
64 | .build()
65 |
66 | val uniqueLibs = libs.libraries.distinctBy { it.name }
67 | items(uniqueLibs) { lib ->
68 | AttributionList(
69 | context,
70 | lib,
71 | shape = getAdaptiveRoundedCornerShape(
72 | index = uniqueLibs.indexOf(lib),
73 | lastIndex = uniqueLibs.lastIndex
74 | )
75 | )
76 | }
77 | }
78 | }
79 | }
80 |
81 | @Composable
82 | private fun AttributionList(ctx: Context, lib: Library, shape: Shape) {
83 | Surface(
84 | shape = shape,
85 | tonalElevation = 4.dp
86 | ) {
87 | Column {
88 | Text(
89 | text = lib.name,
90 | modifier = Modifier.padding(8.dp),
91 | style = MaterialTheme.typography.headlineSmall,
92 | )
93 | Text(
94 | text = lib.description ?: stringResource(R.string.libraries_no_description),
95 | modifier = Modifier.padding(8.dp),
96 | style = MaterialTheme.typography.bodySmall,
97 | )
98 | if (lib.website != null) Row(
99 | modifier = Modifier
100 | .fillMaxWidth()
101 | .padding(8.dp)
102 | ) {
103 | Button(modifier = Modifier.weight(1f), onClick = {
104 | val i = Intent(Intent.ACTION_VIEW)
105 | i.data = Uri.parse(lib.website)
106 | ctx.applicationContext.startActivity(i, null)
107 | }) {
108 | Text(text = stringResource(R.string.libraries_open_website))
109 | }
110 | }
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/screens/about/GradesExplainedScreen.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.screens.about
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
7 | import androidx.compose.material3.*
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.unit.dp
14 | import androidx.navigation.NavController
15 | import xyz.ptgms.tosdr.R
16 | import xyz.ptgms.tosdr.components.settings.SettingsGroup
17 | import xyz.ptgms.tosdr.components.settings.SettingsTitle
18 | import xyz.ptgms.tosdr.ui.theme.ToSDRColorScheme
19 |
20 | @OptIn(ExperimentalMaterial3Api::class)
21 | @Composable
22 | fun GradesExplainedScreen(navController: NavController) {
23 | Scaffold(
24 | topBar = {
25 | TopAppBar(
26 | navigationIcon = {
27 | IconButton(onClick = { navController.navigateUp() }) {
28 | Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
29 | }
30 | },
31 | title = { Text(stringResource(R.string.grades_title)) }
32 | )
33 | }
34 | ) { padding ->
35 | LazyColumn(
36 | modifier = Modifier
37 | .fillMaxSize()
38 | .padding(padding)
39 | .padding(horizontal = 16.dp)
40 | ) {
41 | item {
42 | SettingsTitle(text = stringResource(R.string.grades_title))
43 | SettingsGroup {
44 | GradeRow(
45 | grade = "A",
46 | title = stringResource(R.string.grade_a_title),
47 | description = stringResource(R.string.grade_a_desc),
48 | color = ToSDRColorScheme.gradeA
49 | )
50 | GradeRow(
51 | grade = "B",
52 | title = stringResource(R.string.grade_b_title),
53 | description = stringResource(R.string.grade_b_desc),
54 | color = ToSDRColorScheme.gradeB
55 | )
56 | GradeRow(
57 | grade = "C",
58 | title = stringResource(R.string.grade_c_title),
59 | description = stringResource(R.string.grade_c_desc),
60 | color = ToSDRColorScheme.gradeC
61 | )
62 | GradeRow(
63 | grade = "D",
64 | title = stringResource(R.string.grade_d_title),
65 | description = stringResource(R.string.grade_d_desc),
66 | color = ToSDRColorScheme.gradeD
67 | )
68 | GradeRow(
69 | grade = "E",
70 | title = stringResource(R.string.grade_e_title),
71 | description = stringResource(R.string.grade_e_desc),
72 | color = ToSDRColorScheme.gradeE
73 | )
74 | }
75 | }
76 |
77 | item {
78 | SettingsTitle(text = stringResource(R.string.grades_other))
79 | SettingsGroup {
80 | GradeRow(
81 | grade = "N/A",
82 | title = stringResource(R.string.grade_na_title),
83 | description = stringResource(R.string.grade_na_desc),
84 | color = ToSDRColorScheme.gradeNA
85 | )
86 | }
87 | }
88 |
89 | item {
90 | Spacer(modifier = Modifier.height(16.dp))
91 | }
92 | }
93 | }
94 | }
95 |
96 | @Composable
97 | private fun GradeRow(
98 | grade: String,
99 | title: String,
100 | description: String,
101 | color: Color
102 | ) {
103 | Surface(
104 | modifier = Modifier.fillMaxWidth(),
105 | color = color,
106 | shape = MaterialTheme.shapes.medium
107 | ) {
108 | Row(
109 | modifier = Modifier
110 | .fillMaxWidth()
111 | .padding(16.dp),
112 | verticalAlignment = Alignment.CenterVertically
113 | ) {
114 | Text(
115 | text = grade,
116 | style = MaterialTheme.typography.titleLarge,
117 | color = Color.White,
118 | modifier = Modifier.padding(end = 16.dp)
119 | )
120 | Column {
121 | Text(
122 | text = title,
123 | style = MaterialTheme.typography.titleMedium,
124 | color = Color.White
125 | )
126 | Text(
127 | text = description,
128 | style = MaterialTheme.typography.bodySmall,
129 | color = Color.White
130 | )
131 | }
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/screens/PointView.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.screens
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
10 | import androidx.compose.material3.*
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.collectAsState
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.unit.dp
21 | import androidx.navigation.NavController
22 | import xyz.ptgms.tosdr.R
23 | import xyz.ptgms.tosdr.components.points.PointsRow
24 | import xyz.ptgms.tosdr.viewmodels.ToSDRViewModel
25 |
26 | @OptIn(ExperimentalMaterial3Api::class)
27 | @Composable
28 | fun PointView(pointId: Int, navController: NavController, viewModel: ToSDRViewModel) {
29 | val context = LocalContext.current
30 | val pointDetails by viewModel.pointDetails.collectAsState()
31 |
32 | LaunchedEffect(pointId) {
33 | viewModel.getPointDetails(pointId)
34 | }
35 |
36 | Scaffold(
37 | topBar = {
38 | TopAppBar(
39 | navigationIcon = {
40 | IconButton(onClick = { navController.navigateUp() }) {
41 | Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(R.string.nav_back))
42 | }
43 | },
44 | title = {
45 | Text(stringResource(R.string.point_details))
46 | }
47 | )
48 | }
49 | ) { padding ->
50 | if (pointDetails != null) {
51 | LazyColumn(
52 | modifier = Modifier
53 | .fillMaxSize()
54 | .padding(padding)
55 | .padding(16.dp)
56 | ) {
57 | item {
58 | Text(stringResource(R.string.point_details), style = MaterialTheme.typography.titleSmall)
59 | PointsRow(point = pointDetails!!)
60 | }
61 |
62 | if (pointDetails!!.case.localized_title != null) {
63 | item {
64 | Spacer(modifier = Modifier.height(8.dp))
65 | Text(stringResource(R.string.point_original_point), style = MaterialTheme.typography.titleSmall)
66 | PointsRow(point = pointDetails!!, original = true)
67 | }
68 | }
69 |
70 | item {
71 | Spacer(modifier = Modifier.height(16.dp))
72 | }
73 |
74 | if (pointDetails!!.case.description.isNotEmpty()) {
75 | item {
76 | SectionCard(title = stringResource(R.string.point_description)) {
77 | Text(pointDetails!!.case.description)
78 | }
79 | Spacer(modifier = Modifier.height(16.dp))
80 | }
81 | }
82 |
83 | item {
84 | SectionCard(title = stringResource(R.string.point_actions)) {
85 | Button(
86 | onClick = {
87 | val intent = Intent(Intent.ACTION_VIEW)
88 | intent.data = Uri.parse("https://edit.tosdr.org/points/${pointDetails!!.id}")
89 | context.startActivity(intent)
90 | },
91 | modifier = Modifier.fillMaxWidth()
92 | ) {
93 | Icon(painterResource(R.drawable.ic_rounded_open_in_browser_24), contentDescription = null)
94 | Spacer(modifier = Modifier.width(8.dp))
95 | Text(stringResource(R.string.point_open_tosdr))
96 | }
97 | }
98 | }
99 | }
100 | } else {
101 | Box(
102 | modifier = Modifier.fillMaxSize(),
103 | contentAlignment = Alignment.Center
104 | ) {
105 | CircularProgressIndicator()
106 | }
107 | }
108 | }
109 | }
110 |
111 | @Composable
112 | private fun SectionCard(
113 | title: String,
114 | content: @Composable ColumnScope.() -> Unit
115 | ) {
116 | Card(
117 | shape = RoundedCornerShape(16.dp),
118 | modifier = Modifier.fillMaxWidth()
119 | ) {
120 | Column(
121 | modifier = Modifier.padding(16.dp)
122 | ) {
123 | Text(
124 | text = title,
125 | style = MaterialTheme.typography.titleMedium,
126 | color = MaterialTheme.colorScheme.primary,
127 | modifier = Modifier.padding(bottom = 8.dp)
128 | )
129 | content()
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/screens/about/PointsExplainedScreen.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.screens.about
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
7 | import androidx.compose.material3.*
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.res.painterResource
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.unit.dp
15 | import androidx.navigation.NavController
16 | import xyz.ptgms.tosdr.R
17 | import xyz.ptgms.tosdr.components.settings.SettingsGroup
18 | import xyz.ptgms.tosdr.components.settings.SettingsTitle
19 | import xyz.ptgms.tosdr.ui.theme.BadgeColors
20 |
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | @Composable
23 | fun PointsExplainedScreen(navController: NavController) {
24 | Scaffold(
25 | topBar = {
26 | TopAppBar(
27 | navigationIcon = {
28 | IconButton(onClick = { navController.navigateUp() }) {
29 | Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
30 | }
31 | },
32 | title = { Text(stringResource(R.string.points_title)) }
33 | )
34 | }
35 | ) { padding ->
36 | LazyColumn(
37 | modifier = Modifier
38 | .fillMaxSize()
39 | .padding(padding)
40 | .padding(horizontal = 16.dp)
41 | ) {
42 | item {
43 | SettingsTitle(text = stringResource(R.string.points_classifications))
44 | SettingsGroup {
45 | ClassificationRow(
46 | title = stringResource(R.string.points_blocker_title),
47 | description = stringResource(R.string.points_blocker_desc),
48 | icon = painterResource(R.drawable.ic_rounded_block_24),
49 | color = BadgeColors.red
50 | )
51 | ClassificationRow(
52 | title = stringResource(R.string.points_bad_title),
53 | description = stringResource(R.string.points_bad_desc),
54 | icon = painterResource(R.drawable.ic_rounded_thumb_down_24),
55 | color = BadgeColors.orange
56 | )
57 | ClassificationRow(
58 | title = stringResource(R.string.points_good_title),
59 | description = stringResource(R.string.points_good_desc),
60 | icon = painterResource(R.drawable.ic_rounded_thumb_up_24),
61 | color = BadgeColors.green
62 | )
63 | ClassificationRow(
64 | title = stringResource(R.string.points_neutral_title),
65 | description = stringResource(R.string.points_neutral_desc),
66 | icon = painterResource(R.drawable.ic_rounded_info_24),
67 | color = BadgeColors.gray
68 | )
69 | }
70 | }
71 |
72 | item {
73 | SettingsTitle(text = stringResource(R.string.points_grade_calculation))
74 | Column(
75 | verticalArrangement = Arrangement.spacedBy(8.dp)
76 | ) {
77 | Text(
78 | stringResource(R.string.points_grade_intro),
79 | style = MaterialTheme.typography.bodyMedium
80 | )
81 | Text(
82 | stringResource(R.string.points_grade_a),
83 | style = MaterialTheme.typography.bodyMedium
84 | )
85 | Text(
86 | stringResource(R.string.points_grade_b),
87 | style = MaterialTheme.typography.bodyMedium
88 | )
89 | Text(
90 | stringResource(R.string.points_grade_c),
91 | style = MaterialTheme.typography.bodyMedium
92 | )
93 | Text(
94 | stringResource(R.string.points_grade_d),
95 | style = MaterialTheme.typography.bodyMedium
96 | )
97 | Text(
98 | stringResource(R.string.points_grade_e),
99 | style = MaterialTheme.typography.bodyMedium
100 | )
101 | }
102 | }
103 |
104 | item {
105 | Spacer(modifier = Modifier.height(16.dp))
106 | }
107 | }
108 | }
109 | }
110 |
111 | @Composable
112 | private fun ClassificationRow(
113 | title: String,
114 | description: String,
115 | icon: androidx.compose.ui.graphics.painter.Painter,
116 | color: Color
117 | ) {
118 | Surface(
119 | modifier = Modifier.fillMaxWidth(),
120 | color = color,
121 | shape = MaterialTheme.shapes.medium
122 | ) {
123 | Row(
124 | modifier = Modifier
125 | .fillMaxWidth()
126 | .padding(16.dp),
127 | verticalAlignment = Alignment.CenterVertically
128 | ) {
129 | Column {
130 | Row(
131 | verticalAlignment = Alignment.CenterVertically,
132 | horizontalArrangement = Arrangement.spacedBy(8.dp)
133 | ) {
134 | Icon(
135 | painter = icon,
136 | contentDescription = null,
137 | tint = Color.White
138 | )
139 | Text(
140 | text = title,
141 | style = MaterialTheme.typography.titleLarge,
142 | color = Color.White
143 | )
144 | }
145 | Text(
146 | text = description,
147 | style = MaterialTheme.typography.bodySmall,
148 | color = Color.White
149 | )
150 | }
151 | }
152 | }
153 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.activity.enableEdgeToEdge
8 | import androidx.activity.viewModels
9 | import androidx.compose.foundation.layout.WindowInsets
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.navigationBarsPadding
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.lifecycle.lifecycleScope
14 | import androidx.compose.material3.*
15 | import androidx.compose.runtime.*
16 | import androidx.compose.ui.Modifier
17 | import androidx.navigation.compose.NavHost
18 | import androidx.navigation.compose.composable
19 | import androidx.navigation.compose.rememberNavController
20 | import kotlinx.coroutines.launch
21 | import xyz.ptgms.tosdr.data.DatabaseUpdater
22 | import xyz.ptgms.tosdr.data.room.ToSDRDatabase
23 | import xyz.ptgms.tosdr.navigation.Screen
24 | import xyz.ptgms.tosdr.screens.*
25 | import xyz.ptgms.tosdr.screens.about.AboutScreen
26 | import xyz.ptgms.tosdr.screens.about.GradesExplainedScreen
27 | import xyz.ptgms.tosdr.screens.about.LibrariesScreen
28 | import xyz.ptgms.tosdr.screens.about.PointsExplainedScreen
29 | import xyz.ptgms.tosdr.screens.about.ServicesExplainedScreen
30 | import xyz.ptgms.tosdr.ui.theme.ToSDRTheme
31 | import xyz.ptgms.tosdr.viewmodels.ToSDRViewModel
32 | import androidx.compose.animation.AnimatedContentTransitionScope
33 | import androidx.compose.animation.core.tween
34 |
35 | class MainActivity : ComponentActivity() {
36 | private lateinit var database: ToSDRDatabase
37 | private val viewModel: ToSDRViewModel by viewModels()
38 |
39 | // Store the initial deep link intent
40 | private var initialServiceId: Int? = null
41 |
42 | override fun onCreate(savedInstanceState: Bundle?) {
43 | super.onCreate(savedInstanceState)
44 |
45 | // Parse deep link before setting content
46 | intent?.data?.let { uri ->
47 | if (uri.scheme == "tosdr" && uri.host == "service") {
48 | initialServiceId = uri.lastPathSegment?.toIntOrNull()
49 | }
50 | }
51 |
52 | enableEdgeToEdge()
53 |
54 | database = ToSDRDatabase.getDatabase(this)
55 |
56 | lifecycleScope.launch {
57 | if (DatabaseUpdater.shouldUpdate(database)) {
58 | viewModel.refreshDatabase(database) {
59 | if (it) Log.i("Updater", "Successfully auto-updated the DB")
60 | else Log.i("Updater", "Could not auto-update the DB")
61 | }
62 | }
63 | }
64 |
65 | setContent {
66 | ToSDRTheme {
67 | Surface(
68 | modifier = Modifier.fillMaxSize(),
69 | color = MaterialTheme.colorScheme.background
70 | ) {
71 | MainScreen(viewModel, initialServiceId)
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 | @OptIn(ExperimentalMaterial3Api::class)
79 | @Composable
80 | fun MainScreen(viewModel: ToSDRViewModel, initialServiceId: Int?) {
81 | val navController = rememberNavController()
82 |
83 | // Handle deep link navigation after composition
84 | LaunchedEffect(initialServiceId) {
85 | initialServiceId?.let { serviceId ->
86 | navController.navigate(Screen.ServiceDetails.createRoute(serviceId))
87 | }
88 | }
89 |
90 | Scaffold(
91 | modifier = Modifier.fillMaxSize(),
92 | contentWindowInsets = WindowInsets(0, 0, 0, 0)
93 | ) { paddingValues ->
94 | NavHost(
95 | navController = navController,
96 | startDestination = Screen.Search.route,
97 | modifier = Modifier
98 | .padding(paddingValues)
99 | .navigationBarsPadding(),
100 | enterTransition = {
101 | slideIntoContainer(
102 | towards = AnimatedContentTransitionScope.SlideDirection.Left,
103 | animationSpec = tween(300)
104 | )
105 | },
106 | exitTransition = {
107 | slideOutOfContainer(
108 | towards = AnimatedContentTransitionScope.SlideDirection.Left,
109 | animationSpec = tween(300)
110 | )
111 | },
112 | popEnterTransition = {
113 | slideIntoContainer(
114 | towards = AnimatedContentTransitionScope.SlideDirection.Right,
115 | animationSpec = tween(300)
116 | )
117 | },
118 | popExitTransition = {
119 | slideOutOfContainer(
120 | towards = AnimatedContentTransitionScope.SlideDirection.Right,
121 | animationSpec = tween(300)
122 | )
123 | }
124 | ) {
125 | composable(Screen.Search.route) { SearchScreen(navController, viewModel) }
126 | composable(Screen.About.route) { AboutScreen(navController) }
127 | composable(Screen.Donate.route) { DonationScreen(navController) }
128 | composable(Screen.Team.route) { TeamScreen(navController) }
129 | composable(Screen.Settings.route) { SettingsScreen(navController, viewModel) }
130 | composable(Screen.ServiceDetails.route) { backStackEntry ->
131 | val serviceId = backStackEntry.arguments?.getString("serviceId")?.toIntOrNull()
132 | if (serviceId != null) {
133 | ServiceDetailsScreen(serviceId = serviceId, navController = navController, viewModel = viewModel)
134 | }
135 | }
136 | composable(Screen.PointView.route) { backStackEntry ->
137 | val pointId = backStackEntry.arguments?.getString("pointId")?.toIntOrNull()
138 | if (pointId != null) {
139 | PointView(pointId = pointId, navController = navController, viewModel = viewModel)
140 | }
141 | }
142 | composable(Screen.GradesExplained.route) { GradesExplainedScreen(navController) }
143 | composable(Screen.PointsExplained.route) { PointsExplainedScreen(navController) }
144 | composable(Screen.ServicesExplained.route) { ServicesExplainedScreen(navController) }
145 | composable(Screen.Libraries.route) { LibrariesScreen(navController) }
146 | }
147 | }
148 | }
--------------------------------------------------------------------------------
/app/src/google/java/xyz/ptgms/tosdr/data/BillingManager.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.data
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import com.android.billingclient.api.*
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 |
9 | class BillingManager(
10 | context: Context
11 | ) : PurchasesUpdatedListener, BillingClientStateListener, BillingManagerInterface {
12 |
13 | private var billingClient: BillingClient = BillingClient.newBuilder(context)
14 | .setListener(this)
15 | .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
16 | .build()
17 |
18 | private val _purchaseState = MutableStateFlow(
19 | BillingManagerInterface.PurchaseState.IDLE
20 | )
21 | override val purchaseState: StateFlow = _purchaseState
22 |
23 | // Store the original ProductDetails objects
24 | private var productDetailsMap: Map = emptyMap()
25 |
26 | init {
27 | billingClient.startConnection(this)
28 | }
29 |
30 | override fun onBillingSetupFinished(billingResult: BillingResult) {
31 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
32 | queryAvailableProducts()
33 | consumeExistingPurchases()
34 | }
35 | }
36 |
37 | override fun onBillingServiceDisconnected() {
38 | billingClient.startConnection(this)
39 | }
40 |
41 | override fun onPurchasesUpdated(
42 | billingResult: BillingResult,
43 | purchases: List?
44 | ) {
45 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
46 | for (purchase in purchases) {
47 | handlePurchase(purchase)
48 | }
49 | }
50 | }
51 |
52 | private fun queryAvailableProducts() {
53 | val params = QueryProductDetailsParams.newBuilder()
54 | .setProductList(
55 | listOf(
56 | QueryProductDetailsParams.Product.newBuilder()
57 | .setProductId("1euro_donation")
58 | .setProductType(BillingClient.ProductType.INAPP)
59 | .build(),
60 | QueryProductDetailsParams.Product.newBuilder()
61 | .setProductId("5euro_donation")
62 | .setProductType(BillingClient.ProductType.INAPP)
63 | .build(),
64 | QueryProductDetailsParams.Product.newBuilder()
65 | .setProductId("10euro_donation")
66 | .setProductType(BillingClient.ProductType.INAPP)
67 | .build()
68 | )
69 | )
70 | .build()
71 |
72 | billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
73 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
74 | val sortedProducts = productDetailsList.sortedBy {
75 | it.oneTimePurchaseOfferDetails?.priceAmountMicros ?: 0L
76 | }
77 | // Store the ProductDetails for later use
78 | productDetailsMap = sortedProducts.associateBy { it.productId }
79 |
80 | val mappedProducts = sortedProducts.map { details ->
81 | BillingManagerInterface.ProductInfo(
82 | id = details.productId,
83 | name = details.name,
84 | price = details.oneTimePurchaseOfferDetails?.formattedPrice ?: ""
85 | )
86 | }
87 | _purchaseState.value = BillingManagerInterface.PurchaseState.ProductsAvailable(mappedProducts)
88 | }
89 | }
90 | }
91 |
92 | private fun consumeExistingPurchases() {
93 | billingClient.queryPurchasesAsync(
94 | QueryPurchasesParams.newBuilder()
95 | .setProductType(BillingClient.ProductType.INAPP)
96 | .build()
97 | ) { billingResult, purchases ->
98 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
99 | purchases.forEach { purchase ->
100 | if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
101 | val consumeParams = ConsumeParams.newBuilder()
102 | .setPurchaseToken(purchase.purchaseToken)
103 | .build()
104 |
105 | billingClient.consumeAsync(consumeParams) { consumeResult, _ ->
106 | if (consumeResult.responseCode != BillingClient.BillingResponseCode.OK) {
107 | _purchaseState.value = BillingManagerInterface.PurchaseState.Error("Failed to consume existing purchase")
108 | }
109 | }
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
116 | override fun launchBillingFlow(activity: Activity, product: BillingManagerInterface.ProductInfo) {
117 | val productDetails = productDetailsMap[product.id]
118 | if (productDetails != null) {
119 | val billingFlowParams = BillingFlowParams.newBuilder()
120 | .setProductDetailsParamsList(
121 | listOf(
122 | BillingFlowParams.ProductDetailsParams.newBuilder()
123 | .setProductDetails(productDetails)
124 | .build()
125 | )
126 | )
127 | .build()
128 |
129 | billingClient.launchBillingFlow(activity, billingFlowParams)
130 | }
131 | }
132 |
133 | private fun handlePurchase(purchase: Purchase) {
134 | if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
135 | val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
136 | .setPurchaseToken(purchase.purchaseToken)
137 | .build()
138 |
139 | billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
140 | if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
141 | val consumeParams = ConsumeParams.newBuilder()
142 | .setPurchaseToken(purchase.purchaseToken)
143 | .build()
144 |
145 | billingClient.consumeAsync(consumeParams) { consumeResult, _ ->
146 | if (consumeResult.responseCode == BillingClient.BillingResponseCode.OK) {
147 | _purchaseState.value = BillingManagerInterface.PurchaseState.PurchaseSuccessful
148 | }
149 | }
150 | }
151 | }
152 | }
153 | }
154 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/viewmodels/ToSDRViewModel.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.viewmodels
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.CoroutineDispatcher
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.StateFlow
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.flow.flow
11 | import kotlinx.coroutines.flow.flowOn
12 | import xyz.ptgms.tosdr.api.ToSDRRepository
13 | import xyz.ptgms.tosdr.api.models.ServiceBasic
14 | import xyz.ptgms.tosdr.api.models.ServiceDetail
15 | import kotlinx.coroutines.Dispatchers
16 | import xyz.ptgms.tosdr.data.DatabaseUpdater
17 | import xyz.ptgms.tosdr.data.room.ToSDRDatabase
18 | import kotlinx.coroutines.Job
19 | import kotlinx.coroutines.delay
20 | import kotlinx.coroutines.flow.take
21 | import kotlinx.coroutines.withContext
22 | import xyz.ptgms.tosdr.api.models.Point
23 |
24 | class ToSDRViewModel(private val dispatcher: CoroutineDispatcher = Dispatchers.IO) : ViewModel() {
25 | private val repository = ToSDRRepository()
26 |
27 | private val _searchResults = MutableStateFlow>(emptyList())
28 | val searchResults: StateFlow> = _searchResults
29 |
30 | private val _serviceDetails = MutableStateFlow(null)
31 | val serviceDetails: StateFlow = _serviceDetails
32 |
33 | private val _pointDetails = MutableStateFlow(null)
34 | val pointDetails: StateFlow = _pointDetails
35 |
36 | data class DbStats(
37 | val lastUpdate: Long = 0L,
38 | val entryCount: Int = 0
39 | )
40 |
41 | private val _dbStats = MutableStateFlow(DbStats())
42 | val dbStats: StateFlow = _dbStats
43 |
44 | private val _preferServerSearch = MutableStateFlow(false)
45 | val preferServerSearch: StateFlow = _preferServerSearch
46 |
47 | private val _baseUrl = MutableStateFlow(ApiClient.defaultUrl)
48 | val baseUrl: StateFlow = _baseUrl
49 |
50 | private val searchJob = Job()
51 | private var searchDebounceJob: Job? = null
52 |
53 | private val _searchQuery = MutableStateFlow("")
54 | val searchQuery: StateFlow = _searchQuery
55 |
56 | fun searchServices(query: String, database: ToSDRDatabase, preferServerSearch: Boolean = false) {
57 | searchDebounceJob?.cancel()
58 | searchDebounceJob = viewModelScope.launch {
59 | delay(300) // Debounce for 300ms
60 |
61 | if (preferServerSearch) {
62 | repository.searchServices(query).onSuccess {
63 | _searchResults.value = it
64 | }
65 | } else {
66 | withContext(dispatcher) {
67 | database.serviceDao().searchServices("%$query%")
68 | .take(20)
69 | .collect { services ->
70 | val mappedResults = services.map { service ->
71 | ServiceBasic(
72 | id = service.id,
73 | name = service.name,
74 | urls = listOf(service.url),
75 | rating = service.rating,
76 | is_comprehensively_reviewed = true,
77 | updated_at = "",
78 | created_at = "",
79 | slug = ""
80 | )
81 | }
82 | _searchResults.value = mappedResults
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | fun clearSearchResults() {
90 | _searchResults.value = emptyList()
91 | }
92 |
93 | fun getServiceDetails(id: Int) {
94 | viewModelScope.launch {
95 | repository.getServiceDetails(id).onSuccess {
96 | _serviceDetails.value = it
97 | }
98 | }
99 | }
100 |
101 | fun isLocalized(): Boolean {
102 | val serviceDetails = _serviceDetails.value
103 | if (serviceDetails != null) {
104 | for (point in serviceDetails.points) {
105 | if (point.case.localized_title != null) {
106 | return true
107 | }
108 | }
109 | }
110 | return false
111 | }
112 |
113 | fun getPointDetails(id: Int) {
114 | viewModelScope.launch {
115 | val point = serviceDetails.value?.points?.find { it.id == id }
116 | if (point != null) {
117 | _pointDetails.value = point
118 | }
119 | }
120 | }
121 |
122 | fun getAppDb(apiKey: String) = flow {
123 | emit(repository.getAppDb(apiKey))
124 | }.flowOn(Dispatchers.IO)
125 |
126 | fun loadDbStats(database: ToSDRDatabase) {
127 | viewModelScope.launch {
128 | val stats = DbStats(
129 | lastUpdate = database.serviceDao().getLastUpdateTime() ?: 0L,
130 | entryCount = database.serviceDao().getCount()
131 | )
132 | _dbStats.value = stats
133 | }
134 | }
135 |
136 | fun refreshDatabase(appDatabase: ToSDRDatabase, onComplete: (Boolean) -> Unit) {
137 | viewModelScope.launch {
138 | DatabaseUpdater.updateDatabase(this@ToSDRViewModel, appDatabase).collect { result ->
139 | result.onSuccess {
140 | loadDbStats(appDatabase)
141 | onComplete(true)
142 | }.onFailure {
143 | onComplete(false)
144 | }
145 | }
146 | }
147 | }
148 |
149 | fun deleteDatabase(database: ToSDRDatabase) {
150 | viewModelScope.launch {
151 | database.serviceDao().clearAll()
152 | loadDbStats(database)
153 | }
154 | }
155 |
156 | fun loadPreferences(context: Context) {
157 | viewModelScope.launch(dispatcher) {
158 | val prefs = context.getSharedPreferences("tosdr_prefs", Context.MODE_PRIVATE)
159 | _preferServerSearch.value = prefs.getBoolean("prefer_server_search", false)
160 | _baseUrl.value = prefs.getString("base_url", ApiClient.defaultUrl) ?: ApiClient.defaultUrl
161 | ApiClient.initialize(context)
162 | }
163 | }
164 |
165 | fun setPreferServerSearch(context: Context, value: Boolean) {
166 | context.getSharedPreferences("tosdr_prefs", Context.MODE_PRIVATE)
167 | .edit()
168 | .putBoolean("prefer_server_search", value)
169 | .apply()
170 | _preferServerSearch.value = value
171 | }
172 |
173 | fun setBaseUrl(context: Context, url: String) {
174 | viewModelScope.launch(dispatcher) {
175 | context.getSharedPreferences("tosdr_prefs", Context.MODE_PRIVATE)
176 | .edit()
177 | .putString("base_url", url)
178 | .apply()
179 | _baseUrl.value = url
180 | ApiClient.updateBaseUrl(url)
181 | }
182 | }
183 |
184 | fun setSearchQuery(query: String) {
185 | _searchQuery.value = query
186 | }
187 |
188 | override fun onCleared() {
189 | super.onCleared()
190 | searchJob.cancel()
191 | }
192 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 设置
4 | 快速操作
5 | 关于 ToS;DR
6 | 团队
7 | 捐赠
8 | 搜索服务…
9 | 清除搜索
10 | 返回
11 | 创始人
12 | 现任团队成员
13 | 过去的贡献者
14 | %1$s 的照片
15 | 电子邮件
16 | 网站
17 | 设置
18 | 搜索
19 | 首选服务器搜索
20 | 始终使用在线搜索而不是本地数据库
21 | 数据库
22 | 数据库未下载
23 | 下载数据库以启用离线功能
24 | 最后更新
25 | 上次刷新数据库的时间
26 | MMM dd, yyyy
27 | 服务
28 | 数据库中的服务数量
29 | 刷新中…
30 | 刷新数据库
31 | 删除数据库
32 | 错误
33 | 更新数据库失败。请再试一次。
34 | 确定
35 | 在浏览器中打开
36 | 加载中…
37 | %1$s 标志
38 | 已复审
39 | 等级 %1$s
40 | %1$s 积分
41 | 阻止者积分
42 | 负面积分
43 | 正面积分
44 | 中性积分
45 | 警告
46 | 以上要点是通过机器翻译的。它们可能并不准确,我们建议您查看原始标题以确保准确性。
47 | 显示原始标题
48 | 积分详情
49 | 原始积分
50 | 描述
51 | 操作
52 | 在 ToS;DR 中打开
53 | 关于
54 | 欢迎!
55 | 欢迎来到 ToS;DR!
56 | 这将指导您了解有关 ToS;DR 的所有信息!随意点击下面的任何内容以了解更多!
57 | 组织
58 | \"服务条款;我没读\"(简称:ToS;DR)是一个于2012年6月开始的年轻项目,旨在解决网络上的\"最大谎言\":几乎没有人真正阅读我们总是同意的服务条款。我们致力于通过从 A 到 E 的 \"便捷\" 等级对热门 web 服务的服务条款和隐私政策进行评级。
59 | ToS;DR 是一个非营利组织,我们所有的团队成员和贡献者都是志愿者,其中报酬很少。我们依靠捐赠来维持我们的基础设施和运营,并通过我们的网站和集体网站公布我们的财务状况。
60 | 术语
61 | 等级
62 | 积分
63 | 服务
64 | 贡献
65 | 策划服务条款
66 | 本应用
67 | 库
68 | 等级
69 | 其他
70 | 优秀
71 | 我们的最佳评级:此服务尊重您的隐私。
72 | 良好
73 | 相当不错的评级:此服务对用户公平,仅需进行轻微调整。
74 | 尚好
75 | 此服务尚可。条款尚可,但有些问题需要您考虑。
76 | 较差
77 | 此服务的条款不均衡或有一些问题需要您注意。
78 | 糟糕
79 | 我们的最差评级:此服务在隐私方面引发一些严重的关注。
80 | 不可用
81 | 此服务尚未收到足够的策划积分以显示准确的评级。欢迎贡献!
82 | 库
83 | 无描述
84 | 打开网站
85 | 积分
86 | 分类
87 | 拦截
88 | 这一点对您的用户权益和/或隐私有严重影响。这一点直接将服务归类为最差评级。
89 | 不好
90 | 这一点对您的用户权益和/或隐私产生负面影响。请注意。
91 | 好
92 | 这一点因对您的用户权益和/或隐私有益而被突出强调!
93 | 中性
94 | 这一点对您的用户权益和/或隐私既不好也不坏。
95 | 评级计算
96 | 服务的评级是根据其积分数量和类型计算的。这些计算经常更改,但一般思路如下:
97 | 用户有很多优秀积分的服务应被归类为 A
98 | 有很多优秀积分和一些负面积分的服务应被归类为 B
99 | 有一些优秀积分和一些负面积分的服务应被归类为 C
100 | 有少量优秀积分和大量负面积分的服务应被归类为 D
101 | 有拦截点的服务应被归类为 E
102 | 服务
103 | 服务徽章
104 | 复审状态
105 | 在 ToS;DR 中通常称为\"全面复审\",意味着此服务有足够的策划积分,可以被认为准确到足以赋予日常评级。
106 | 服务内容
107 | 每项服务都包含确定最终等级的积分、与 ToS;DR 相关的所有政策链接以及其他有用信息。
108 | 支持 ToS;DR
109 | 您的捐赠帮助我们维护和改进我们的服务。选择下面的金额:
110 | 选择金额
111 | 购买
112 | 您的捐赠帮助我们维护和改进 ToS;DR,使每个人都受益。
113 | 谢谢你!
114 | ToS;DR 评级查找
115 | 你想查看 %1$s 的评级吗?
116 | 抱歉,我找不到任何结果!
117 | 捐赠选项
118 | 感谢您在 Google Play 商店外使用 ToS;DR!您的支持对我们非常重要。我们邀请您探索我们的捐赠选项,并为我们的使命贡献力量。每一点支持我们都非常感激!
119 | 在 OpenCollective 上支持我们
120 | 加密货币
121 | 关于
122 | ToS;DR; 版本 %1$s
123 | 您正在运行变体 \'%1$s\'。
124 | API 设置
125 | API 端点
126 | 选择 API 端点
127 | 自定义 URL……
128 |
129 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/screens/TeamScreen.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.screens
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.lazy.items
6 | import androidx.compose.foundation.shape.CircleShape
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
9 | import androidx.compose.material.icons.rounded.Email
10 | import androidx.compose.material.icons.rounded.Home
11 | import androidx.compose.material3.*
12 | import androidx.compose.runtime.*
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.clip
16 | import androidx.compose.ui.platform.LocalUriHandler
17 | import androidx.compose.ui.res.painterResource
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.navigation.NavController
21 | import coil.compose.AsyncImage
22 | import dev.jeziellago.compose.markdowntext.MarkdownText
23 | import kotlinx.coroutines.launch
24 | import retrofit2.Retrofit
25 | import retrofit2.converter.gson.GsonConverterFactory
26 | import retrofit2.http.GET
27 | import xyz.ptgms.tosdr.R
28 | import xyz.ptgms.tosdr.api.models.Team
29 | import xyz.ptgms.tosdr.api.models.TeamMember
30 |
31 | @OptIn(ExperimentalMaterial3Api::class)
32 | @Composable
33 | fun TeamScreen(navController: NavController) {
34 | var team by remember { mutableStateOf(null) }
35 | val scope = rememberCoroutineScope()
36 |
37 | LaunchedEffect(Unit) {
38 | scope.launch {
39 | team = fetchTeam()
40 | }
41 | }
42 |
43 | Scaffold(
44 | topBar = {
45 | TopAppBar(
46 | navigationIcon = {
47 | IconButton(onClick = { navController.navigateUp() }) {
48 | Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = stringResource(
49 | R.string.nav_back
50 | ))
51 | }
52 | },
53 | title = { Text(stringResource(R.string.team_title)) }
54 | )
55 | }
56 | ) { padding ->
57 | if (team != null) {
58 | LazyColumn(
59 | modifier = Modifier
60 | .fillMaxSize()
61 | .padding(padding)
62 | ) {
63 | item {
64 | Text(
65 | stringResource(R.string.team_founders),
66 | style = MaterialTheme.typography.titleMedium,
67 | modifier = Modifier.padding(16.dp)
68 | )
69 | }
70 | items(team!!.founders) { member ->
71 | TeamMemberCard(member = member)
72 | }
73 |
74 | item {
75 | Text(
76 | stringResource(R.string.team_current),
77 | style = MaterialTheme.typography.titleMedium,
78 | modifier = Modifier.padding(16.dp)
79 | )
80 | }
81 | items(team!!.current) { member ->
82 | TeamMemberCard(member = member)
83 | }
84 |
85 | item {
86 | Text(
87 | stringResource(R.string.team_past),
88 | style = MaterialTheme.typography.titleMedium,
89 | modifier = Modifier.padding(16.dp)
90 | )
91 | }
92 | items(team!!.past) { member ->
93 | TeamMemberCard(member = member)
94 | }
95 | }
96 | } else {
97 | Box(
98 | modifier = Modifier.fillMaxSize(),
99 | contentAlignment = Alignment.Center
100 | ) {
101 | CircularProgressIndicator()
102 | }
103 | }
104 | }
105 | }
106 |
107 | @Composable
108 | fun TeamMemberCard(member: TeamMember) {
109 | val uriHandler = LocalUriHandler.current
110 |
111 | Card(
112 | modifier = Modifier
113 | .fillMaxWidth()
114 | .padding(horizontal = 16.dp, vertical = 8.dp)
115 | ) {
116 | Column(
117 | modifier = Modifier.padding(16.dp)
118 | ) {
119 | Row(
120 | verticalAlignment = Alignment.CenterVertically,
121 | modifier = Modifier.fillMaxWidth()
122 | ) {
123 | AsyncImage(
124 | model = member.photo,
125 | contentDescription = stringResource(R.string.team_photo_desc, member.name),
126 | modifier = Modifier
127 | .size(60.dp)
128 | .clip(CircleShape),
129 | error = painterResource(id = R.drawable.ic_service_placeholder),
130 | placeholder = painterResource(id = R.drawable.ic_service_placeholder)
131 | )
132 |
133 | Spacer(modifier = Modifier.width(16.dp))
134 |
135 | Column {
136 | Text(
137 | text = member.name,
138 | style = MaterialTheme.typography.titleMedium
139 | )
140 | if (member.title.isNotEmpty()) {
141 | Text(
142 | text = member.title,
143 | style = MaterialTheme.typography.bodyMedium,
144 | color = MaterialTheme.colorScheme.onSurfaceVariant
145 | )
146 | }
147 | }
148 | }
149 |
150 | Spacer(modifier = Modifier.height(12.dp))
151 |
152 | MarkdownText(
153 | markdown = member.description,
154 | style = MaterialTheme.typography.bodyMedium
155 | )
156 |
157 | if (!member.links.isEmpty) {
158 | Spacer(modifier = Modifier.height(12.dp))
159 |
160 | Row(
161 | horizontalArrangement = Arrangement.spacedBy(8.dp)
162 | ) {
163 | member.links.email?.let { email ->
164 | IconButton(onClick = { uriHandler.openUri("mailto:$email") }) {
165 | Icon(Icons.Rounded.Email, contentDescription = stringResource(R.string.team_email))
166 | }
167 | }
168 |
169 | member.links.github?.let { github ->
170 | IconButton(onClick = { uriHandler.openUri(github) }) {
171 | Icon(painterResource(id = R.drawable.ic_github), contentDescription = "GitHub")
172 | }
173 | }
174 |
175 | member.links.website?.let { website ->
176 | IconButton(onClick = { uriHandler.openUri(website) }) {
177 | Icon(Icons.Rounded.Home, contentDescription = stringResource(R.string.team_website))
178 | }
179 | }
180 |
181 | member.links.mastodon?.let { mastodon ->
182 | IconButton(onClick = { uriHandler.openUri(mastodon) }) {
183 | Icon(painterResource(id = R.drawable.ic_mastodon), contentDescription = "Mastodon")
184 | }
185 | }
186 |
187 | member.links.twitter?.let { twitter ->
188 | IconButton(onClick = { uriHandler.openUri(twitter) }) {
189 | Icon(painterResource(id = R.drawable.ic_twitter), contentDescription = "Twitter")
190 | }
191 | }
192 | }
193 | }
194 | }
195 | }
196 | }
197 |
198 | private suspend fun fetchTeam(): Team? {
199 | return try {
200 | val retrofit = Retrofit.Builder()
201 | .baseUrl("https://tosdr.org/api/")
202 | .addConverterFactory(GsonConverterFactory.create())
203 | .build()
204 |
205 | val service: suspend () -> Team = retrofit.create(TeamService::class.java)::getTeam
206 | service()
207 | } catch (_: Exception) {
208 | null
209 | }
210 | }
211 |
212 | fun interface TeamService {
213 | @GET("teams")
214 | suspend fun getTeam(): Team
215 | }
216 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ja-rJP/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 設定
4 | クイックアクション
5 | ToS;DRについて
6 | チーム
7 | 寄付
8 | サービスを検索…
9 | 検索をクリア
10 | 戻る
11 | 創設者
12 | 現在のチームメンバー
13 | 過去の貢献者
14 | %1$sの写真
15 | メール
16 | ウェブサイト
17 | 設定
18 | 検索
19 | サーバー検索を優先
20 | ローカルデータベースの代わりに常にオンライン検索を使用
21 | データベース
22 | データベースがダウンロードされていません
23 | オフライン機能を有効にするにはデータベースをダウンロード
24 | 最終更新
25 | データベースが最後に更新されたとき
26 | MMM dd, yyyy
27 | サービス
28 | データベース内のサービス数
29 | 更新中…
30 | データベースを更新
31 | データベースを削除
32 | エラー
33 | データベースの更新に失敗しました。もう一度試して下さい。
34 | OK
35 | ブラウザで開く
36 | 読み込み中…
37 | %1$s ロゴ
38 | レビュー済み
39 | グレード %1$s
40 | %1$s ポイント
41 | ブロッカーポイント
42 | 悪いポイント
43 | 良いポイント
44 | 中立なポイント
45 | 警告
46 | 上記のポイントは機械翻訳されています。%正確でない可能性があるため、正確性を確認するために元のポイントを確認することをお勧めします。
47 | 元のタイトルを表示
48 | ポイントの詳細
49 | 元のポイント
50 | 説明
51 | アクション
52 | ToS;DRで開く
53 | 情報
54 | ようこそ!
55 | ToS;DRへようこそ!
56 | これにより、ToS;DRについて知っておくべきすべてのことを案内します!以下のいずれかをクリックして、詳細をご覧ください!
57 | 組織
58 | \"Terms of Service; Didn\'t Read\" (short: ToS;DR) is a young project started in June 2012 to help fix the \"biggest lie on the web\": almost no one really reads the terms of service we agree to all the time. We aim at rating popular web services Terms of Service and Privacy Policies by summarizing them in \"convenient\" grades from A to E with so called \"Points\".
59 | ToS;DRは非営利団体であり、すべてのチームメンバーと貢献者はボランティアとして働いており、報酬は稀です。私たちはインフラと運営を維持するために寄付に依存しており、財務状況はウェブサイトと集団サイトで公表されています。
60 | 用語
61 | グレード
62 | ポイント
63 | サービス
64 | 貢献する
65 | 利用規約をキュレート
66 | このアプリ
67 | ライブラリー
68 | グレード
69 | その他
70 | 優秀
71 | 私たちの最高の評価: このサービスはあなたのプライバシーを尊重します。
72 | 良い
73 | かなり良い評価: このサービスはユーザーに対して公平であり、軽微な調整が必要な場合があります。
74 | まあまあ
75 | このサービスはまあまあです。条件はまあまあですが、いくつかの問題を検討する必要があります。
76 | 悪い
77 | このサービスの条件は不均一であるか、注意が必要な問題があります。
78 | ひどい
79 | 私たちの最低の評価: このサービスはプライバシーに関する深刻な懸念を引き起こします。
80 | 利用不可
81 | このサービスは、正確なグレードを表示するのに十分なキュレートされたポイントを受け取っていません。自由に貢献してください!
82 | ライブラリー
83 | 説明なし
84 | ウェブサイトを開く
85 | ポイント
86 | 分類
87 | ブロッカー
88 | このポイントはあなたのユーザー権利および/またはプライバシーに重大な影響を与えます。このポイントは直ちにサービスを最低評価として分類します。
89 | 悪い
90 | このポイントはあなたのユーザー権利および/またはプライバシーに悪影響を与えます。ご注意ください。
91 | 良い
92 | このポイントはあなたのユーザー権利および/またはプライバシーにとって価値があり、良いものであると目立っています!
93 | 中立
94 | このポイントはあなたのユーザー権利および/またはプライバシーにとって良くも悪くもありません。
95 | グレード計算
96 | サービスのグレードは、いくつのポイントを持ち、それがどの種類のものであるかに基づいて計算されます。この計算は頻繁に変更されますが、一般的な考え方は次の通りです。
97 | ユーザーに優れたポイントが多いサービスはAに分類されるべきです
98 | 優れたポイントが多く、一部にネガティブなポイントがあるサービスはBに分類されるべきです
99 | 優れたポイントとネガティブなポイントが少しあるサービスはCに分類されるべきです
100 | 優れたポイントがほとんどなく、多くのネガティブなポイントがあるサービスはDに分類されるべきです
101 | ブロッカーを持つサービスはEに分類されるべきです
102 | サービス
103 | サービスバッジ
104 | レビュー状態
105 | ToS;DRでは、従来「包括的にレビュー済み」として参照されており、このサービスには日常的な評価に十分なほど正確とみなされる十分なキュレーションポイントがあることを意味します。
106 | サービス内容
107 | 各サービスには、最終的な評価を決定するポイント、ToS;DRに関連するすべてのポリシーへのリンク、および他の有用な情報が含まれています。
108 | ToS;DRをサポートする
109 | ご寄付は私たちのサービスを維持し改善するのに役立ちます。以下の金額を選択してください:
110 | 金額を選択
111 | 購入
112 | ご寄付は、皆様のためにToS;DRを維持し改善するのに役立ちます。
113 | どういたしまして!
114 | ToS;DR 評価検索
115 | %1$s の評価を調べますか?
116 | 申し訳ありませんが、結果は見つかりませんでした。
117 | 寄付オプション
118 | Google Playストア外でToS;DRをご利用いただきありがとうございます!皆様のサポートは私たちにとって非常に重要です。寄付オプションを是非ご覧いただき、私たちのミッションにご協力ください。皆様の支援に心より感謝致します!
119 | OpenCollectiveで私たちをサポート
120 | 暗号通貨
121 | 情報
122 | ToS;DR; バージョン %1$s
123 | バリアント 「%1$s」 を実行しています。
124 | API設定
125 | APIエンドポイント
126 | APIエンドポイントを選択
127 | カスタムURL…
128 |
129 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/ptgms/tosdr/screens/about/AboutScreen.kt:
--------------------------------------------------------------------------------
1 | package xyz.ptgms.tosdr.screens.about
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
9 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
10 | import androidx.compose.material.icons.rounded.*
11 | import androidx.compose.material3.*
12 | import androidx.compose.runtime.*
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.unit.dp
18 | import androidx.navigation.NavController
19 | import xyz.ptgms.tosdr.R
20 | import xyz.ptgms.tosdr.components.settings.SettingsGroup
21 | import xyz.ptgms.tosdr.components.settings.SettingsRow
22 | import xyz.ptgms.tosdr.components.settings.SettingsTitle
23 | import xyz.ptgms.tosdr.navigation.Screen
24 |
25 | @OptIn(ExperimentalMaterial3Api::class)
26 | @Composable
27 | fun AboutScreen(navController: NavController) {
28 | val context = LocalContext.current
29 |
30 | Scaffold(
31 | topBar = {
32 | TopAppBar(
33 | navigationIcon = {
34 | IconButton(onClick = { navController.navigateUp() }) {
35 | Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
36 | }
37 | },
38 | title = { Text(stringResource(R.string.about_title)) }
39 | )
40 | }
41 | ) { padding ->
42 | LazyColumn(
43 | modifier = Modifier
44 | .fillMaxSize()
45 | .padding(padding)
46 | .padding(horizontal = 16.dp)
47 | ) {
48 | item {
49 | SettingsTitle(text = stringResource(R.string.about_welcome))
50 | SettingsGroup {
51 | SettingsRow(
52 | leading = {
53 | Icon(
54 | painterResource(R.drawable.ic_rounded_celebration_24),
55 | contentDescription = null,
56 | tint = MaterialTheme.colorScheme.primary
57 | )
58 | },
59 | title = { Text(stringResource(R.string.about_welcome_title)) }
60 | )
61 |
62 | SettingsRow(
63 | title = {
64 | Text(
65 | stringResource(R.string.about_welcome_desc),
66 | style = MaterialTheme.typography.bodyMedium
67 | )
68 | }
69 | )
70 | }
71 | }
72 |
73 | item {
74 | SettingsTitle(text = stringResource(R.string.about_organization))
75 | SettingsGroup {
76 | SettingsRow(
77 | title = {
78 | Text(
79 | stringResource(R.string.about_organization_desc1),
80 | style = MaterialTheme.typography.bodyMedium
81 | )
82 | }
83 | )
84 |
85 | SettingsRow(
86 | title = {
87 | Text(
88 | stringResource(R.string.about_organization_desc2),
89 | style = MaterialTheme.typography.bodyMedium
90 | )
91 | }
92 | )
93 | }
94 | }
95 |
96 | item {
97 | SettingsTitle(text = stringResource(R.string.about_terminology))
98 | SettingsGroup {
99 | SettingsRow(
100 | leading = {
101 | Icon(
102 | painterResource(R.drawable.ic_rounded_school_24),
103 | contentDescription = null,
104 | tint = MaterialTheme.colorScheme.primary
105 | )
106 | },
107 | title = { Text(stringResource(R.string.about_grades)) },
108 | trailing = {
109 | Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, contentDescription = null)
110 | },
111 | onClick = { navController.navigate(Screen.GradesExplained.route) }
112 | )
113 |
114 | SettingsRow(
115 | leading = {
116 | Icon(
117 | painterResource(R.drawable.ic_rounded_format_quote_24),
118 | contentDescription = null,
119 | tint = MaterialTheme.colorScheme.primary
120 | )
121 | },
122 | title = { Text(stringResource(R.string.about_points)) },
123 | trailing = {
124 | Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, contentDescription = null)
125 | },
126 | onClick = { navController.navigate(Screen.PointsExplained.route) }
127 | )
128 |
129 | SettingsRow(
130 | leading = {
131 | Icon(
132 | painterResource(R.drawable.ic_rounded_home_storage_24),
133 | contentDescription = null,
134 | tint = MaterialTheme.colorScheme.primary
135 | )
136 | },
137 | title = { Text(stringResource(R.string.about_services)) },
138 | trailing = {
139 | Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, contentDescription = null)
140 | },
141 | onClick = { navController.navigate(Screen.ServicesExplained.route) }
142 | )
143 | }
144 | }
145 |
146 | item {
147 | SettingsTitle(text = stringResource(R.string.about_contribute))
148 | SettingsGroup {
149 | SettingsRow(
150 | leading = {
151 | Icon(
152 | Icons.Rounded.Search,
153 | contentDescription = null,
154 | tint = MaterialTheme.colorScheme.primary
155 | )
156 | },
157 | title = { Text(stringResource(R.string.about_curate)) },
158 | trailing = {
159 | Icon(
160 | painterResource(R.drawable.ic_rounded_open_in_browser_24),
161 | contentDescription = null
162 | )
163 | },
164 | onClick = {
165 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://edit.tosdr.org"))
166 | context.startActivity(intent)
167 | }
168 | )
169 | }
170 | }
171 |
172 | item {
173 | SettingsTitle(text = stringResource(R.string.about_this_app))
174 | SettingsGroup {
175 | SettingsRow(
176 | leading = {
177 | Icon(
178 | Icons.Rounded.Favorite,
179 | contentDescription = null,
180 | tint = MaterialTheme.colorScheme.primary
181 | )
182 | },
183 | title = { Text(stringResource(R.string.about_libraries)) },
184 | trailing = {
185 | Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, contentDescription = null)
186 | },
187 | onClick = { navController.navigate(Screen.Libraries.route) }
188 | )
189 | }
190 | }
191 |
192 | item {
193 | Spacer(modifier = Modifier.height(16.dp))
194 | }
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ToS;DR
3 | Settings
4 | Quick Actions
5 | About ToS;DR
6 | Team
7 | Donate
8 | Search for services…
9 | Clear search
10 | Back
11 | Founders
12 | Current Team
13 | Past Contributors
14 | %1$s\'s photo
15 | Email
16 | Website
17 | Settings
18 | Search
19 | Prefer Server Search
20 | Always use online search instead of local database
21 | Database
22 | Database not downloaded
23 | Download the database to enable offline functionality
24 | Last Update
25 | When the database was last refreshed
26 | MMM dd, yyyy
27 | Services
28 | Number of services in the database
29 | Refreshing…
30 | Refresh Database
31 | Delete Database
32 | Error
33 | Failed to update the database. Please try again.
34 | OK
35 | Open in Browser
36 | Loading…
37 | %1$s logo
38 | Reviewed
39 | Grade %1$s
40 | %1$s Points
41 | Blocker Points
42 | Bad Points
43 | Good Points
44 | Neutral Points
45 | Warning
46 | The points above have been machine translated and may not be accurate. We recommend checking the original titles to ensure accuracy.
47 | Show original titles
48 | Point Details
49 | Original Point
50 | Description
51 | Actions
52 | Open on ToS;DR
53 | About
54 | Welcome!
55 | Welcome to ToS;DR!
56 | This will guide you through everything there is to know about ToS;DR! Feel free to click anything below to learn more!
57 | Organization
58 | \"Terms of Service; Didn\'t Read\" (short: ToS;DR) is a young project started in June 2012 to help fix the \"biggest lie on the web\": almost no one really reads the terms of service we agree to all the time. We aim at rating popular web services Terms of Service and Privacy Policies by summarizing them in \"convenient\" grades from A to E with so called \"Points\".
59 | ToS;DR is a non-profit organization, and all of our team members and contributors do their work as volunteers, with payment being rare. We rely on donations to keep our infrastructure and operations up, and our finances are laid out through our website and collective websites.
60 | Terminology
61 | Grades
62 | Points
63 | Services
64 | Contribute
65 | Curate Terms of Service
66 | This App
67 | Libraries
68 | Grades
69 | Other
70 | Excellent
71 | Our best grade: This service respects your privacy.
72 | Good
73 | A pretty good grade: This service are fair for the user and could use minor adjustments.
74 | Okay
75 | This service is okay. The terms are okay, but some issues need your consideration.
76 | Bad
77 | This service\'s terms are uneven or there are some issues that need your attention.
78 | Awful
79 | Our worst grade: This service raises some serious concerns regarding privacy.
80 | Not Available
81 | This service has not received enough curated points to display an accurate grade. Feel free to contribute!
82 | Libraries
83 | No description
84 | Open Website
85 | Points
86 | Classifications
87 | Blocker
88 | This point has severe effects on your user rights and/or privacy. This point immediately classifies a service as having the worst grade.
89 | Bad
90 | This point negatively impacts your user rights and/or privacy. Be advised.
91 | Good
92 | This point stands out as being valuable and good for your user rights and/or privacy!
93 | Neutral
94 | This point is neither good nor bad for your user rights and/or privacy.
95 | Grade Calculation
96 | The grade for an Service is calculated based on how many points it has and what type they are. These calculations change frequently, however, the general idea is as follows:
97 | Services with many great points for the user should be classified as an A
98 | Services with many great points and some negative ones should be classified as an B
99 | Services with some great points and some negative ones should be classified as an C
100 | Services with few great points and many negative ones should be classified as an D
101 | Services with a blocker should be classified as an E
102 | Services
103 | Service Badges
104 | Review Status
105 | In ToS;DR classically referred to as \'Comprehensively Reviewed\', meaning this service has enough curated points to be deemed accurate enough for an everyday rating.
106 | Service Contents
107 | Each Service includes Points that determine a final Grade, Links to all policies that are relevant to ToS;DR and other useful information.
108 | Support ToS;DR
109 | Your donation helps us maintain and improve our services. Choose an amount below:
110 | Choose Amount
111 | Purchase
112 | Your donation helps us maintain and improve ToS;DR for everyone.
113 | Thank You!
114 | ToS;DR Rating Lookup
115 | Do you wish to look up the rating for %1$s?
116 | I could not find any results, sorry!
117 | Donation Options
118 | Thank you for using ToS;DR outside of the Google Play store! Your support is means a lot to us. We invite you to explore our donation options and contribute to our mission. Every bit of support is greatly appreciated!
119 | Support us on OpenCollective
120 | Cryptocurrency
121 | About
122 | ToS;DR; Version %1$s
123 | You are running the variant \'%1$s\'.
124 | API Settings
125 | API Endpoint
126 | Select API endpoint
127 | Custom URL...
128 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru-rRU/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Настройки
4 | Быстрые действия
5 | О нас ToS;DR
6 | Команда
7 | Пожертвовать
8 | Поиск услуги…
9 | Очистить поиск
10 | Назад
11 | Основатели
12 | Текущие члены команды
13 | Бывшие участники
14 | Фото %1$s
15 | Электронная почта
16 | Вебсайт
17 | Настройки
18 | Поиск
19 | Предпочитать серверный поиск
20 | Всегда использовать онлайн-поиск вместо локальной базы данных
21 | База данных
22 | База данных не загружена
23 | Скачайте базу данных, чтобы включить офлайн-функциональность
24 | Последнее обновление
25 | Когда база данных была обновлена в последний раз
26 | dd MMM yyyy
27 | Услуги
28 | Количество услуг в базе данных
29 | Обновление…
30 | Обновить базу данных
31 | Удалить базу данных
32 | Ошибка
33 | Не удалось обновить базу данных. Пожалуйста, попробуйте ещё раз.
34 | ОК
35 | Открыть в браузере
36 | Загрузка…
37 | логотип %1$s
38 | Проверено
39 | Оценка %1$s
40 | %1$s баллов
41 | Блокирующие баллы
42 | Плохие баллы
43 | Хорошие баллы
44 | Нейтральные баллы
45 | Предупреждение
46 | Пункты выше были переведены машиной. Они могут быть не на % точными, и мы рекомендуем вам проверить оригинальный пункт для подтверждения точности.
47 | Показать оригинальные названия
48 | Детали пункта
49 | Оригинальный пункт
50 | Описание
51 | Действия
52 | Открыть на ToS;DR
53 | О нас
54 | Добро пожаловать!
55 | Добро пожаловать в ToS;DR!
56 | Это руководство поможет вам узнать всё о ToS;DR! Не стесняйтесь нажимать на любой элемент ниже, чтобы узнать больше!
57 | Организация
58 | \"Условия обслуживания; Не читал\" (сокращенно: ToS;DR) — это проект, запущенный в июне 2012 года, чтобы помочь исправить \"самую большую ложь в интернете\": почти никто не читает условия использования, с которыми мы соглашаемся постоянно\".
59 | ToS;DR — это некоммерческая организация, и все наши члены команды и участники работают как волонтеры, получение оплаты редкое. Мы зависим от пожертвований для поддержания нашей инфраструктуры и операций, и наши финансы изложены на нашем вебсайте и коллективных сайтах.
60 | Терминология
61 | Оценки
62 | Баллы
63 | Услуги
64 | Внести вклад
65 | Курировать Условия использования
66 | Это приложение
67 | Библиотеки
68 | Оценки
69 | Другие
70 | Отлично
71 | Наша лучшая оценка: этот сервис уважает вашу конфиденциальность.
72 | Хорошо
73 | Довольно хорошая оценка: этот сервис справедлив по отношению к пользователю и может нуждаться в незначительных коррективах.
74 | Средне
75 | Условия обслуживания приемлемы, но некоторые вопросы требуют вашего внимания.
76 | Плохо
77 | Условия обслуживания очень неравномерны, или есть некоторые важные вопросы, которые требуют вашего внимания.
78 | Ужасно
79 | Наша худшая оценка: этот сервиз вызывает серьезные опасения в отношении конфиденциальности.
80 | Недоступно
81 | Эта услуга не получила достаточно курированных баллов для отображения точной оценки. Всегда рады вашему вкладу!
82 | Библиотеки
83 | Нет описания
84 | Открыть сайт
85 | Баллы
86 | Классификации
87 | Блокатор
88 | Этот балл сильно влияет на ваши пользовательские права и/или конфиденциальность. Этот балл немедленно классифицирует услугу как имеющую наихудшую оценку.
89 | Плохо
90 | Этот балл негативно влияет на ваши пользовательские права и/или конфиденциальность. Имейте это в виду.
91 | Хорошо
92 | Этот балл выделяется как полезный и хороший для ваших пользовательских прав и/или конфиденциальности!
93 | Нейтрально
94 | Этот балл не является ни хорошим, ни плохим для ваших пользовательских прав и/или конфиденциальности.
95 | Расчет оценки
96 | Оценка за сервис рассчитывается на основе того, сколько у него баллов и какие они. Эти расчеты часто меняются, однако основная идея следующая:
97 | Услуги с множеством отличных баллов для пользователя должны быть классифицированы как A
98 | Услуги с множеством отличных баллов и некоторыми плохими должны быть классифицированы как B
99 | Услуги с несколькими отличными баллами и несколькими плохими должны быть классифицированы как C
100 | Услуги с небольшим количеством отличных баллов и множеством плохих должны быть классифицированы как D
101 | Услуги с блокатором должны быть классифицированы как E
102 | Услуги
103 | Значки сервиса
104 | Статус проверки
105 | В ToS;DR классически именуется \"Полностью проверено\", что означает, что у этого сервиса достаточно курируемых баллов для ежедневной оценки.
106 | Содержимое сервиса
107 | Каждый сервис включает баллы, определяющие конечную оценку, ссылки на все политики, относящиеся к ToS;DR, и другую полезную информацию.
108 | Поддержите ToS;DR
109 | Ваше пожертвование помогает нам поддерживать и улучшать наши Услуги. Выберите сумму ниже:
110 | Выберите сумму
111 | Покупка
112 | Ваше пожертвование помогает нам поддерживать и улучшать ToS;DR для всех.
113 | Пожалуйста!
114 | Поиск рейтинга ToS;DR
115 | Ты хочешь посмотреть рейтинг для %1$s?
116 | Я не смог найти никаких результатов, извини!
117 | Опции пожертвований
118 | Спасибо за использование ToS;DR вне магазина Google Play! Ваша поддержка очень важна для нас. Мы приглашаем вас изучить наши опции пожертвований и внести вклад в нашу миссию. Каждая поддержка очень ценится!
119 | Поддержите нас на OpenCollective
120 | Криптовалюта
121 | О нас
122 | ToS;DR; Версия %1$s
123 | Вы используете вариант \'%1$s\'.
124 | Настройки API
125 | API-Endpoint
126 | Выбрать API-endpoint
127 | Пользовательский URL...
128 |
129 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pl-rPL/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Ustawienia
4 | Szybkie działania
5 | O ToS;DR
6 | Zespół
7 | Darowizna
8 | Szukaj usługi…
9 | Wyczyść wyszukiwanie
10 | Powrót
11 | Założyciele
12 | Aktualny zespół
13 | Poprzedni współpracownicy
14 | Zdjęcie %1$s
15 | E-mail
16 | Strona internetowa
17 | Ustawienia
18 | Szukaj
19 | Preferuj wyszukiwanie serwerowe
20 | Zawsze używaj wyszukiwania online zamiast lokalnej bazy danych
21 | Baza danych
22 | Baza danych nie została pobrana
23 | Pobierz bazę danych, aby umożliwić funkcjonalność offline
24 | Ostatnia aktualizacja
25 | Kiedy baza danych została ostatnio odświeżona
26 | MMM dd, yyyy
27 | Usługi
28 | Liczba usług w bazie danych
29 | Odświeżanie…
30 | Odśwież bazę danych
31 | Usuń bazę danych
32 | Błąd
33 | Nie udało się zaktualizować bazy danych. Spróbuj ponownie.
34 | OK
35 | Otwórz w przeglądarce
36 | Ładowanie…
37 | logo %1$s
38 | Przejrzane
39 | Ocena %1$s
40 | %1$s Punkty
41 | Blokujące Punkty
42 | Złe Punkty
43 | Dobre Punkty
44 | Neutralne Punkty
45 | Ostrzeżenie
46 | Powyższe punkty zostały maszynowo przetłumaczone i mogą nie być dokładne. Zalecamy sprawdzenie oryginalnych tytułów, aby upewnić się co do dokładności.
47 | Pokaż oryginalne tytuły
48 | Szczegóły punktu
49 | Oryginalny Punkt
50 | Opis
51 | Czynności
52 | Otwórz w ToS;DR
53 | O nas
54 | Witaj!
55 | Witaj w ToS;DR!
56 | Ta aplikacja poprowadzi cię przez wszystko, co jest do wiedzenia o ToS;DR! Śmiało, kliknij cokolwiek poniżej, aby dowiedzieć się więcej!
57 | Organizacja
58 | \"Warunki Usługi; Nie Przeczytałem\" (skrót: ToS;DR) to projekt rozpoczęty w czerwcu 2012 roku w celu naprawy \"największego kłamstwa w sieci\": prawie nikt tak naprawdę nie czyta warunków usługi, które akceptujemy cały czas. Naszym celem jest ocena popularnych usług internetowych pod względem Warunków Usługi oraz Polityki Prywatności poprzez ich podsumowanie w \"dogodnych\" ocenach od A do E z tak zwanymi \"Punktami\".
59 | ToS;DR to organizacja non-profit, a wszyscy nasi członkowie zespołu i współpracownicy pracują jako wolontariusze, gdzie wynagrodzenie jest rzadkością. Polegamy na darowiznach, aby utrzymać naszą infrastrukturę i działania, a nasze finanse są przedstawione na naszej stronie internetowej i stronach zbiorowych.
60 | Terminologia
61 | Oceny
62 | Punkty
63 | Usługi
64 | Pomóż
65 | Kurator Warunki Usługi
66 | Ta aplikacja
67 | Biblioteki
68 | Oceny
69 | Inne
70 | Doskonałe
71 | Nasza najlepsza ocena: Ta usługa szanuje twoją prywatność.
72 | Dobry
73 | Całkiem dobra ocena: Ta usługa jest uczciwa wobec użytkownika i może wymagać drobnych korekt.
74 | Średnio
75 | Regulaminy usług są przyzwoite, ale pewne kwestie wymagają twojej uwagi.
76 | Zły
77 | Regulaminy usług są bardzo nierówne lub istnieją istotne kwestie, które wymagają twojej uwagi.
78 | Okropny
79 | Nasza najgorsza ocena: Ta usługa wywołuje poważne obawy dotyczące prywatności.
80 | Brak dostępności
81 | Ta usługa nie otrzymała wystarczającej liczby opracowanych punktów, aby wyświetlić dokładną ocenę. Zachęcamy do współpracy!
82 | Biblioteki
83 | Brak opisu
84 | Otwórz stronę internetową
85 | Punkty
86 | Klasyfikacje
87 | Blokujące
88 | Ten punkt ma poważne skutki dla twoich praw użytkownika i/lub prywatności. Ten punkt natychmiast klasyfikuje usługę jako mającą najgorszą ocenę.
89 | Zły
90 | Ten punkt negatywnie wpływa na twoje prawa użytkownika i/lub prywatność. Zostałeś ostrzeżony.
91 | Dobry
92 | Ten punkt wyróżnia się jako wartościowy i korzystny dla twoich praw użytkownika i/lub prywatności!
93 | Neutralny
94 | Ten punkt nie jest ani dobry, ani zły dla twoich praw użytkownika i/lub prywatności.
95 | Obliczanie oceny
96 | Ocena dla Usługi jest obliczana na podstawie liczby punktów, jakie posiada i jakiego są one rodzaju. Te obliczenia często się zmieniają, jednak ogólna idea jest następująca:
97 | Usługi z wieloma doskonałymi punktami dla użytkownika powinny być sklasyfikowane jako A
98 | Usługi z wieloma doskonałymi punktami i kilkoma negatywnymi powinny być sklasyfikowane jako B
99 | Usługi z kilkoma doskonałymi punktami i kilkoma negatywnymi powinny być sklasyfikowane jako C
100 | Usługi z niewieloma doskonałymi punktami i wieloma negatywnymi powinny być sklasyfikowane jako D
101 | Usługi z blokerem powinny być sklasyfikowane jako E
102 | Usługi
103 | Odznaki Usługi
104 | Status Przeglądu
105 | W ToS;DR klasycznie odnosi się do \'Kompleksowo Przejrzane\', co oznacza, że ta usługa ma wystarczającą ilość dopracowanych punktów, aby uznano ją za wystarczająco dokładną do codziennej oceny.
106 | Zawartość Usługi
107 | Każda Usługa zawiera Punkty, które określają końcową Ocenę, Linki do wszystkich zasad dotyczących ToS;DR oraz inne przydatne informacje.
108 | Wesprzyj ToS;DR
109 | Twoja darowizna pomaga nam utrzymać i poprawić nasze usługi. Wybierz kwotę poniżej:
110 | Wybierz Kwotę
111 | Zakup
112 | Twoja darowizna pomaga nam utrzymać i poprawić ToS;DR dla wszystkich.
113 | Dziękuję!
114 | ToS;DR Wyszukiwanie Oceny
115 | Czy chcesz wyszukać ocenę dla %1$s?
116 | Nie mogłem znaleźć żadnych wyników, przepraszam!
117 | Opcje darowizn
118 | Dziękujemy za korzystanie z ToS;DR poza sklepem Google Play! Twoje wsparcie wiele dla nas znaczy. Zapraszamy do zapoznania się z naszymi opcjami darowizn i wsparcia naszej misji. Każda pomoc jest bardzo doceniana!
119 | Wspieraj nas na OpenCollective
120 | Kryptowaluty
121 | O nas
122 | ToS;DR; Wersja %1$s
123 | Korzystasz z wariantu \"%1$s\".
124 | Ustawienia API
125 | Punkt końcowy API
126 | Wybierz punkt końcowy API
127 | Niestandardowy URL...
128 |
129 |
--------------------------------------------------------------------------------