Skip to content
This repository was archived by the owner on Jul 26, 2025. It is now read-only.

Commit b6970b1

Browse files
authored
Create a screen to view information about available apps (#439)
1 parent 810f54b commit b6970b1

File tree

20 files changed

+715
-17
lines changed

20 files changed

+715
-17
lines changed

app/src/test/kotlin/com/nasdroid/DITest.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package com.nasdroid
33
import android.app.Application
44
import android.content.Context
55
import androidx.lifecycle.SavedStateHandle
6+
import io.mockk.every
7+
import io.mockk.mockk
68
import io.mockk.mockkClass
79
import kotlinx.coroutines.Dispatchers
810
import kotlinx.coroutines.ExperimentalCoroutinesApi
11+
import kotlinx.coroutines.flow.MutableStateFlow
912
import kotlinx.coroutines.test.StandardTestDispatcher
1013
import kotlinx.coroutines.test.setMain
1114
import org.koin.core.context.startKoin
@@ -16,9 +19,16 @@ import kotlin.test.Test
1619

1720
class DITest {
1821

22+
private lateinit var mockSavedStateHandle: SavedStateHandle
23+
1924
@OptIn(ExperimentalCoroutinesApi::class)
2025
@BeforeTest
2126
fun setUp() {
27+
mockSavedStateHandle = mockk()
28+
every { mockSavedStateHandle.get<String>(any()) } returns "1"
29+
every { mockSavedStateHandle.getStateFlow<String?>(any(), any()) } answers {
30+
MutableStateFlow(args[1] as String?)
31+
}
2232
Dispatchers.setMain(StandardTestDispatcher())
2333
MockProvider.register { clazz -> mockkClass(clazz, relaxed = true) }
2434
}
@@ -30,7 +40,7 @@ class DITest {
3040
checkModules {
3141
withInstance<Context>()
3242
withInstance<Application>()
33-
withInstance<SavedStateHandle>()
43+
withInstance(mockSavedStateHandle)
3444
}
3545
}
3646
}

config/detekt.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ complexity:
100100
StringLiteralDuplication:
101101
active: true
102102
threshold: 3
103+
ignoreAnnotated:
104+
- 'Preview'
105+
- 'PreviewScreenSize'
106+
- 'PreviewFontScale'
107+
- 'PreviewLightDark'
108+
- 'PreviewDynamicColors'
103109
TooManyFunctions:
104110
active: true
105111
ignoreOverridden: true

core/api/src/commonMain/kotlin/com/nasdroid/api/v2/catalog/CatalogV2Api.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.nasdroid.api.TimestampUnwrapper
44
import com.nasdroid.api.exception.HttpNotOkException
55
import kotlinx.serialization.SerialName
66
import kotlinx.serialization.Serializable
7+
import kotlinx.serialization.json.JsonObject
78

89
/**
910
* Describes the TrueNAS API V2 "Catalog" group. The Catalogs API is responsible for managing
@@ -132,6 +133,7 @@ value class CatalogItems(
132133
* @property screenshotUrls A list of URLs for available screenshots of the app this item runs.
133134
* @property sourceUrls A list of URLs for available source code this catalog item uses.
134135
* @property iconUrl The URL of the catalog item icon.
136+
* @property versions Contains all metadata for available versions.
135137
*/
136138
@Serializable
137139
data class CatalogItem(
@@ -174,6 +176,8 @@ data class CatalogItem(
174176
val sourceUrls: List<String>,
175177
@SerialName("icon_url")
176178
val iconUrl: String?,
179+
@SerialName("versions")
180+
val versions: JsonObject
177181
) {
178182

179183
/**

core/design/src/main/kotlin/com/nasdroid/design/Paddings.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package com.nasdroid.design
22

3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.foundation.layout.calculateEndPadding
5+
import androidx.compose.foundation.layout.calculateStartPadding
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.remember
38
import androidx.compose.runtime.staticCompositionLocalOf
9+
import androidx.compose.ui.platform.LocalLayoutDirection
410
import androidx.compose.ui.unit.Dp
511
import androidx.compose.ui.unit.dp
612

@@ -38,3 +44,19 @@ fun defaultPaddings(): Paddings = Paddings(
3844
* value of this CompositionLocal, use [MaterialThemeExt.paddings].
3945
*/
4046
val LocalPaddings = staticCompositionLocalOf { defaultPaddings() }
47+
48+
/**
49+
* Adds [this] PaddingValues to [other] PaddingValues.
50+
*/
51+
@Composable
52+
operator fun PaddingValues.plus(other: PaddingValues): PaddingValues {
53+
val layoutDirection = LocalLayoutDirection.current
54+
return remember(this, other) {
55+
PaddingValues(
56+
start = this.calculateStartPadding(layoutDirection) + other.calculateStartPadding(layoutDirection),
57+
top = this.calculateTopPadding() + other.calculateTopPadding(),
58+
end = this.calculateEndPadding(layoutDirection) + other.calculateEndPadding(layoutDirection),
59+
bottom = this.calculateBottomPadding() + other.calculateBottomPadding()
60+
)
61+
}
62+
}

features/apps/logic/src/main/kotlin/com/nasdroid/apps/logic/DI.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.nasdroid.apps.logic.discover.GetAvailableApps
66
import com.nasdroid.apps.logic.discover.GetAvailableCatalogs
77
import com.nasdroid.apps.logic.discover.GetAvailableCategories
88
import com.nasdroid.apps.logic.discover.GetSimilarApps
9+
import com.nasdroid.apps.logic.discover.StripHtmlTags
910
import com.nasdroid.apps.logic.installed.DeleteApp
1011
import com.nasdroid.apps.logic.installed.GetAppLogs
1112
import com.nasdroid.apps.logic.installed.GetInstalledApp
@@ -33,6 +34,7 @@ val AppsLogicModule = module {
3334
factoryOf(::GetAvailableCatalogs)
3435
factoryOf(::GetAvailableCategories)
3536
factoryOf(::GetSimilarApps)
37+
factoryOf(::StripHtmlTags)
3638

3739
factoryOf(::DeleteApp)
3840
factoryOf(::GetAppLogs)

features/apps/logic/src/main/kotlin/com/nasdroid/apps/logic/discover/GetAvailableAppDetails.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import kotlinx.datetime.Instant
1010
* [invoke] for details.
1111
*/
1212
class GetAvailableAppDetails(
13-
private val catalogV2Api: CatalogV2Api
13+
private val catalogV2Api: CatalogV2Api,
14+
private val stripHtmlTags: StripHtmlTags
1415
) {
1516

1617
/**
@@ -35,11 +36,12 @@ class GetAvailableAppDetails(
3536
}
3637
val details = AvailableAppDetails(
3738
id = catalogDetails.name,
39+
iconUrl = catalogDetails.iconUrl.orEmpty(),
3840
name = catalogDetails.title,
3941
version = catalogDetails.latestAppVersion,
4042
tags = catalogDetails.tags,
4143
homepage = catalogDetails.homeUrl,
42-
description = catalogDetails.description.orEmpty(),
44+
description = stripHtmlTags(catalogDetails.appReadme),
4345
screenshots = catalogDetails.screenshotUrls,
4446
sources = catalogDetails.sourceUrls,
4547
lastUpdatedAt = Instant.fromEpochMilliseconds(catalogDetails.lastUpdate),
@@ -66,6 +68,7 @@ class GetAvailableAppDetails(
6668
*
6769
* @property id A unique identifier for this app within its catalog and train. This is usually
6870
* similar to the app name.
71+
* @property iconUrl The URL for the app icon.
6972
* @property name The name of the app.
7073
* @property version The version of the app that these details are for. This is usually also the
7174
* latest version of the app.
@@ -80,6 +83,7 @@ class GetAvailableAppDetails(
8083
*/
8184
data class AvailableAppDetails(
8285
val id: String,
86+
val iconUrl: String,
8387
val name: String,
8488
val version: String,
8589
val tags: List<String>,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.nasdroid.apps.logic.discover
2+
3+
/**
4+
* Strips all header text, and other HTML tags from a String. See [invoke] for details.
5+
*/
6+
class StripHtmlTags {
7+
8+
/**
9+
* Processes [text] such that all HTML headers (h1-h6) are removed, and general HTML tags are
10+
* stripped.
11+
*/
12+
operator fun invoke(text: String): String {
13+
return text
14+
.replace(Regex("<h[1-6]>.*</h[16]>"), "") // Remove headers
15+
.replace(Regex("<[^>]*>"), "") // Remove HTML tags
16+
.trim() // Remove any leading/trailing whitespace
17+
}
18+
}

features/apps/ui/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies {
3939
implementation(projects.core.composeLogviewer)
4040
implementation(projects.core.design)
4141
implementation(projects.core.navigation)
42+
implementation(projects.core.skeleton)
4243

4344
implementation(projects.features.apps.logic)
4445

features/apps/ui/src/main/kotlin/com/nasdroid/apps/ui/DI.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.nasdroid.apps.ui
22

33
import com.nasdroid.apps.logic.AppsLogicModule
44
import com.nasdroid.apps.ui.discover.DiscoverAppsViewModel
5+
import com.nasdroid.apps.ui.discover.details.AvailableAppDetailsViewModel
56
import com.nasdroid.apps.ui.installed.details.InstalledAppDetailsViewModel
67
import com.nasdroid.apps.ui.installed.overview.InstalledAppsOverviewViewModel
78
import com.nasdroid.apps.ui.installed.overview.logs.LogsViewModel
@@ -14,6 +15,7 @@ import org.koin.dsl.module
1415
val AppsModule = module {
1516
includes(AppsLogicModule)
1617

18+
viewModelOf(::AvailableAppDetailsViewModel)
1719
viewModelOf(::DiscoverAppsViewModel)
1820

1921
viewModelOf(::InstalledAppDetailsViewModel)

features/apps/ui/src/main/kotlin/com/nasdroid/apps/ui/Navigation.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import androidx.navigation.compose.composable
88
import androidx.navigation.navArgument
99
import androidx.navigation.navigation
1010
import com.nasdroid.apps.ui.discover.DiscoverAppsScreen
11+
import com.nasdroid.apps.ui.discover.details.AvailableAppDetailsScreen
1112
import com.nasdroid.apps.ui.installed.InstalledAppsScreen
12-
import com.nasdroid.apps.ui.installed.details.InstalledAppDetailsScreen
1313
import com.nasdroid.apps.ui.installed.overview.logs.LogsScreen
1414

1515
/**
@@ -34,10 +34,38 @@ fun NavGraphBuilder.appsGraph(
3434
}
3535
composable("discover") {
3636
DiscoverAppsScreen(
37+
onAppClick = { appId, appCatalog, appTrain ->
38+
navController.navigate("discover/$appCatalog/$appTrain/$appId")
39+
},
3740
navigateBack = { navController.popBackStack() },
3841
modifier = modifier,
3942
)
4043
}
44+
composable(
45+
route = "discover/{catalog}/{train}/{id}",
46+
arguments = listOf(
47+
navArgument("catalog") {
48+
type = NavType.StringType
49+
nullable = false
50+
},
51+
navArgument("train") {
52+
type = NavType.StringType
53+
nullable = false
54+
},
55+
navArgument("id") {
56+
type = NavType.StringType
57+
nullable = false
58+
}
59+
)
60+
) {
61+
AvailableAppDetailsScreen(
62+
onNavigateBack = navController::popBackStack,
63+
onNavigateToAppDetails = { id, catalog, train ->
64+
navController.navigate("discover/${catalog}/${train}/${id}")
65+
},
66+
modifier = modifier
67+
)
68+
}
4169
composable(
4270
route = "logs/{appName}",
4371
arguments = listOf(

0 commit comments

Comments
 (0)