openuispec 0.1.44 → 0.1.46
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/README.md +79 -55
- package/cli/init.ts +14 -3
- package/examples/social-app/.mcp.json +10 -0
- package/examples/social-app/AGENTS.md +105 -0
- package/examples/social-app/CLAUDE.md +105 -0
- package/examples/social-app/README.md +19 -0
- package/examples/social-app/backend/.gitkeep +1 -0
- package/examples/social-app/generated/android/social-app/app/build.gradle.kts +92 -0
- package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +26 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +20 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +35 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +13 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +98 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +19 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +68 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +15 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +34 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +390 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +234 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +641 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +113 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +212 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +113 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +137 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +180 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +157 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +85 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +74 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +293 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +116 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +161 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +162 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +95 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +123 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +33 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +41 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +20 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +82 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +60 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +9 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +91 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +10 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +79 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +79 -0
- package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +23 -0
- package/examples/social-app/generated/android/social-app/build.gradle.kts +6 -0
- package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +48 -0
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/examples/social-app/generated/android/social-app/gradle.properties +11 -0
- package/examples/social-app/generated/android/social-app/gradlew +25 -0
- package/examples/social-app/generated/android/social-app/settings.gradle.kts +23 -0
- package/examples/social-app/generated/web/social-app/index.html +12 -0
- package/examples/social-app/generated/web/social-app/package-lock.json +2517 -0
- package/examples/social-app/generated/web/social-app/package.json +27 -0
- package/examples/social-app/generated/web/social-app/src/app/App.tsx +58 -0
- package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +247 -0
- package/examples/social-app/generated/web/social-app/src/components/cards.tsx +317 -0
- package/examples/social-app/generated/web/social-app/src/components/ui.tsx +328 -0
- package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +86 -0
- package/examples/social-app/generated/web/social-app/src/i18n.tsx +59 -0
- package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +85 -0
- package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +70 -0
- package/examples/social-app/generated/web/social-app/src/lib/utils.ts +97 -0
- package/examples/social-app/generated/web/social-app/src/locales/en.json +67 -0
- package/examples/social-app/generated/web/social-app/src/locales/ru.json +67 -0
- package/examples/social-app/generated/web/social-app/src/locales/uz.json +67 -0
- package/examples/social-app/generated/web/social-app/src/main.tsx +16 -0
- package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +90 -0
- package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +86 -0
- package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +57 -0
- package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +113 -0
- package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +52 -0
- package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +41 -0
- package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +115 -0
- package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +57 -0
- package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +76 -0
- package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +96 -0
- package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +77 -0
- package/examples/social-app/generated/web/social-app/src/state/store.ts +592 -0
- package/examples/social-app/generated/web/social-app/src/styles.css +124 -0
- package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +1 -0
- package/examples/social-app/generated/web/social-app/tsconfig.json +22 -0
- package/examples/social-app/generated/web/social-app/tsconfig.node.json +13 -0
- package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +1 -0
- package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +1 -0
- package/examples/social-app/generated/web/social-app/vite.config.d.ts +2 -0
- package/examples/social-app/generated/web/social-app/vite.config.js +6 -0
- package/examples/social-app/generated/web/social-app/vite.config.ts +7 -0
- package/examples/social-app/openuispec/README.md +56 -0
- package/examples/social-app/openuispec/contracts/.gitkeep +0 -0
- package/examples/social-app/openuispec/contracts/action_trigger.yaml +73 -0
- package/examples/social-app/openuispec/contracts/collection.yaml +43 -0
- package/examples/social-app/openuispec/contracts/data_display.yaml +47 -0
- package/examples/social-app/openuispec/contracts/feedback.yaml +49 -0
- package/examples/social-app/openuispec/contracts/input_field.yaml +41 -0
- package/examples/social-app/openuispec/contracts/nav_container.yaml +34 -0
- package/examples/social-app/openuispec/contracts/surface.yaml +41 -0
- package/examples/social-app/openuispec/flows/.gitkeep +0 -0
- package/examples/social-app/openuispec/flows/create_post.yaml +66 -0
- package/examples/social-app/openuispec/locales/.gitkeep +0 -0
- package/examples/social-app/openuispec/locales/en.json +67 -0
- package/examples/social-app/openuispec/locales/ru.json +67 -0
- package/examples/social-app/openuispec/locales/uz.json +67 -0
- package/examples/social-app/openuispec/openuispec.yaml +214 -0
- package/examples/social-app/openuispec/platform/.gitkeep +0 -0
- package/examples/social-app/openuispec/platform/android.yaml +30 -0
- package/examples/social-app/openuispec/platform/ios.yaml +19 -0
- package/examples/social-app/openuispec/platform/web.yaml +23 -0
- package/examples/social-app/openuispec/screens/.gitkeep +0 -0
- package/examples/social-app/openuispec/screens/chat_detail.yaml +53 -0
- package/examples/social-app/openuispec/screens/discover.yaml +78 -0
- package/examples/social-app/openuispec/screens/edit_profile.yaml +78 -0
- package/examples/social-app/openuispec/screens/home_feed.yaml +123 -0
- package/examples/social-app/openuispec/screens/messages_inbox.yaml +43 -0
- package/examples/social-app/openuispec/screens/notifications.yaml +29 -0
- package/examples/social-app/openuispec/screens/post_detail.yaml +86 -0
- package/examples/social-app/openuispec/screens/profile_self.yaml +53 -0
- package/examples/social-app/openuispec/screens/profile_user.yaml +60 -0
- package/examples/social-app/openuispec/screens/search_results.yaml +62 -0
- package/examples/social-app/openuispec/screens/settings.yaml +94 -0
- package/examples/social-app/openuispec/tokens/.gitkeep +0 -0
- package/examples/social-app/openuispec/tokens/color.yaml +76 -0
- package/examples/social-app/openuispec/tokens/elevation.yaml +31 -0
- package/examples/social-app/openuispec/tokens/icons.yaml +147 -0
- package/examples/social-app/openuispec/tokens/layout.yaml +37 -0
- package/examples/social-app/openuispec/tokens/motion.yaml +28 -0
- package/examples/social-app/openuispec/tokens/spacing.yaml +19 -0
- package/examples/social-app/openuispec/tokens/themes.yaml +31 -0
- package/examples/social-app/openuispec/tokens/typography.yaml +50 -0
- package/examples/social-app/package.json +12 -0
- package/mcp-server/index.ts +69 -0
- package/package.json +1 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package com.social.app.data
|
|
2
|
+
|
|
3
|
+
import com.social.app.model.*
|
|
4
|
+
|
|
5
|
+
object MockData {
|
|
6
|
+
val users = listOf(
|
|
7
|
+
User(
|
|
8
|
+
id = "user_me",
|
|
9
|
+
handle = "rustam",
|
|
10
|
+
displayName = "Rustam Abdurahmonov",
|
|
11
|
+
avatarUrl = "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=320&q=80",
|
|
12
|
+
bio = "Designing interface systems that feel editorial, human, and alive.",
|
|
13
|
+
followers = 1240,
|
|
14
|
+
following = 184
|
|
15
|
+
),
|
|
16
|
+
User(
|
|
17
|
+
id = "u_9921",
|
|
18
|
+
handle = "elara_vox",
|
|
19
|
+
displayName = "Elara Vance",
|
|
20
|
+
avatarUrl = "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=256&q=80",
|
|
21
|
+
bio = "Digital Architect | Exploring the intersection of AR and urban spaces 🏙️",
|
|
22
|
+
followers = 12400,
|
|
23
|
+
following = 842,
|
|
24
|
+
isFollowed = true
|
|
25
|
+
),
|
|
26
|
+
User(
|
|
27
|
+
id = "u_4432",
|
|
28
|
+
handle = "kai_zen",
|
|
29
|
+
displayName = "Kai Nakamura",
|
|
30
|
+
avatarUrl = "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=256&q=80",
|
|
31
|
+
bio = "Sustainable tech & minimalist living. 🌿 Tokyo -> Berlin",
|
|
32
|
+
followers = 3100,
|
|
33
|
+
following = 1100
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
val posts = listOf(
|
|
38
|
+
Post(
|
|
39
|
+
id = "p_77102",
|
|
40
|
+
authorId = "u_9921",
|
|
41
|
+
authorName = "Elara Vance",
|
|
42
|
+
authorHandle = "elara_vox",
|
|
43
|
+
authorAvatar = "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=256&q=80",
|
|
44
|
+
body = "The new Neo-Tokyo district looks incredible in the morning light. The AR overlays are finally syncing perfectly with the physical architecture. #FutureCity #DigitalTwin #2026",
|
|
45
|
+
mediaUrl = "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=1080&q=80",
|
|
46
|
+
likeCount = 1432,
|
|
47
|
+
commentCount = 42,
|
|
48
|
+
timestamp = "4 hours ago",
|
|
49
|
+
liked = true
|
|
50
|
+
),
|
|
51
|
+
Post(
|
|
52
|
+
id = "p_77103",
|
|
53
|
+
authorId = "u_4432",
|
|
54
|
+
authorName = "Kai Nakamura",
|
|
55
|
+
authorHandle = "kai_zen",
|
|
56
|
+
authorAvatar = "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=256&q=80",
|
|
57
|
+
body = "Workspace evolution. Keeping it clean and focused this year.",
|
|
58
|
+
mediaUrl = "https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1080&q=80",
|
|
59
|
+
likeCount = 890,
|
|
60
|
+
commentCount = 15,
|
|
61
|
+
timestamp = "9 hours ago"
|
|
62
|
+
),
|
|
63
|
+
Post(
|
|
64
|
+
id = "p_1",
|
|
65
|
+
authorId = "user_me",
|
|
66
|
+
authorName = "Rustam Abdurahmonov",
|
|
67
|
+
authorHandle = "rustam",
|
|
68
|
+
authorAvatar = "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=320&q=80",
|
|
69
|
+
body = "Spent the morning photographing a cafe before it opened. The cups were already warm, the chairs still slightly crooked. Those in-between moments always feel the most honest.",
|
|
70
|
+
mediaUrl = "https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=1200&q=80",
|
|
71
|
+
likeCount = 243,
|
|
72
|
+
commentCount = 12,
|
|
73
|
+
timestamp = "1 day ago"
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
val stories = listOf(
|
|
78
|
+
Story(id = "s_1", authorId = "u_9921", authorName = "Elara Vance", previewUrl = "https://images.unsplash.com/photo-1550745165-9bc0b252726f?auto=format&fit=crop&w=720&q=80"),
|
|
79
|
+
Story(id = "s_2", authorId = "u_4432", authorName = "Kai Nakamura", previewUrl = "https://images.unsplash.com/photo-1511467687858-23d96c32e4ae?auto=format&fit=crop&w=720&q=80"),
|
|
80
|
+
Story(id = "s_3", authorId = "user_me", authorName = "Rustam", previewUrl = "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=320&q=80")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
val trends = listOf(
|
|
84
|
+
Trend(id = "t_1", label = "Editorial UI", postCount = 8420),
|
|
85
|
+
Trend(id = "t_2", label = "Warm Brutalism", postCount = 4135),
|
|
86
|
+
Trend(id = "t_3", label = "Motion Details", postCount = 2940),
|
|
87
|
+
Trend(id = "t_4", label = "Design Systems", postCount = 8677)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
val notifications = listOf(
|
|
91
|
+
Notification(id = "n_1", type = "like", actorName = "Elara Vance", actorAvatar = "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=256&q=80", message = "liked your post", timestamp = "34m ago"),
|
|
92
|
+
Notification(id = "n_2", type = "comment", actorName = "Kai Nakamura", actorAvatar = "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=256&q=80", message = "commented on your post", timestamp = "7h ago"),
|
|
93
|
+
Notification(id = "n_3", type = "follow", actorName = "Elara Vance", actorAvatar = "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=256&q=80", message = "started following you", timestamp = "1d ago")
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
data class Trend(val id: String, val label: String, val postCount: Int)
|
|
98
|
+
data class Notification(val id: String, val type: String, val actorName: String, val actorAvatar: String?, val message: String, val timestamp: String)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package com.social.app.data.preferences
|
|
2
|
+
|
|
3
|
+
enum class ThemeMode(val storageValue: String) {
|
|
4
|
+
System("system"),
|
|
5
|
+
Light("light"),
|
|
6
|
+
Dark("dark");
|
|
7
|
+
|
|
8
|
+
companion object {
|
|
9
|
+
fun fromStorageValue(value: String?): ThemeMode =
|
|
10
|
+
entries.firstOrNull { it.storageValue == value } ?: System
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
data class AppPreferences(
|
|
15
|
+
val themeMode: ThemeMode = ThemeMode.System,
|
|
16
|
+
val pushNotifications: Boolean = true,
|
|
17
|
+
val messagePreviews: Boolean = true,
|
|
18
|
+
val autoTranslate: Boolean = false,
|
|
19
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package com.social.app.data.preferences
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.datastore.core.DataStore
|
|
5
|
+
import androidx.datastore.preferences.core.Preferences
|
|
6
|
+
import androidx.datastore.preferences.core.booleanPreferencesKey
|
|
7
|
+
import androidx.datastore.preferences.core.edit
|
|
8
|
+
import androidx.datastore.preferences.core.emptyPreferences
|
|
9
|
+
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
10
|
+
import androidx.datastore.preferences.preferencesDataStore
|
|
11
|
+
import java.io.IOException
|
|
12
|
+
import kotlinx.coroutines.flow.Flow
|
|
13
|
+
import kotlinx.coroutines.flow.catch
|
|
14
|
+
import kotlinx.coroutines.flow.map
|
|
15
|
+
|
|
16
|
+
private val Context.socialAppDataStore: DataStore<Preferences> by preferencesDataStore(name = "social_app_preferences")
|
|
17
|
+
|
|
18
|
+
class DataStorePreferencesRepository(
|
|
19
|
+
private val context: Context,
|
|
20
|
+
) : PreferencesRepository {
|
|
21
|
+
override val preferences: Flow<AppPreferences> =
|
|
22
|
+
context.socialAppDataStore.data
|
|
23
|
+
.catch { exception ->
|
|
24
|
+
if (exception is IOException) {
|
|
25
|
+
emit(emptyPreferences())
|
|
26
|
+
} else {
|
|
27
|
+
throw exception
|
|
28
|
+
}
|
|
29
|
+
}.map { storedPreferences ->
|
|
30
|
+
AppPreferences(
|
|
31
|
+
themeMode = ThemeMode.fromStorageValue(storedPreferences[Keys.ThemeMode]),
|
|
32
|
+
pushNotifications = storedPreferences[Keys.PushNotifications] ?: true,
|
|
33
|
+
messagePreviews = storedPreferences[Keys.MessagePreviews] ?: true,
|
|
34
|
+
autoTranslate = storedPreferences[Keys.AutoTranslate] ?: false,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override suspend fun updateThemeMode(themeMode: ThemeMode) {
|
|
39
|
+
context.socialAppDataStore.edit { storedPreferences ->
|
|
40
|
+
storedPreferences[Keys.ThemeMode] = themeMode.storageValue
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override suspend fun updatePushNotifications(enabled: Boolean) {
|
|
45
|
+
context.socialAppDataStore.edit { storedPreferences ->
|
|
46
|
+
storedPreferences[Keys.PushNotifications] = enabled
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override suspend fun updateMessagePreviews(enabled: Boolean) {
|
|
51
|
+
context.socialAppDataStore.edit { storedPreferences ->
|
|
52
|
+
storedPreferences[Keys.MessagePreviews] = enabled
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override suspend fun updateAutoTranslate(enabled: Boolean) {
|
|
57
|
+
context.socialAppDataStore.edit { storedPreferences ->
|
|
58
|
+
storedPreferences[Keys.AutoTranslate] = enabled
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private object Keys {
|
|
63
|
+
val ThemeMode = stringPreferencesKey("theme_mode")
|
|
64
|
+
val PushNotifications = booleanPreferencesKey("push_notifications")
|
|
65
|
+
val MessagePreviews = booleanPreferencesKey("message_previews")
|
|
66
|
+
val AutoTranslate = booleanPreferencesKey("auto_translate")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.social.app.data.preferences
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.flow.Flow
|
|
4
|
+
|
|
5
|
+
interface PreferencesRepository {
|
|
6
|
+
val preferences: Flow<AppPreferences>
|
|
7
|
+
|
|
8
|
+
suspend fun updateThemeMode(themeMode: ThemeMode)
|
|
9
|
+
|
|
10
|
+
suspend fun updatePushNotifications(enabled: Boolean)
|
|
11
|
+
|
|
12
|
+
suspend fun updateMessagePreviews(enabled: Boolean)
|
|
13
|
+
|
|
14
|
+
suspend fun updateAutoTranslate(enabled: Boolean)
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.social.app.model
|
|
2
|
+
|
|
3
|
+
data class User(
|
|
4
|
+
val id: String,
|
|
5
|
+
val handle: String,
|
|
6
|
+
val displayName: String,
|
|
7
|
+
val avatarUrl: String? = null,
|
|
8
|
+
val bio: String? = null,
|
|
9
|
+
val followers: Int = 0,
|
|
10
|
+
val following: Int = 0,
|
|
11
|
+
val isFollowed: Boolean = false
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
data class Post(
|
|
15
|
+
val id: String,
|
|
16
|
+
val authorId: String,
|
|
17
|
+
val authorName: String,
|
|
18
|
+
val authorHandle: String,
|
|
19
|
+
val authorAvatar: String? = null,
|
|
20
|
+
val body: String,
|
|
21
|
+
val mediaUrl: String? = null,
|
|
22
|
+
val likeCount: Int = 0,
|
|
23
|
+
val commentCount: Int = 0,
|
|
24
|
+
val timestamp: String,
|
|
25
|
+
val liked: Boolean = false
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
data class Story(
|
|
29
|
+
val id: String,
|
|
30
|
+
val authorId: String,
|
|
31
|
+
val authorName: String,
|
|
32
|
+
val authorAvatar: String? = null,
|
|
33
|
+
val previewUrl: String? = null
|
|
34
|
+
)
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
package com.social.app.ui
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.clickable
|
|
4
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
5
|
+
import androidx.compose.foundation.layout.Box
|
|
6
|
+
import androidx.compose.foundation.layout.Column
|
|
7
|
+
import androidx.compose.foundation.layout.Row
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxHeight
|
|
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.layout.width
|
|
13
|
+
import androidx.compose.material.icons.Icons
|
|
14
|
+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
15
|
+
import androidx.compose.material.icons.filled.AddCircle
|
|
16
|
+
import androidx.compose.material.icons.filled.Explore
|
|
17
|
+
import androidx.compose.material.icons.filled.Home
|
|
18
|
+
import androidx.compose.material.icons.filled.Notifications
|
|
19
|
+
import androidx.compose.material.icons.filled.Person
|
|
20
|
+
import androidx.compose.material3.Badge
|
|
21
|
+
import androidx.compose.material3.BadgedBox
|
|
22
|
+
import androidx.compose.material3.BottomSheetDefaults
|
|
23
|
+
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
24
|
+
import androidx.compose.material3.Icon
|
|
25
|
+
import androidx.compose.material3.MaterialTheme
|
|
26
|
+
import androidx.compose.material3.ModalBottomSheet
|
|
27
|
+
import androidx.compose.material3.NavigationBar
|
|
28
|
+
import androidx.compose.material3.NavigationBarItem
|
|
29
|
+
import androidx.compose.material3.NavigationBarItemDefaults
|
|
30
|
+
import androidx.compose.material3.NavigationRail
|
|
31
|
+
import androidx.compose.material3.Scaffold
|
|
32
|
+
import androidx.compose.material3.SnackbarHostState
|
|
33
|
+
import androidx.compose.material3.Surface
|
|
34
|
+
import androidx.compose.material3.Text
|
|
35
|
+
import androidx.compose.material3.TopAppBar
|
|
36
|
+
import androidx.compose.material3.IconButton
|
|
37
|
+
import androidx.compose.runtime.Composable
|
|
38
|
+
import androidx.compose.runtime.getValue
|
|
39
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
40
|
+
import androidx.compose.runtime.remember
|
|
41
|
+
import androidx.compose.runtime.rememberCoroutineScope
|
|
42
|
+
import androidx.compose.runtime.setValue
|
|
43
|
+
import androidx.compose.ui.Alignment
|
|
44
|
+
import androidx.compose.ui.Modifier
|
|
45
|
+
import androidx.compose.ui.platform.LocalConfiguration
|
|
46
|
+
import androidx.compose.ui.res.stringResource
|
|
47
|
+
import androidx.compose.ui.text.style.TextAlign
|
|
48
|
+
import androidx.compose.ui.unit.dp
|
|
49
|
+
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
|
50
|
+
import com.social.app.R
|
|
51
|
+
import com.social.app.data.preferences.PreferencesRepository
|
|
52
|
+
import com.arkivanov.mvikotlin.core.store.StoreFactory
|
|
53
|
+
import com.social.app.data.MockData
|
|
54
|
+
import com.social.app.ui.navigation.RootComponent
|
|
55
|
+
import com.social.app.ui.screens.ChatDetailScreen
|
|
56
|
+
import com.social.app.ui.screens.CreatePostScreen
|
|
57
|
+
import com.social.app.ui.screens.DiscoverScreen
|
|
58
|
+
import com.social.app.ui.screens.EditProfileScreen
|
|
59
|
+
import com.social.app.ui.screens.HomeFeedScreen
|
|
60
|
+
import com.social.app.ui.screens.MessagesInboxScreen
|
|
61
|
+
import com.social.app.ui.screens.NotificationsScreen
|
|
62
|
+
import com.social.app.ui.screens.PostDetailScreen
|
|
63
|
+
import com.social.app.ui.screens.ProfileSelfScreen
|
|
64
|
+
import com.social.app.ui.screens.SearchResultsScreen
|
|
65
|
+
import com.social.app.ui.screens.SettingsScreen
|
|
66
|
+
import com.social.app.ui.screens.UserProfileScreen
|
|
67
|
+
import com.social.app.ui.components.ContractSnackbarHost
|
|
68
|
+
import com.social.app.ui.theme.Shapes
|
|
69
|
+
import kotlinx.coroutines.delay
|
|
70
|
+
import kotlinx.coroutines.launch
|
|
71
|
+
|
|
72
|
+
private enum class MainDestination {
|
|
73
|
+
Home,
|
|
74
|
+
Discover,
|
|
75
|
+
Notifications,
|
|
76
|
+
Profile,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private data class NavItem(
|
|
80
|
+
val destination: MainDestination?,
|
|
81
|
+
val labelRes: Int,
|
|
82
|
+
val icon: @Composable () -> Unit,
|
|
83
|
+
val onClick: () -> Unit,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
87
|
+
@Composable
|
|
88
|
+
fun MainShell(
|
|
89
|
+
root: RootComponent,
|
|
90
|
+
preferencesRepository: PreferencesRepository,
|
|
91
|
+
storeFactory: StoreFactory,
|
|
92
|
+
) {
|
|
93
|
+
val stack by root.stack.subscribeAsState()
|
|
94
|
+
val activeChild = stack.active.instance
|
|
95
|
+
val previousChild = stack.backStack.lastOrNull()?.instance
|
|
96
|
+
val mainChild =
|
|
97
|
+
if (activeChild is RootComponent.Child.CreatePost && previousChild != null) previousChild else activeChild
|
|
98
|
+
|
|
99
|
+
val isExpanded = LocalConfiguration.current.screenWidthDp >= 1025
|
|
100
|
+
var unreadCount by remember { mutableIntStateOf(MockData.notifications.size) }
|
|
101
|
+
val snackbarHostState = remember { SnackbarHostState() }
|
|
102
|
+
val scope = rememberCoroutineScope()
|
|
103
|
+
val createPostSuccessMessage = stringResource(R.string.create_post_success)
|
|
104
|
+
val activeDestination = activeDestinationFor(mainChild)
|
|
105
|
+
val primaryScreen =
|
|
106
|
+
mainChild is RootComponent.Child.Home ||
|
|
107
|
+
mainChild is RootComponent.Child.Discover ||
|
|
108
|
+
mainChild is RootComponent.Child.Notifications ||
|
|
109
|
+
mainChild is RootComponent.Child.Profile
|
|
110
|
+
|
|
111
|
+
val navItems = listOf(
|
|
112
|
+
NavItem(
|
|
113
|
+
destination = MainDestination.Home,
|
|
114
|
+
labelRes = R.string.nav_home,
|
|
115
|
+
icon = { Icon(Icons.Default.Home, contentDescription = null) },
|
|
116
|
+
onClick = root::onHomeTabClicked,
|
|
117
|
+
),
|
|
118
|
+
NavItem(
|
|
119
|
+
destination = MainDestination.Discover,
|
|
120
|
+
labelRes = R.string.nav_discover,
|
|
121
|
+
icon = { Icon(Icons.Default.Explore, contentDescription = null) },
|
|
122
|
+
onClick = root::onDiscoverTabClicked,
|
|
123
|
+
),
|
|
124
|
+
NavItem(
|
|
125
|
+
destination = null,
|
|
126
|
+
labelRes = R.string.nav_create,
|
|
127
|
+
icon = { Icon(Icons.Default.AddCircle, contentDescription = null) },
|
|
128
|
+
onClick = root::onCreatePostClicked,
|
|
129
|
+
),
|
|
130
|
+
NavItem(
|
|
131
|
+
destination = MainDestination.Notifications,
|
|
132
|
+
labelRes = R.string.nav_notifications,
|
|
133
|
+
icon = {
|
|
134
|
+
BadgedBox(
|
|
135
|
+
badge = {
|
|
136
|
+
if (unreadCount > 0) {
|
|
137
|
+
Badge {
|
|
138
|
+
Text(unreadCount.toString())
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
) {
|
|
143
|
+
Icon(Icons.Default.Notifications, contentDescription = null)
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
onClick = root::onNotificationsTabClicked,
|
|
147
|
+
),
|
|
148
|
+
NavItem(
|
|
149
|
+
destination = MainDestination.Profile,
|
|
150
|
+
labelRes = R.string.nav_profile,
|
|
151
|
+
icon = { Icon(Icons.Default.Person, contentDescription = null) },
|
|
152
|
+
onClick = root::onProfileTabClicked,
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
Scaffold(
|
|
157
|
+
modifier = Modifier.fillMaxSize(),
|
|
158
|
+
snackbarHost = {
|
|
159
|
+
ContractSnackbarHost(hostState = snackbarHostState)
|
|
160
|
+
},
|
|
161
|
+
bottomBar = {
|
|
162
|
+
if (primaryScreen && !isExpanded) {
|
|
163
|
+
CompactTabBar(
|
|
164
|
+
items = navItems,
|
|
165
|
+
activeDestination = activeDestination,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
) { padding ->
|
|
170
|
+
Row(
|
|
171
|
+
modifier = Modifier
|
|
172
|
+
.fillMaxSize()
|
|
173
|
+
.padding(padding),
|
|
174
|
+
) {
|
|
175
|
+
if (primaryScreen && isExpanded) {
|
|
176
|
+
ExpandedSidebar(
|
|
177
|
+
items = navItems,
|
|
178
|
+
activeDestination = activeDestination,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Box(
|
|
183
|
+
modifier = Modifier
|
|
184
|
+
.fillMaxHeight()
|
|
185
|
+
.weight(1f),
|
|
186
|
+
) {
|
|
187
|
+
Surface(
|
|
188
|
+
modifier = Modifier.fillMaxSize(),
|
|
189
|
+
color = MaterialTheme.colorScheme.surface,
|
|
190
|
+
) {
|
|
191
|
+
RenderChild(
|
|
192
|
+
child = mainChild,
|
|
193
|
+
root = root,
|
|
194
|
+
preferencesRepository = preferencesRepository,
|
|
195
|
+
storeFactory = storeFactory,
|
|
196
|
+
onNotificationRead = {
|
|
197
|
+
unreadCount = (unreadCount - 1).coerceAtLeast(0)
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (activeChild is RootComponent.Child.CreatePost) {
|
|
205
|
+
ModalBottomSheet(
|
|
206
|
+
onDismissRequest = root::onBackClicked,
|
|
207
|
+
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
208
|
+
dragHandle = { BottomSheetDefaults.DragHandle() },
|
|
209
|
+
shape = Shapes.SheetShape,
|
|
210
|
+
) {
|
|
211
|
+
CreatePostScreen(
|
|
212
|
+
onDismiss = root::onBackClicked,
|
|
213
|
+
onPublish = { _, _ ->
|
|
214
|
+
scope.launch {
|
|
215
|
+
snackbarHostState.currentSnackbarData?.dismiss()
|
|
216
|
+
snackbarHostState.showSnackbar(message = createPostSuccessMessage)
|
|
217
|
+
}
|
|
218
|
+
scope.launch {
|
|
219
|
+
delay(3000)
|
|
220
|
+
snackbarHostState.currentSnackbarData?.dismiss()
|
|
221
|
+
}
|
|
222
|
+
root.onBackClicked()
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@Composable
|
|
231
|
+
private fun CompactTabBar(
|
|
232
|
+
items: List<NavItem>,
|
|
233
|
+
activeDestination: MainDestination?,
|
|
234
|
+
) {
|
|
235
|
+
NavigationBar(
|
|
236
|
+
containerColor = MaterialTheme.colorScheme.surface,
|
|
237
|
+
contentColor = MaterialTheme.colorScheme.onSurface,
|
|
238
|
+
) {
|
|
239
|
+
items.forEach { item ->
|
|
240
|
+
NavigationBarItem(
|
|
241
|
+
selected = item.destination != null && item.destination == activeDestination,
|
|
242
|
+
onClick = item.onClick,
|
|
243
|
+
icon = item.icon,
|
|
244
|
+
label = { Text(stringResource(item.labelRes)) },
|
|
245
|
+
colors = NavigationBarItemDefaults.colors(
|
|
246
|
+
selectedIconColor = MaterialTheme.colorScheme.primary,
|
|
247
|
+
selectedTextColor = MaterialTheme.colorScheme.primary,
|
|
248
|
+
indicatorColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@Composable
|
|
256
|
+
private fun ExpandedSidebar(
|
|
257
|
+
items: List<NavItem>,
|
|
258
|
+
activeDestination: MainDestination?,
|
|
259
|
+
) {
|
|
260
|
+
NavigationRail(
|
|
261
|
+
modifier = Modifier.fillMaxHeight(),
|
|
262
|
+
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
263
|
+
) {
|
|
264
|
+
Column(
|
|
265
|
+
modifier = Modifier
|
|
266
|
+
.fillMaxHeight()
|
|
267
|
+
.padding(vertical = 16.dp, horizontal = 10.dp),
|
|
268
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
269
|
+
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
270
|
+
) {
|
|
271
|
+
items.forEach { item ->
|
|
272
|
+
val isActive = item.destination != null && item.destination == activeDestination
|
|
273
|
+
Surface(
|
|
274
|
+
modifier = Modifier
|
|
275
|
+
.width(92.dp)
|
|
276
|
+
.clickable(onClick = item.onClick),
|
|
277
|
+
shape = Shapes.RoundedCapPrimary,
|
|
278
|
+
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
|
|
279
|
+
contentColor = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
280
|
+
) {
|
|
281
|
+
Column(
|
|
282
|
+
modifier = Modifier
|
|
283
|
+
.fillMaxWidth()
|
|
284
|
+
.padding(horizontal = 8.dp, vertical = 12.dp),
|
|
285
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
286
|
+
verticalArrangement = Arrangement.spacedBy(6.dp),
|
|
287
|
+
) {
|
|
288
|
+
item.icon()
|
|
289
|
+
Text(
|
|
290
|
+
text = stringResource(item.labelRes),
|
|
291
|
+
style = MaterialTheme.typography.labelSmall,
|
|
292
|
+
textAlign = TextAlign.Center,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@Composable
|
|
302
|
+
private fun RenderChild(
|
|
303
|
+
child: RootComponent.Child,
|
|
304
|
+
root: RootComponent,
|
|
305
|
+
preferencesRepository: PreferencesRepository,
|
|
306
|
+
storeFactory: StoreFactory,
|
|
307
|
+
onNotificationRead: (String) -> Unit,
|
|
308
|
+
) {
|
|
309
|
+
when (child) {
|
|
310
|
+
is RootComponent.Child.Home -> HomeFeedScreen(
|
|
311
|
+
onPostClick = { postId -> root.onPostClicked(postId) },
|
|
312
|
+
onUserClick = { userId -> root.onUserClicked(userId) },
|
|
313
|
+
)
|
|
314
|
+
is RootComponent.Child.Discover -> DiscoverScreen(
|
|
315
|
+
onUserClick = { userId -> root.onUserClicked(userId) },
|
|
316
|
+
onSearchClick = { query -> root.onSearchClicked(query) },
|
|
317
|
+
)
|
|
318
|
+
is RootComponent.Child.CreatePost -> Unit
|
|
319
|
+
is RootComponent.Child.Notifications -> NotificationsScreen(
|
|
320
|
+
onNotificationRead = onNotificationRead,
|
|
321
|
+
)
|
|
322
|
+
is RootComponent.Child.MessagesInbox -> MessagesInboxScreen(
|
|
323
|
+
onConversationClick = { conversationId -> root.onConversationClicked(conversationId) },
|
|
324
|
+
)
|
|
325
|
+
is RootComponent.Child.Profile -> ProfileSelfScreen(
|
|
326
|
+
onEditProfileClick = root::onEditProfileClicked,
|
|
327
|
+
onPostClick = { postId -> root.onPostClicked(postId) },
|
|
328
|
+
onSettingsClick = root::onSettingsClicked,
|
|
329
|
+
)
|
|
330
|
+
is RootComponent.Child.PostDetail -> PostDetailScreen(
|
|
331
|
+
postId = child.postId,
|
|
332
|
+
onBackClick = root::onBackClicked,
|
|
333
|
+
onUserClick = { userId -> root.onUserClicked(userId) },
|
|
334
|
+
)
|
|
335
|
+
is RootComponent.Child.UserProfile -> UserProfileScreen(
|
|
336
|
+
userId = child.userId,
|
|
337
|
+
onBackClick = root::onBackClicked,
|
|
338
|
+
onPostClick = { postId -> root.onPostClicked(postId) },
|
|
339
|
+
)
|
|
340
|
+
is RootComponent.Child.SearchResults -> SearchResultsScreen(
|
|
341
|
+
query = child.query,
|
|
342
|
+
onBackClick = root::onBackClicked,
|
|
343
|
+
onPostClick = { postId -> root.onPostClicked(postId) },
|
|
344
|
+
onUserClick = { userId -> root.onUserClicked(userId) },
|
|
345
|
+
)
|
|
346
|
+
is RootComponent.Child.EditProfile -> EditProfileScreen(
|
|
347
|
+
onBackClick = root::onBackClicked,
|
|
348
|
+
)
|
|
349
|
+
is RootComponent.Child.ChatDetail -> ChatDetailScreen(
|
|
350
|
+
conversationId = child.conversationId,
|
|
351
|
+
onBackClick = root::onBackClicked,
|
|
352
|
+
)
|
|
353
|
+
is RootComponent.Child.Settings -> SettingsScreen(
|
|
354
|
+
onBackClick = root::onBackClicked,
|
|
355
|
+
onEditProfileClick = root::onEditProfileClicked,
|
|
356
|
+
preferencesRepository = preferencesRepository,
|
|
357
|
+
storeFactory = storeFactory,
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private fun activeDestinationFor(child: RootComponent.Child): MainDestination? =
|
|
363
|
+
when (child) {
|
|
364
|
+
is RootComponent.Child.Home -> MainDestination.Home
|
|
365
|
+
is RootComponent.Child.Discover -> MainDestination.Discover
|
|
366
|
+
is RootComponent.Child.Notifications -> MainDestination.Notifications
|
|
367
|
+
is RootComponent.Child.Profile -> MainDestination.Profile
|
|
368
|
+
else -> null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
372
|
+
@Composable
|
|
373
|
+
fun PlaceholderScreen(title: String, onBack: () -> Unit) {
|
|
374
|
+
Scaffold(
|
|
375
|
+
topBar = {
|
|
376
|
+
TopAppBar(
|
|
377
|
+
title = { Text(title) },
|
|
378
|
+
navigationIcon = {
|
|
379
|
+
IconButton(onClick = onBack) {
|
|
380
|
+
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
) { padding ->
|
|
386
|
+
Box(modifier = Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
|
|
387
|
+
Text("This screen is coming soon!", style = MaterialTheme.typography.bodyLarge)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|