Skip to content

Commit 0a67b85

Browse files
authored
Merge pull request #335 from hellcp/explore
Create appstore tab in compose
2 parents 902d958 + d09c105 commit 0a67b85

File tree

18 files changed

+751
-20
lines changed

18 files changed

+751
-20
lines changed

android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,12 @@ class KMPApiBridge @Inject constructor(
3737
activity.startActivity(intent)
3838
}
3939
}
40+
override fun openStoreView() {
41+
activity?.let {
42+
Timber.d("Opening store view")
43+
val intent = Intent(activity.context, MainActivity::class.java)
44+
intent.putExtra("navigationPath", Routes.Home.STORE_WATCHFACES)
45+
activity.startActivity(intent)
46+
}
47+
}
4048
}

android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,21 @@
44
package io.rebble.cobble.pigeons;
55

66
import android.util.Log;
7-
87
import androidx.annotation.NonNull;
98
import androidx.annotation.Nullable;
10-
9+
import io.flutter.plugin.common.BasicMessageChannel;
10+
import io.flutter.plugin.common.BinaryMessenger;
11+
import io.flutter.plugin.common.MessageCodec;
12+
import io.flutter.plugin.common.StandardMessageCodec;
1113
import java.io.ByteArrayOutputStream;
1214
import java.nio.ByteBuffer;
1315
import java.util.ArrayList;
16+
import java.util.Arrays;
1417
import java.util.Collections;
18+
import java.util.HashMap;
1519
import java.util.List;
1620
import java.util.Map;
1721

