@shaykec/app-agent 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engines/claude-engine.d.ts.map +1 -1
- package/dist/engines/claude-engine.js +1 -5
- package/dist/engines/claude-engine.js.map +1 -1
- package/dist/workspace.js +30 -4
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/AppConfig.kt +9 -0
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/data/DataSourceResolver.kt +23 -0
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/features/booking/BookingScreen.kt +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/features/discovery/ProviderDetailScreen.kt +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/features/discovery/SearchScreen.kt +1 -1
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/mock/MockDataProvider.kt +12 -11
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/AppConfig.kt +9 -0
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/data/DataRepository.kt +33 -0
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/data/DataSourceResolver.kt +23 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/AppConfig.kt +9 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/data/DataRepository.kt +62 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/data/DataSourceResolver.kt +23 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/mock/MockDataProvider.kt +28 -28
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/AppConfig.kt +9 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/data/DataRepository.kt +30 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/data/DataSourceResolver.kt +23 -0
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/create/CreateScreen.kt +3 -2
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/detail/DetailScreen.kt +4 -3
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/explore/ExploreScreen.kt +7 -6
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/favorites/FavoritesScreen.kt +4 -3
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/home/HomeScreen.kt +5 -4
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/mock/MockDataProvider.kt +12 -11
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/AppConfig.kt +9 -0
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/data/DataRepository.kt +38 -0
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/data/DataSourceResolver.kt +23 -0
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/features/feed/FeedScreen.kt +2 -2
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/features/profile/ProfileScreen.kt +1 -1
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/features/search/SearchScreen.kt +1 -1
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/mock/MockDataProvider.kt +7 -6
- package/templates/ios/BookTemplate/BookTemplate/App/AppConfig.swift +12 -2
- package/templates/ios/BookTemplate/BookTemplate/Data/DataRepository.swift +33 -0
- package/templates/ios/BookTemplate/BookTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/BookTemplate/BookTemplate/Features/Booking/BookingView.swift +1 -1
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/DiscoveryView.swift +3 -3
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/MapExploreView.swift +1 -1
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/ProviderDetailView.swift +1 -1
- package/templates/ios/BookTemplate/BookTemplate/Features/Discovery/SearchView.swift +2 -2
- package/templates/ios/BookTemplate/BookTemplate/MockData/MockDataProvider.swift +18 -1
- package/templates/ios/MapTemplate/MapTemplate/App/AppConfig.swift +11 -9
- package/templates/ios/MapTemplate/MapTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/MapTemplate/MapTemplate/Features/Favorites/FavoritesView.swift +1 -1
- package/templates/ios/MapTemplate/MapTemplate/Features/PlaceDetail/PlaceDetailView.swift +2 -2
- package/templates/ios/MapTemplate/MapTemplate/Features/Profile/ProfileView.swift +1 -1
- package/templates/ios/MapTemplate/MapTemplate/Features/Route/RouteView.swift +1 -1
- package/templates/ios/MapTemplate/MapTemplate/Features/Search/SearchView.swift +1 -1
- package/templates/ios/ShopTemplate/ShopTemplate/App/AppConfig.swift +9 -0
- package/templates/ios/ShopTemplate/ShopTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Home/HomeView.swift +9 -7
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Orders/OrdersView.swift +7 -3
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Products/ProductDetailView.swift +4 -2
- package/templates/ios/ShopTemplate/ShopTemplate/Features/Products/ProductListView.swift +6 -3
- package/templates/ios/ShopTemplate/ShopTemplate/MockData/MockDataProvider.swift +1 -1
- package/templates/ios/Skeleton/Skeleton/App/AppConfig.swift +9 -0
- package/templates/ios/Skeleton/Skeleton/Data/DataRepository.swift +30 -0
- package/templates/ios/Skeleton/Skeleton/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/Skeleton/Skeleton/Features/Explore/ExploreViewModel.swift +7 -6
- package/templates/ios/Skeleton/Skeleton/Features/Home/HomeViewModel.swift +8 -7
- package/templates/ios/Skeleton/Skeleton/MockData/MockDataProvider.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/App/AppConfig.swift +4 -3
- package/templates/ios/SocialTemplate/SocialTemplate/Data/DataRepository.swift +37 -0
- package/templates/ios/SocialTemplate/SocialTemplate/Data/DataSourceResolver.swift +20 -0
- package/templates/ios/SocialTemplate/SocialTemplate/Data/SyncManager.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/CreatePost/CreatePostView.swift +2 -2
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Feed/CommentsView.swift +2 -2
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Feed/FeedView.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Messages/MessagesView.swift +4 -4
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Notifications/NotificationsView.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Profile/ProfileView.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Features/Search/SearchView.swift +5 -4
- package/templates/ios/SocialTemplate/SocialTemplate/MockData/MockDataProvider.swift +1 -1
- package/templates/ios/SocialTemplate/SocialTemplate/Models/Models.swift +2 -2
package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/mock/MockDataProvider.kt
CHANGED
|
@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|
|
9
9
|
import kotlinx.coroutines.flow.asStateFlow
|
|
10
10
|
|
|
11
11
|
// CUSTOMIZE:API - Replace mock data with real API calls
|
|
12
|
-
object MockDataProvider {
|
|
12
|
+
object MockDataProvider : DataRepository {
|
|
13
13
|
|
|
14
14
|
// MARK: - Default Seed Data
|
|
15
15
|
private val defaultCategories = listOf(
|
|
@@ -258,28 +258,28 @@ object MockDataProvider {
|
|
|
258
258
|
|
|
259
259
|
// MARK: - StateFlow Properties
|
|
260
260
|
private val _categories = MutableStateFlow(defaultCategories)
|
|
261
|
-
val categories: StateFlow<List<Category>> = _categories.asStateFlow()
|
|
261
|
+
override val categories: StateFlow<List<Category>> = _categories.asStateFlow()
|
|
262
262
|
|
|
263
263
|
private val _products = MutableStateFlow(defaultProducts)
|
|
264
|
-
val products: StateFlow<List<Product>> = _products.asStateFlow()
|
|
264
|
+
override val products: StateFlow<List<Product>> = _products.asStateFlow()
|
|
265
265
|
|
|
266
266
|
private val _banners = MutableStateFlow(defaultBanners)
|
|
267
|
-
val banners: StateFlow<List<Banner>> = _banners.asStateFlow()
|
|
267
|
+
override val banners: StateFlow<List<Banner>> = _banners.asStateFlow()
|
|
268
268
|
|
|
269
269
|
private val _favorites = MutableStateFlow<Set<String>>(emptySet())
|
|
270
|
-
val favorites: StateFlow<Set<String>> = _favorites.asStateFlow()
|
|
270
|
+
override val favorites: StateFlow<Set<String>> = _favorites.asStateFlow()
|
|
271
271
|
|
|
272
272
|
private val _orders = MutableStateFlow<List<Order>>(emptyList())
|
|
273
|
-
val orders: StateFlow<List<Order>> = _orders.asStateFlow()
|
|
273
|
+
override val orders: StateFlow<List<Order>> = _orders.asStateFlow()
|
|
274
274
|
|
|
275
275
|
// MARK: - Static Data (unchanged)
|
|
276
|
-
val shippingOptions = listOf(
|
|
276
|
+
override val shippingOptions = listOf(
|
|
277
277
|
ShippingOption("s1", "Standard Shipping", "5-7 business days", AppConfig.Business.STANDARD_SHIPPING_COST, "5-7 business days", "local_shipping"),
|
|
278
278
|
ShippingOption("s2", "Express Shipping", "2-3 business days", AppConfig.Business.EXPRESS_SHIPPING_COST, "2-3 business days", "local_shipping"),
|
|
279
279
|
ShippingOption("s3", "Same Day Delivery", "Order before 2 PM", 19.99, "Today", "bolt")
|
|
280
280
|
)
|
|
281
281
|
|
|
282
|
-
val storeLocations = listOf(
|
|
282
|
+
override val storeLocations = listOf(
|
|
283
283
|
StoreLocation(
|
|
284
284
|
id = "store1",
|
|
285
285
|
name = "Downtown Store",
|
|
@@ -344,78 +344,78 @@ object MockDataProvider {
|
|
|
344
344
|
// MARK: - CRUD Functions
|
|
345
345
|
|
|
346
346
|
// Categories
|
|
347
|
-
fun addCategory(category: Category) {
|
|
347
|
+
override fun addCategory(category: Category) {
|
|
348
348
|
_categories.value = _categories.value + category
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
fun updateCategory(category: Category) {
|
|
351
|
+
override fun updateCategory(category: Category) {
|
|
352
352
|
_categories.value = _categories.value.map { if (it.id == category.id) category else it }
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
fun deleteCategory(categoryId: String) {
|
|
355
|
+
override fun deleteCategory(categoryId: String) {
|
|
356
356
|
_categories.value = _categories.value.filter { it.id != categoryId }
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
// Products
|
|
360
|
-
fun addProduct(product: Product) {
|
|
360
|
+
override fun addProduct(product: Product) {
|
|
361
361
|
_products.value = _products.value + product
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
fun updateProduct(product: Product) {
|
|
364
|
+
override fun updateProduct(product: Product) {
|
|
365
365
|
_products.value = _products.value.map { if (it.id == product.id) product else it }
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
fun deleteProduct(productId: String) {
|
|
368
|
+
override fun deleteProduct(productId: String) {
|
|
369
369
|
_products.value = _products.value.filter { it.id != productId }
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
// Banners
|
|
373
|
-
fun addBanner(banner: Banner) {
|
|
373
|
+
override fun addBanner(banner: Banner) {
|
|
374
374
|
_banners.value = _banners.value + banner
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
fun updateBanner(banner: Banner) {
|
|
377
|
+
override fun updateBanner(banner: Banner) {
|
|
378
378
|
_banners.value = _banners.value.map { if (it.id == banner.id) banner else it }
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
fun deleteBanner(bannerId: String) {
|
|
381
|
+
override fun deleteBanner(bannerId: String) {
|
|
382
382
|
_banners.value = _banners.value.filter { it.id != bannerId }
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
// Favorites
|
|
386
|
-
fun addToFavorites(productId: String) {
|
|
386
|
+
override fun addToFavorites(productId: String) {
|
|
387
387
|
val newFavorites = _favorites.value + productId
|
|
388
388
|
_favorites.value = newFavorites
|
|
389
389
|
SyncManager.addPendingFavorite(productId)
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
fun removeFromFavorites(productId: String) {
|
|
392
|
+
override fun removeFromFavorites(productId: String) {
|
|
393
393
|
val newFavorites = _favorites.value - productId
|
|
394
394
|
_favorites.value = newFavorites
|
|
395
395
|
SyncManager.removePendingFavorite(productId)
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
fun isFavorite(productId: String): Boolean = productId in _favorites.value
|
|
398
|
+
override fun isFavorite(productId: String): Boolean = productId in _favorites.value
|
|
399
399
|
|
|
400
400
|
// Orders
|
|
401
|
-
fun addOrder(order: Order) {
|
|
401
|
+
override fun addOrder(order: Order) {
|
|
402
402
|
_orders.value = _orders.value + order
|
|
403
403
|
SyncManager.addPendingOrder(order)
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
fun updateOrder(order: Order) {
|
|
406
|
+
override fun updateOrder(order: Order) {
|
|
407
407
|
_orders.value = _orders.value.map { if (it.id == order.id) order else it }
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
fun deleteOrder(orderId: String) {
|
|
410
|
+
override fun deleteOrder(orderId: String) {
|
|
411
411
|
_orders.value = _orders.value.filter { it.id != orderId }
|
|
412
412
|
SyncManager.removePendingOrder(orderId)
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
fun getOrder(orderId: String): Order? = _orders.value.find { it.id == orderId }
|
|
415
|
+
override fun getOrder(orderId: String): Order? = _orders.value.find { it.id == orderId }
|
|
416
416
|
|
|
417
417
|
// MARK: - Reset
|
|
418
|
-
fun reset() {
|
|
418
|
+
override fun reset() {
|
|
419
419
|
_categories.value = defaultCategories
|
|
420
420
|
_products.value = defaultProducts
|
|
421
421
|
_banners.value = defaultBanners
|
|
@@ -425,12 +425,12 @@ object MockDataProvider {
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
// MARK: - Helper Functions
|
|
428
|
-
fun getProduct(id: String): Product? = _products.value.find { it.id == id }
|
|
428
|
+
override fun getProduct(id: String): Product? = _products.value.find { it.id == id }
|
|
429
429
|
|
|
430
|
-
fun getProductsByCategory(categoryId: String): List<Product> =
|
|
430
|
+
override fun getProductsByCategory(categoryId: String): List<Product> =
|
|
431
431
|
_products.value.filter { it.category.id == categoryId }
|
|
432
432
|
|
|
433
|
-
fun searchProducts(query: String): List<Product> {
|
|
433
|
+
override fun searchProducts(query: String): List<Product> {
|
|
434
434
|
if (query.isBlank()) return _products.value
|
|
435
435
|
val q = query.lowercase()
|
|
436
436
|
return _products.value.filter {
|
|
@@ -49,6 +49,15 @@ object AppConfig {
|
|
|
49
49
|
val profile: ImageVector = Icons.Filled.Person
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// CUSTOMIZE:DATASOURCE - Data source configuration
|
|
53
|
+
object DataSource {
|
|
54
|
+
enum class SourceType {
|
|
55
|
+
LOCAL_STORAGE, // Room — real local database (DEFAULT)
|
|
56
|
+
MOCK // In-memory seed data — for development/testing only
|
|
57
|
+
}
|
|
58
|
+
val active: SourceType = SourceType.MOCK
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
// CUSTOMIZE:CATEGORIES - Domain categories
|
|
53
62
|
data class CategoryConfig(val name: String, val icon: ImageVector)
|
|
54
63
|
|
package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/data/DataRepository.kt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.appship.skeleton.data
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Abstracts the data layer. Both MockDataProvider and LocalStorageProvider
|
|
7
|
+
* implement this, allowing the app to swap data sources via AppConfig.DataSource.active.
|
|
8
|
+
*
|
|
9
|
+
* Screens and ViewModels depend on this interface rather than on a concrete implementation.
|
|
10
|
+
* Use [DataSourceResolver.repository] to get the active provider.
|
|
11
|
+
*/
|
|
12
|
+
interface DataRepository {
|
|
13
|
+
// Observable state
|
|
14
|
+
val items: StateFlow<List<Item>>
|
|
15
|
+
val categories: StateFlow<List<Category>>
|
|
16
|
+
val favoriteItems: StateFlow<List<Item>>
|
|
17
|
+
|
|
18
|
+
// Data loading
|
|
19
|
+
fun loadData()
|
|
20
|
+
|
|
21
|
+
// CRUD operations
|
|
22
|
+
fun addItem(item: Item)
|
|
23
|
+
fun updateItem(item: Item)
|
|
24
|
+
fun deleteItem(item: Item)
|
|
25
|
+
fun toggleFavorite(item: Item)
|
|
26
|
+
|
|
27
|
+
// Queries
|
|
28
|
+
fun searchItems(query: String): List<Item>
|
|
29
|
+
fun itemsByCategory(categoryName: String): List<Item>
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.appship.skeleton.data
|
|
2
|
+
|
|
3
|
+
import com.appship.skeleton.AppConfig
|
|
4
|
+
import com.appship.skeleton.mock.MockDataProvider
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the active [DataRepository] implementation based on [AppConfig.DataSource.active].
|
|
8
|
+
* Screens use this to obtain the correct data source without knowing the concrete type.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* val repository = DataSourceResolver.repository
|
|
12
|
+
* val items by repository.items.collectAsState()
|
|
13
|
+
*/
|
|
14
|
+
object DataSourceResolver {
|
|
15
|
+
val repository: DataRepository
|
|
16
|
+
get() = when (AppConfig.DataSource.active) {
|
|
17
|
+
AppConfig.DataSource.SourceType.LOCAL_STORAGE -> {
|
|
18
|
+
// TODO: Return LocalStorageProvider once implemented
|
|
19
|
+
MockDataProvider
|
|
20
|
+
}
|
|
21
|
+
AppConfig.DataSource.SourceType.MOCK -> MockDataProvider
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -9,8 +9,8 @@ import androidx.compose.ui.Modifier
|
|
|
9
9
|
import androidx.compose.ui.platform.testTag
|
|
10
10
|
import androidx.compose.ui.unit.dp
|
|
11
11
|
import com.appship.skeleton.AppConfig
|
|
12
|
+
import com.appship.skeleton.data.DataSourceResolver
|
|
12
13
|
import com.appship.skeleton.data.Item
|
|
13
|
-
import com.appship.skeleton.mock.MockDataProvider
|
|
14
14
|
|
|
15
15
|
// PLACEHOLDER: Customize this screen for creating new items.
|
|
16
16
|
|
|
@@ -20,6 +20,7 @@ fun CreateScreen(
|
|
|
20
20
|
onBack: () -> Unit,
|
|
21
21
|
modifier: Modifier = Modifier
|
|
22
22
|
) {
|
|
23
|
+
val repository = remember { DataSourceResolver.repository }
|
|
23
24
|
var title by remember { mutableStateOf("") }
|
|
24
25
|
var subtitle by remember { mutableStateOf("") }
|
|
25
26
|
var description by remember { mutableStateOf("") }
|
|
@@ -47,7 +48,7 @@ fun CreateScreen(
|
|
|
47
48
|
description = description,
|
|
48
49
|
category = selectedCategory
|
|
49
50
|
)
|
|
50
|
-
|
|
51
|
+
repository.addItem(newItem)
|
|
51
52
|
onBack()
|
|
52
53
|
},
|
|
53
54
|
enabled = title.isNotBlank(),
|
|
@@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
|
|
|
13
13
|
import androidx.compose.ui.draw.clip
|
|
14
14
|
import androidx.compose.ui.platform.testTag
|
|
15
15
|
import androidx.compose.ui.unit.dp
|
|
16
|
-
import com.appship.skeleton.
|
|
16
|
+
import com.appship.skeleton.data.DataSourceResolver
|
|
17
17
|
|
|
18
18
|
// PLACEHOLDER: Customize this screen to show full item details.
|
|
19
19
|
|
|
@@ -24,7 +24,8 @@ fun DetailScreen(
|
|
|
24
24
|
onBack: () -> Unit,
|
|
25
25
|
modifier: Modifier = Modifier
|
|
26
26
|
) {
|
|
27
|
-
val
|
|
27
|
+
val repository = remember { DataSourceResolver.repository }
|
|
28
|
+
val items by repository.items.collectAsState()
|
|
28
29
|
val item = items.firstOrNull { it.id == itemId }
|
|
29
30
|
|
|
30
31
|
Scaffold(
|
|
@@ -40,7 +41,7 @@ fun DetailScreen(
|
|
|
40
41
|
actions = {
|
|
41
42
|
if (item != null) {
|
|
42
43
|
IconButton(
|
|
43
|
-
onClick = {
|
|
44
|
+
onClick = { repository.toggleFavorite(item) },
|
|
44
45
|
modifier = Modifier.testTag("detail_favorite_button")
|
|
45
46
|
) {
|
|
46
47
|
Icon(
|
|
@@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
|
|
|
12
12
|
import androidx.compose.ui.platform.testTag
|
|
13
13
|
import androidx.compose.ui.unit.dp
|
|
14
14
|
import com.appship.skeleton.AppConfig
|
|
15
|
-
import com.appship.skeleton.
|
|
15
|
+
import com.appship.skeleton.data.DataSourceResolver
|
|
16
16
|
import com.appship.skeleton.features.home.ItemCard
|
|
17
17
|
|
|
18
18
|
// PLACEHOLDER: Customize this screen for search/browse functionality.
|
|
@@ -23,10 +23,11 @@ fun ExploreScreen(
|
|
|
23
23
|
onItemClick: (String) -> Unit,
|
|
24
24
|
modifier: Modifier = Modifier
|
|
25
25
|
) {
|
|
26
|
+
val repository = remember { DataSourceResolver.repository }
|
|
26
27
|
var searchQuery by remember { mutableStateOf("") }
|
|
27
28
|
var isActive by remember { mutableStateOf(false) }
|
|
28
|
-
val categories by
|
|
29
|
-
val searchResults = remember(searchQuery) {
|
|
29
|
+
val categories by repository.categories.collectAsState()
|
|
30
|
+
val searchResults = remember(searchQuery) { repository.searchItems(searchQuery) }
|
|
30
31
|
|
|
31
32
|
Scaffold(
|
|
32
33
|
topBar = {
|
|
@@ -79,7 +80,7 @@ fun ExploreScreen(
|
|
|
79
80
|
ItemCard(
|
|
80
81
|
item = item,
|
|
81
82
|
onClick = { onItemClick(item.id) },
|
|
82
|
-
onFavorite = {
|
|
83
|
+
onFavorite = { repository.toggleFavorite(item) },
|
|
83
84
|
modifier = Modifier.testTag("explore_result_${index}_card")
|
|
84
85
|
)
|
|
85
86
|
}
|
|
@@ -91,7 +92,7 @@ fun ExploreScreen(
|
|
|
91
92
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
92
93
|
) {
|
|
93
94
|
itemsIndexed(categories) { index, category ->
|
|
94
|
-
val categoryItems =
|
|
95
|
+
val categoryItems = repository.itemsByCategory(category.name)
|
|
95
96
|
Column(modifier = Modifier.testTag("explore_category_${index}")) {
|
|
96
97
|
Text(
|
|
97
98
|
text = "${category.name} (${categoryItems.size})",
|
|
@@ -102,7 +103,7 @@ fun ExploreScreen(
|
|
|
102
103
|
ItemCard(
|
|
103
104
|
item = item,
|
|
104
105
|
onClick = { onItemClick(item.id) },
|
|
105
|
-
onFavorite = {
|
|
106
|
+
onFavorite = { repository.toggleFavorite(item) },
|
|
106
107
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
107
108
|
)
|
|
108
109
|
}
|
|
@@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
|
|
|
12
12
|
import androidx.compose.ui.platform.testTag
|
|
13
13
|
import androidx.compose.ui.unit.dp
|
|
14
14
|
import com.appship.skeleton.AppConfig
|
|
15
|
-
import com.appship.skeleton.
|
|
15
|
+
import com.appship.skeleton.data.DataSourceResolver
|
|
16
16
|
import com.appship.skeleton.features.home.ItemCard
|
|
17
17
|
|
|
18
18
|
// PLACEHOLDER: Customize this screen to show saved/favorited items.
|
|
@@ -23,7 +23,8 @@ fun FavoritesScreen(
|
|
|
23
23
|
onItemClick: (String) -> Unit,
|
|
24
24
|
modifier: Modifier = Modifier
|
|
25
25
|
) {
|
|
26
|
-
val
|
|
26
|
+
val repository = remember { DataSourceResolver.repository }
|
|
27
|
+
val favoriteItems by repository.favoriteItems.collectAsState()
|
|
27
28
|
|
|
28
29
|
Scaffold(
|
|
29
30
|
topBar = {
|
|
@@ -64,7 +65,7 @@ fun FavoritesScreen(
|
|
|
64
65
|
ItemCard(
|
|
65
66
|
item = item,
|
|
66
67
|
onClick = { onItemClick(item.id) },
|
|
67
|
-
onFavorite = {
|
|
68
|
+
onFavorite = { repository.toggleFavorite(item) },
|
|
68
69
|
modifier = Modifier.testTag("favorites_item_${index}_card")
|
|
69
70
|
)
|
|
70
71
|
}
|
|
@@ -16,8 +16,8 @@ import androidx.compose.ui.draw.clip
|
|
|
16
16
|
import androidx.compose.ui.platform.testTag
|
|
17
17
|
import androidx.compose.ui.unit.dp
|
|
18
18
|
import com.appship.skeleton.AppConfig
|
|
19
|
+
import com.appship.skeleton.data.DataSourceResolver
|
|
19
20
|
import com.appship.skeleton.data.Item
|
|
20
|
-
import com.appship.skeleton.mock.MockDataProvider
|
|
21
21
|
|
|
22
22
|
// PLACEHOLDER: Customize this screen for your app's main content.
|
|
23
23
|
|
|
@@ -28,8 +28,9 @@ fun HomeScreen(
|
|
|
28
28
|
onCreateClick: () -> Unit,
|
|
29
29
|
modifier: Modifier = Modifier
|
|
30
30
|
) {
|
|
31
|
-
val
|
|
32
|
-
val
|
|
31
|
+
val repository = remember { DataSourceResolver.repository }
|
|
32
|
+
val items by repository.items.collectAsState()
|
|
33
|
+
val categories by repository.categories.collectAsState()
|
|
33
34
|
var selectedCategory by remember { mutableStateOf<String?>(null) }
|
|
34
35
|
|
|
35
36
|
val filteredItems = if (selectedCategory != null) {
|
|
@@ -82,7 +83,7 @@ fun HomeScreen(
|
|
|
82
83
|
ItemCard(
|
|
83
84
|
item = item,
|
|
84
85
|
onClick = { onItemClick(item.id) },
|
|
85
|
-
onFavorite = {
|
|
86
|
+
onFavorite = { repository.toggleFavorite(item) },
|
|
86
87
|
modifier = Modifier.testTag("home_item_${index}_card")
|
|
87
88
|
)
|
|
88
89
|
}
|
package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/mock/MockDataProvider.kt
CHANGED
|
@@ -2,6 +2,7 @@ package com.appship.skeleton.mock
|
|
|
2
2
|
|
|
3
3
|
import com.appship.skeleton.AppConfig
|
|
4
4
|
import com.appship.skeleton.data.Category
|
|
5
|
+
import com.appship.skeleton.data.DataRepository
|
|
5
6
|
import com.appship.skeleton.data.Item
|
|
6
7
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
7
8
|
import kotlinx.coroutines.flow.StateFlow
|
|
@@ -11,21 +12,21 @@ import java.util.Date
|
|
|
11
12
|
// PLACEHOLDER: Replace sample data with domain-specific content.
|
|
12
13
|
// Keep the singleton pattern and StateFlow interface.
|
|
13
14
|
|
|
14
|
-
object MockDataProvider {
|
|
15
|
+
object MockDataProvider : DataRepository {
|
|
15
16
|
private val _items = MutableStateFlow<List<Item>>(emptyList())
|
|
16
|
-
val items: StateFlow<List<Item>> = _items.asStateFlow()
|
|
17
|
+
override val items: StateFlow<List<Item>> = _items.asStateFlow()
|
|
17
18
|
|
|
18
19
|
private val _categories = MutableStateFlow<List<Category>>(emptyList())
|
|
19
|
-
val categories: StateFlow<List<Category>> = _categories.asStateFlow()
|
|
20
|
+
override val categories: StateFlow<List<Category>> = _categories.asStateFlow()
|
|
20
21
|
|
|
21
22
|
private val _favoriteItems = MutableStateFlow<List<Item>>(emptyList())
|
|
22
|
-
val favoriteItems: StateFlow<List<Item>> = _favoriteItems.asStateFlow()
|
|
23
|
+
override val favoriteItems: StateFlow<List<Item>> = _favoriteItems.asStateFlow()
|
|
23
24
|
|
|
24
25
|
init {
|
|
25
26
|
loadData()
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
fun loadData() {
|
|
29
|
+
override fun loadData() {
|
|
29
30
|
_categories.value = generateCategories()
|
|
30
31
|
_items.value = generateItems()
|
|
31
32
|
_favoriteItems.value = _items.value.filter { it.isFavorite }
|
|
@@ -33,30 +34,30 @@ object MockDataProvider {
|
|
|
33
34
|
|
|
34
35
|
// MARK: - CRUD Operations
|
|
35
36
|
|
|
36
|
-
fun addItem(item: Item) {
|
|
37
|
+
override fun addItem(item: Item) {
|
|
37
38
|
_items.value = listOf(item) + _items.value
|
|
38
39
|
if (item.isFavorite) {
|
|
39
40
|
_favoriteItems.value = listOf(item) + _favoriteItems.value
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
fun updateItem(item: Item) {
|
|
44
|
+
override fun updateItem(item: Item) {
|
|
44
45
|
_items.value = _items.value.map { if (it.id == item.id) item else it }
|
|
45
46
|
_favoriteItems.value = _items.value.filter { it.isFavorite }
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
fun deleteItem(item: Item) {
|
|
49
|
+
override fun deleteItem(item: Item) {
|
|
49
50
|
_items.value = _items.value.filter { it.id != item.id }
|
|
50
51
|
_favoriteItems.value = _favoriteItems.value.filter { it.id != item.id }
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
fun toggleFavorite(item: Item) {
|
|
54
|
+
override fun toggleFavorite(item: Item) {
|
|
54
55
|
val updated = item.copy(isFavorite = !item.isFavorite)
|
|
55
56
|
_items.value = _items.value.map { if (it.id == item.id) updated else it }
|
|
56
57
|
_favoriteItems.value = _items.value.filter { it.isFavorite }
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
fun searchItems(query: String): List<Item> {
|
|
60
|
+
override fun searchItems(query: String): List<Item> {
|
|
60
61
|
if (query.isBlank()) return _items.value
|
|
61
62
|
val lower = query.lowercase()
|
|
62
63
|
return _items.value.filter {
|
|
@@ -66,7 +67,7 @@ object MockDataProvider {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
fun itemsByCategory(categoryName: String): List<Item> {
|
|
70
|
+
override fun itemsByCategory(categoryName: String): List<Item> {
|
|
70
71
|
return _items.value.filter { it.category == categoryName }
|
|
71
72
|
}
|
|
72
73
|
|
package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/AppConfig.kt
CHANGED
|
@@ -61,4 +61,13 @@ object AppConfig {
|
|
|
61
61
|
OnboardingSlide("Discover", "Explore content from creators worldwide", "search"),
|
|
62
62
|
OnboardingSlide("Create", "Express yourself through posts and stories", "add_circle")
|
|
63
63
|
)
|
|
64
|
+
|
|
65
|
+
// CUSTOMIZE:DATASOURCE - Data source configuration
|
|
66
|
+
object DataSource {
|
|
67
|
+
enum class SourceType {
|
|
68
|
+
LOCAL_STORAGE, // Room — real local database (DEFAULT)
|
|
69
|
+
MOCK // In-memory seed data — for development/testing only
|
|
70
|
+
}
|
|
71
|
+
val active: SourceType = SourceType.MOCK
|
|
72
|
+
}
|
|
64
73
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.appship.social.data
|
|
2
|
+
|
|
3
|
+
import com.appship.social.data.*
|
|
4
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Abstracts the data layer. Both MockDataProvider and LocalStorageProvider
|
|
8
|
+
* implement this, allowing the app to swap data sources via AppConfig.DataSource.active.
|
|
9
|
+
*
|
|
10
|
+
* Screens and ViewModels depend on this interface rather than on a concrete implementation.
|
|
11
|
+
* Use [DataSourceResolver.repository] to get the active provider.
|
|
12
|
+
*/
|
|
13
|
+
interface DataRepository {
|
|
14
|
+
// Observable state
|
|
15
|
+
val currentUser: User
|
|
16
|
+
val users: List<User>
|
|
17
|
+
val posts: StateFlow<List<Post>>
|
|
18
|
+
val conversations: StateFlow<List<Conversation>>
|
|
19
|
+
val notifications: StateFlow<List<AppNotification>>
|
|
20
|
+
val stories: List<Story>
|
|
21
|
+
val trendingHashtags: List<Hashtag>
|
|
22
|
+
|
|
23
|
+
// Helper functions
|
|
24
|
+
fun getComments(postId: String): List<Comment>
|
|
25
|
+
|
|
26
|
+
// CRUD operations
|
|
27
|
+
fun createPost(content: String, mediaUrls: List<String> = emptyList(), mediaType: MediaType = MediaType.NONE, location: String? = null, tags: List<String>? = null): Post
|
|
28
|
+
fun updatePost(post: Post)
|
|
29
|
+
fun deletePost(postId: String)
|
|
30
|
+
fun likePost(postId: String)
|
|
31
|
+
fun unlikePost(postId: String)
|
|
32
|
+
fun followUser(userId: String)
|
|
33
|
+
fun unfollowUser(userId: String)
|
|
34
|
+
fun sendMessage(conversationId: String, content: String, mediaUrl: String? = null, mediaType: MediaType = MediaType.NONE): Message
|
|
35
|
+
fun markNotificationRead(notificationId: String)
|
|
36
|
+
fun markAllNotificationsRead()
|
|
37
|
+
fun reset()
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.appship.social.data
|
|
2
|
+
|
|
3
|
+
import com.appship.social.AppConfig
|
|
4
|
+
import com.appship.social.mock.MockDataProvider
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the active [DataRepository] implementation based on [AppConfig.DataSource.active].
|
|
8
|
+
* Screens use this to obtain the correct data source without knowing the concrete type.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* val repository = DataSourceResolver.repository
|
|
12
|
+
* val posts by repository.posts.collectAsState()
|
|
13
|
+
*/
|
|
14
|
+
object DataSourceResolver {
|
|
15
|
+
val repository: DataRepository
|
|
16
|
+
get() = when (AppConfig.DataSource.active) {
|
|
17
|
+
AppConfig.DataSource.SourceType.LOCAL_STORAGE -> {
|
|
18
|
+
// TODO: Return LocalStorageProvider once implemented
|
|
19
|
+
MockDataProvider
|
|
20
|
+
}
|
|
21
|
+
AppConfig.DataSource.SourceType.MOCK -> MockDataProvider
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -34,7 +34,7 @@ import com.appship.social.core.theme.StoryRingMiddle
|
|
|
34
34
|
import com.appship.social.core.theme.StoryRingStart
|
|
35
35
|
import com.appship.social.data.Post
|
|
36
36
|
import com.appship.social.data.Story
|
|
37
|
-
import com.appship.social.
|
|
37
|
+
import com.appship.social.data.DataSourceResolver
|
|
38
38
|
|
|
39
39
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
40
40
|
@Composable
|
|
@@ -129,7 +129,7 @@ fun StoriesRow(
|
|
|
129
129
|
) {
|
|
130
130
|
Box {
|
|
131
131
|
AsyncImage(
|
|
132
|
-
model =
|
|
132
|
+
model = repository.currentUser.avatarUrl,
|
|
133
133
|
contentDescription = null,
|
|
134
134
|
modifier = Modifier
|
|
135
135
|
.size(64.dp)
|
|
@@ -25,7 +25,7 @@ import coil.compose.AsyncImage
|
|
|
25
25
|
import com.appship.social.AppConfig
|
|
26
26
|
import com.appship.social.data.Post
|
|
27
27
|
import com.appship.social.data.User
|
|
28
|
-
import com.appship.social.
|
|
28
|
+
import com.appship.social.data.DataSourceResolver
|
|
29
29
|
|
|
30
30
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
31
31
|
@Composable
|
|
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
|
|
25
25
|
import coil.compose.AsyncImage
|
|
26
26
|
import com.appship.social.AppConfig
|
|
27
27
|
import com.appship.social.data.User
|
|
28
|
-
import com.appship.social.
|
|
28
|
+
import com.appship.social.data.DataSourceResolver
|
|
29
29
|
|
|
30
30
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
31
31
|
@Composable
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.appship.social.mock
|
|
2
2
|
|
|
3
3
|
import com.appship.social.data.*
|
|
4
|
+
import com.appship.social.data.DataRepository
|
|
4
5
|
import com.appship.social.data.SyncManager
|
|
5
6
|
import java.time.LocalDateTime
|
|
6
7
|
import java.util.UUID
|
|
@@ -8,7 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
8
9
|
import kotlinx.coroutines.flow.StateFlow
|
|
9
10
|
import kotlinx.coroutines.flow.asStateFlow
|
|
10
11
|
|
|
11
|
-
object MockDataProvider {
|
|
12
|
+
object MockDataProvider : DataRepository {
|
|
12
13
|
|
|
13
14
|
val currentUser = User(
|
|
14
15
|
id = "current",
|
|
@@ -255,7 +256,7 @@ object MockDataProvider {
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
// Messages
|
|
258
|
-
fun sendMessage(conversationId: String, content: String, mediaUrl: String? = null, mediaType: MediaType = MediaType.NONE): Message {
|
|
259
|
+
override fun sendMessage(conversationId: String, content: String, mediaUrl: String? = null, mediaType: MediaType = MediaType.NONE): Message {
|
|
259
260
|
val newMessage = Message(
|
|
260
261
|
id = UUID.randomUUID().toString(),
|
|
261
262
|
senderId = currentUser.id,
|
|
@@ -280,7 +281,7 @@ object MockDataProvider {
|
|
|
280
281
|
return newMessage
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
fun markNotificationRead(notificationId: String) {
|
|
284
|
+
override fun markNotificationRead(notificationId: String) {
|
|
284
285
|
_notifications.value = _notifications.value.map { notif ->
|
|
285
286
|
if (notif.id == notificationId && !notif.isRead) {
|
|
286
287
|
SyncManager.addPendingNotification(notificationId)
|
|
@@ -289,7 +290,7 @@ object MockDataProvider {
|
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
fun markAllNotificationsRead() {
|
|
293
|
+
override fun markAllNotificationsRead() {
|
|
293
294
|
_notifications.value = _notifications.value.map { notif ->
|
|
294
295
|
if (!notif.isRead) {
|
|
295
296
|
SyncManager.addPendingNotification(notif.id)
|
|
@@ -299,7 +300,7 @@ object MockDataProvider {
|
|
|
299
300
|
}
|
|
300
301
|
|
|
301
302
|
// MARK: - Reset
|
|
302
|
-
fun reset() {
|
|
303
|
+
override fun reset() {
|
|
303
304
|
_posts.value = defaultPosts
|
|
304
305
|
_conversations.value = defaultConversations
|
|
305
306
|
_notifications.value = defaultNotifications
|
|
@@ -307,7 +308,7 @@ object MockDataProvider {
|
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
// MARK: - Helper Functions
|
|
310
|
-
fun getComments(postId: String): List<Comment> {
|
|
311
|
+
override fun getComments(postId: String): List<Comment> {
|
|
311
312
|
return (1..5).map { i ->
|
|
312
313
|
Comment(
|
|
313
314
|
id = "c$postId-$i",
|