18-
import io.flutter.plugin.common.BasicMessageChannel;
19-
import io.flutter.plugin.common.BinaryMessenger;
20-
import io.flutter.plugin.common.MessageCodec;
21-
import io.flutter.plugin.common.StandardMessageCodec;
22-
2322
/** Generated class from Pigeon. */
2423
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
2524
public class Pigeons {
@@ -5458,6 +5457,8 @@ public interface KMPApi {
54585457

54595458
void openLockerView();
54605459

5460+
void openStoreView();
5461+
54615462
/** The codec used by KMPApi. */
54625463
static @NonNull MessageCodec<Object> getCodec() {
54635464
return KMPApiCodec.INSTANCE;
@@ -5500,6 +5501,28 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable KMPApi api
55005501
api.openLockerView();
55015502
wrapped.add(0, null);
55025503
}
5504+
catch (Throwable exception) {
5505+
ArrayList<Object> wrappedError = wrapError(exception);
5506+
wrapped = wrappedError;
5507+
}
5508+
reply.reply(wrapped);
5509+
});
5510+
} else {
5511+
channel.setMessageHandler(null);
5512+
}
5513+
}
5514+
{
5515+
BasicMessageChannel<Object> channel =
5516+
new BasicMessageChannel<>(
5517+
binaryMessenger, "dev.flutter.pigeon.KMPApi.openStoreView", getCodec());
5518+
if (api != null) {
5519+
channel.setMessageHandler(
5520+
(message, reply) -> {
5521+
ArrayList<Object> wrapped = new ArrayList<Object>();
5522+
try {
5523+
api.openStoreView();
5524+
wrapped.add(0, null);
5525+
}
55035526
catch (Throwable exception) {
55045527
ArrayList<Object> wrappedError = wrapError(exception);
55055528
wrapped = wrappedError;

android/shared/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ kotlin {
7171
implementation(libs.compose.viewmodel)
7272
implementation(libs.compose.components.resources)
7373
implementation(libs.compose.components.reorderable)
74+
api("io.github.kevinnzou:compose-webview-multiplatform:1.9.40")
7475
}
7576
androidMain.dependencies {
7677
implementation(libs.ktor.client.okhttp)

android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ class AuthClient(
3030

3131
return res.body() ?: error("Failed to deserialize account")
3232
}
33-
}
33+
}

android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ object RWS: KoinComponent {
1717
private val token: StateFlow<CurrentToken> by inject(named("currentToken"))
1818
private val scope = CoroutineScope(Dispatchers.Default)
1919

20+
val currentTokenFlow: StateFlow<CurrentToken> get() = token
21+
2022
val appstoreClientFlow = token.map {
2123
it.tokenOrNull?.let { t -> AppstoreClient("https://appstore-api.$domainSuffix/api", t) }
2224
}.stateIn(scope, SharingStarted.Eagerly, null)
@@ -32,4 +34,4 @@ object RWS: KoinComponent {
3234
get() = authClientFlow.value
3335
val timelineClient: TimelineClient?
3436
get() = timelineClientFlow.value
35-
}
37+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.rebble.cobble.shared.domain.store
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class JsFrame<T>(
8+
val methodName: String,
9+
val callbackId: Int,
10+
val data: T,
11+
)
12+
13+
@Serializable
14+
data class LoadAppToDeviceAndLocker(
15+
val id: String,
16+
val uuid: String,
17+
val title: String,
18+
@SerialName("list_image")
19+
val listImage: String,
20+
@SerialName("icon_image")
21+
val iconImage: String,
22+
@SerialName("screenshot_image")
23+
val screenshotImage: String,
24+
val type: String,
25+
@SerialName("pbw_file")
26+
val pbwFile: String,
27+
val links: AppLinks,
28+
)
29+
30+
@Serializable
31+
data class AppLinks(
32+
val add: String,
33+
val remove: String,
34+
val share: String,
35+
@SerialName("add_flag")
36+
val addFlag: String? = null,
37+
@SerialName("add_heart")
38+
val addHeart: String? = null,
39+
@SerialName("remove_flag")
40+
val removeFlag: String? = null,
41+
@SerialName("remove_heart")
42+
val removeHeart: String? = null,
43+
)
44+
45+
@Serializable
46+
data class SetNavBarTitle(
47+
val title: String,
48+
val browserTitle: String? = null,
49+
@SerialName("show_search")
50+
val showSearch: Boolean = true,
51+
@SerialName("show_share")
52+
val showShare: Boolean? = null,
53+
)
54+
55+
@Serializable
56+
data class OpenURL(
57+
val url: String,
58+
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.rebble.cobble.shared.domain.store
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.encodeToString
6+
import kotlinx.serialization.json.Json
7+
8+
class PebbleBridge {
9+
companion object {
10+
internal val json =
11+
Json {
12+
ignoreUnknownKeys = true
13+
encodeDefaults = true
14+
}
15+
16+
internal val className = "PebbleBridge"
17+
18+
fun searchRequest(
19+
query: String,
20+
section: String,
21+
): String {
22+
val request = PebbleBridgeSearchRequest(query = query, section = section)
23+
return createRequest(request)
24+
}
25+
26+
fun navigateRequest(url: String): String {
27+
val request = PebbleBridgeNavigateRequest(url = url)
28+
return createRequest(request)
29+
}
30+
31+
fun refreshRequest(): String {
32+
val request = PebbleBridgeRefreshRequest()
33+
return createRequest(request)
34+
}
35+
36+
fun lockerResponse(
37+
callbackId: Int,
38+
addedToLocker: Boolean,
39+
): String {
40+
val response = PebbleBridgeLockerResponse(addedToLocker)
41+
return createResponse(callbackId, response)
42+
}
43+
44+
private inline fun <reified T> createRequest(request: T): String {
45+
val requestJson = json.encodeToString(request)
46+
return "$className.handleRequest($requestJson)"
47+
}
48+
49+
private inline fun <reified T> createResponse(
50+
callbackId: Int,
51+
response: T,
52+
): String {
53+
val responseData = PebbleBridgeResponse(callbackId, response)
54+
val responseJson = json.encodeToString(responseData)
55+
return "$className.handleResponse($responseJson)"
56+
}
57+
}
58+
}
59+
60+
@Serializable
61+
data class PebbleBridgeResponse<T>(
62+
val callbackId: Int,
63+
val data: T,
64+
)
65+
66+
@Serializable
67+
data class PebbleBridgeLockerResponse(
68+
@SerialName("added_to_locker")
69+
val addedToLocker: Boolean,
70+
)
71+
72+
@Serializable
73+
data class PebbleBridgeSearchRequest(
74+
val methodName: String = "search",
75+
val query: String,
76+
val section: String,
77+
)
78+
79+
@Serializable
80+
data class PebbleBridgeNavigateRequest(
81+
val methodName: String = "navigate",
82+
val url: String,
83+
)
84+
85+
@Serializable
86+
data class PebbleBridgeRefreshRequest(
87+
val methodName: String = "refresh",
88+
)

android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/nav/Routes.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ object Routes {
55
object Home {
66
const val LOCKER_APPS = "locker_apps"
77
const val LOCKER_WATCHFACES = "locker_watchfaces"
8+
const val STORE_APPS = "store_apps"
9+
const val STORE_WATCHFACES = "store_watchfaces"
810
const val TEST_PAGE = "test_page"
911
}
10-
}
12+
}

android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/MainView.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.rebble.cobble.shared.ui.view.dialogs.AppInstallDialog
2020
import io.rebble.cobble.shared.ui.view.home.HomePage
2121
import io.rebble.cobble.shared.ui.view.home.HomeScaffold
2222
import io.rebble.cobble.shared.ui.view.home.locker.LockerTabs
23+
import io.rebble.cobble.shared.ui.view.home.store.StoreTabs
2324
import kotlinx.serialization.json.Json
2425
import kotlinx.serialization.json.Json.Default.decodeFromString
2526
import org.koin.compose.KoinContext
@@ -43,6 +44,12 @@ fun MainView(navController: NavHostController = rememberNavController()) {
4344
composable(Routes.Home.LOCKER_APPS) {
4445
HomeScaffold(HomePage.Locker(LockerTabs.Apps), onNavChange = navController::navigate)
4546
}
47+
composable(Routes.Home.STORE_WATCHFACES) {
48+
HomeScaffold(HomePage.Store(StoreTabs.Watchfaces), onNavChange = navController::navigate)
49+
}
50+
composable(Routes.Home.STORE_APPS) {
51+
HomeScaffold(HomePage.Store(StoreTabs.Apps), onNavChange = navController::navigate)
52+
}
4653
composable(Routes.Home.TEST_PAGE) {
4754
HomeScaffold(HomePage.TestPage, onNavChange = navController::navigate)
4855
}
@@ -60,4 +67,4 @@ fun MainView(navController: NavHostController = rememberNavController()) {
6067
}
6168
}
6269
}
63-
}
70+
}

android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ import io.rebble.cobble.shared.ui.common.RebbleIcons
1515
import io.rebble.cobble.shared.ui.nav.Routes
1616
import io.rebble.cobble.shared.ui.view.home.locker.Locker
1717
import io.rebble.cobble.shared.ui.view.home.locker.LockerTabs
18+
import io.rebble.cobble.shared.ui.view.home.store.Store
19+
import io.rebble.cobble.shared.ui.view.home.store.StoreTabs
1820
import kotlinx.coroutines.launch
1921

2022
open class HomePage {
2123
class Locker(val tab: LockerTabs) : HomePage()
24+
class Store(val tab: StoreTabs) : HomePage()
2225
object TestPage : HomePage()
2326
}
2427

2528
@Composable
2629
fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) {
2730
val snackbarHostState = remember { SnackbarHostState() }
2831
val scope = rememberCoroutineScope()
29-
val searchingState = remember { mutableStateOf(false) }
3032
Scaffold(
3133
snackbarHost = { SnackbarHost(snackbarHostState) },
3234
/*topBar = {
@@ -51,6 +53,12 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) {
5153
icon = { RebbleIcons.locker() },
5254
label = { Text("Locker") }
5355
)
56+
NavigationBarItem(
57+
selected = page is HomePage.Store,
58+
onClick = { onNavChange(Routes.Home.STORE_WATCHFACES) },
59+
icon = { RebbleIcons.rebbleStore() },
60+
label = { Text("Store") }
61+
)
5462
}
5563
},
5664
floatingActionButton = {
@@ -60,10 +68,14 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) {
6068
modifier = Modifier
6169
.padding(16.dp),
6270
onClick = {
63-
searchingState.value = true
71+
if (page.tab == LockerTabs.Watchfaces) {
72+
onNavChange(Routes.Home.STORE_WATCHFACES)
73+
} else {
74+
onNavChange(Routes.Home.STORE_APPS)
75+
}
6476
},
6577
content = {
66-
RebbleIcons.search()
78+
RebbleIcons.plusAdd()
6779
},
6880
)
6981
}
@@ -73,7 +85,12 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) {
7385
Box(modifier = Modifier.padding(innerPadding)) {
7486
when (page) {
7587
is HomePage.Locker -> {
76-
Locker(searchingState, page.tab, onTabChanged = {
88+
Locker(page.tab, onTabChanged = {
89+
onNavChange(it.navRoute)
90+
})
91+
}
92+
is HomePage.Store -> {
93+
Store(page.tab, onTabChanged = {
7794
onNavChange(it.navRoute)
7895
})
7996
}
@@ -87,4 +104,4 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) {
87104
}
88105
}
89106
}
90-
}
107+
}

0 commit comments

Comments
 (0)