├── .gitignore ├── Dockerfile ├── README.md ├── androidapp ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── stevdza │ │ └── san │ │ └── androidapp │ │ ├── MainActivity.kt │ │ ├── components │ │ ├── EmptyUI.kt │ │ ├── NavigationDrawer.kt │ │ └── PostCard.kt │ │ ├── data │ │ ├── MongoSync.kt │ │ └── MongoSyncRepository.kt │ │ ├── models │ │ └── Post.kt │ │ ├── navigation │ │ ├── NavGraph.kt │ │ ├── Screen.kt │ │ └── destinations │ │ │ ├── Category.kt │ │ │ ├── Details.kt │ │ │ └── Home.kt │ │ ├── screens │ │ ├── category │ │ │ ├── CategoryScreen.kt │ │ │ └── CategoryViewModel.kt │ │ ├── details │ │ │ └── DetailsScreen.kt │ │ └── home │ │ │ ├── HomeScreen.kt │ │ │ └── HomeViewModel.kt │ │ ├── ui │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── util │ │ ├── Constants.kt │ │ ├── Functions.kt │ │ └── RequestState.kt │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ └── logo.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── shared ├── .gitignore ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── example │ │ └── shared │ │ └── Category.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── example │ │ └── shared │ │ ├── Category.kt │ │ └── Constants.kt │ ├── jsMain │ └── kotlin │ │ └── com │ │ └── example │ │ └── shared │ │ └── Category.kt │ └── jvmMain │ └── kotlin │ └── com │ └── example │ └── shared │ └── Category.kt └── site ├── .gitignore ├── .kobweb └── conf.yaml ├── build.gradle.kts └── src ├── commonMain └── kotlin │ └── com │ └── example │ └── blogmultiplatform │ └── models │ ├── ApiListResponse.kt │ ├── Constants.kt │ ├── Newsletter.kt │ ├── Post.kt │ └── User.kt ├── jsMain ├── kotlin │ └── com │ │ └── example │ │ └── blogmultiplatform │ │ ├── MyApp.kt │ │ ├── components │ │ ├── AdminPageLayout.kt │ │ ├── CategoryChip.kt │ │ ├── CategoryNavigationItems.kt │ │ ├── ErrorView.kt │ │ ├── LoadingIndicator.kt │ │ ├── Popup.kt │ │ ├── PostPreview.kt │ │ ├── PostsView.kt │ │ ├── SearchBar.kt │ │ └── SidePanel.kt │ │ ├── models │ │ ├── ControlStyle.kt │ │ ├── EditorControl.kt │ │ ├── RandomJoke.kt │ │ └── User.kt │ │ ├── navigation │ │ └── Screen.kt │ │ ├── pages │ │ ├── Index.kt │ │ ├── admin │ │ │ ├── Create.kt │ │ │ ├── Index.kt │ │ │ ├── Login.kt │ │ │ ├── MyPosts.kt │ │ │ └── Success.kt │ │ ├── posts │ │ │ └── Index.kt │ │ └── search │ │ │ └── Index.kt │ │ ├── sections │ │ ├── FooterSection.kt │ │ ├── HeaderSection.kt │ │ ├── MainSection.kt │ │ ├── NewsletterSection.kt │ │ ├── PostsSection.kt │ │ └── SponsoredPostsSection.kt │ │ ├── styles │ │ ├── CreateStyle.kt │ │ ├── HeaderStyle.kt │ │ ├── LoginStyle.kt │ │ ├── NewsletterStyle.kt │ │ ├── PostPreviewStyle.kt │ │ └── SidePanelStyle.kt │ │ └── util │ │ ├── ApiFunctions.kt │ │ ├── Constants.kt │ │ └── Functions.kt └── resources │ └── public │ ├── bold.svg │ ├── checkmark.svg │ ├── code.svg │ ├── favicon.ico │ ├── github-dark.css │ ├── highlight.min.js │ ├── image.svg │ ├── italic.svg │ ├── laugh.png │ ├── link.svg │ ├── logo.svg │ ├── quote.svg │ ├── subtitle.svg │ └── title.svg └── jvmMain └── kotlin └── com └── example └── blogmultiplatform ├── api ├── Newsletter.kt ├── Posts.kt └── UserCheck.kt ├── data ├── MongoDB.kt └── MongoRepository.kt ├── models └── User.kt └── util └── Constants.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # General ignores 2 | .DS_Store 3 | build 4 | out 5 | kotlin-js-store 6 | 7 | # IntelliJ ignores 8 | *.iml 9 | /*.ipr 10 | 11 | /.idea/caches 12 | /.idea/libraries 13 | /.idea/modules.xml 14 | /.idea/workspace.xml 15 | /.idea/gradle.xml 16 | /.idea/navEditor.xml 17 | /.idea/assetWizardSettings.xml 18 | /.idea/artifacts 19 | /.idea/compiler.xml 20 | /.idea/jarRepositories.xml 21 | /.idea/*.iml 22 | /.idea/modules 23 | /.idea/libraries-with-intellij-classes.xml 24 | 25 | # Gradle ignores 26 | .gradle 27 | 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Variables shared across multiple stages (they need to be explicitly opted 3 | # into each stage by being declaring there too, but their values need only be 4 | # specified once). 5 | ARG KOBWEB_APP_ROOT="site" 6 | 7 | FROM eclipse-temurin:17 as java 8 | 9 | FROM java as export 10 | 11 | #----------------------------------------------------------------------------- 12 | # Create an intermediate stage which builds and exports our site. In the 13 | # final stage, we'll only extract what we need from this stage, saving a lot 14 | # of space. 15 | 16 | ENV KOBWEB_CLI_VERSION=0.9.13 17 | ARG KOBWEB_APP_ROOT 18 | 19 | ENV NODE_MAJOR=20 20 | 21 | # Copy the project code to an arbitrary subdir so we can install stuff in the 22 | # Docker container root without worrying about clobbering project files. 23 | COPY . /project 24 | 25 | # Update and install required OS packages to continue 26 | # Note: Node install instructions from: https://github.com/nodesource/distributions#installation-instructions 27 | # Note: Playwright is a system for running browsers, and here we use it to 28 | # install Chromium. 29 | RUN apt-get update \ 30 | && apt-get install -y ca-certificates curl gnupg unzip wget \ 31 | && mkdir -p /etc/apt/keyrings \ 32 | && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ 33 | && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ 34 | && apt-get update \ 35 | && apt-get install -y nodejs \ 36 | && npm init -y \ 37 | && npx playwright install --with-deps chromium 38 | 39 | # Fetch the latest version of the Kobweb CLI 40 | RUN wget https://github.com/varabyte/kobweb-cli/releases/download/v${KOBWEB_CLI_VERSION}/kobweb-${KOBWEB_CLI_VERSION}.zip \ 41 | && unzip kobweb-${KOBWEB_CLI_VERSION}.zip \ 42 | && rm kobweb-${KOBWEB_CLI_VERSION}.zip 43 | 44 | ENV PATH="/kobweb-${KOBWEB_CLI_VERSION}/bin:${PATH}" 45 | 46 | WORKDIR /project/${KOBWEB_APP_ROOT} 47 | 48 | # Decrease Gradle memory usage to avoid OOM situations in tight environments 49 | # (many free Cloud tiers only give you 512M of RAM). The following amount 50 | # should be more than enough to build and export our site. 51 | RUN mkdir ~/.gradle && \ 52 | echo "org.gradle.jvmargs=-Xmx256m" >> ~/.gradle/gradle.properties 53 | 54 | RUN kobweb export --notty 55 | 56 | #----------------------------------------------------------------------------- 57 | # Create the final stage, which contains just enough bits to run the Kobweb 58 | # server. 59 | FROM java as run 60 | 61 | ARG KOBWEB_APP_ROOT 62 | 63 | COPY --from=export /project/${KOBWEB_APP_ROOT}/.kobweb .kobweb 64 | 65 | ENTRYPOINT .kobweb/server/start.sh -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack Kotlin Multiplatform KMP Development | Web Mobile 2 |

3 | Online Course 4 |

5 |

6 | 7 |

8 | -------------------------------------------------------------------------------- /androidapp/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /androidapp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.com.android.application) 4 | alias(libs.plugins.org.jetbrains.kotlin.android) 5 | alias(libs.plugins.mongodb.realm) 6 | alias(libs.plugins.serialization.plugin) 7 | } 8 | 9 | android { 10 | namespace = "com.stevdza.san.androidapp" 11 | compileSdk = 34 12 | 13 | defaultConfig { 14 | applicationId = "com.stevdza.san.androidapp" 15 | minSdk = 24 16 | targetSdk = 34 17 | versionCode = 1 18 | versionName = "1.0" 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | vectorDrawables { 22 | useSupportLibrary = true 23 | } 24 | } 25 | 26 | buildTypes { 27 | release { 28 | isMinifyEnabled = false 29 | proguardFiles( 30 | getDefaultProguardFile("proguard-android-optimize.txt"), 31 | "proguard-rules.pro" 32 | ) 33 | } 34 | } 35 | compileOptions { 36 | sourceCompatibility = JavaVersion.VERSION_17 37 | targetCompatibility = JavaVersion.VERSION_17 38 | } 39 | tasks.withType().configureEach { 40 | kotlinOptions { 41 | jvmTarget = "17" 42 | } 43 | } 44 | kotlinOptions { 45 | jvmTarget = "17" 46 | } 47 | buildFeatures { 48 | compose = true 49 | } 50 | composeOptions { 51 | kotlinCompilerExtensionVersion = "1.5.7" 52 | } 53 | packaging { 54 | resources { 55 | excludes += "/META-INF/**" 56 | } 57 | } 58 | } 59 | 60 | dependencies { 61 | implementation(libs.core.ktx) 62 | implementation(libs.lifecycle.runtime.ktx) 63 | implementation(libs.activity.compose) 64 | implementation(platform(libs.compose.bom)) 65 | implementation(libs.ui) 66 | implementation(libs.ui.graphics) 67 | implementation(libs.ui.tooling.preview) 68 | implementation(libs.material3) 69 | 70 | implementation(libs.navigation.compose) 71 | implementation(libs.kotlinx.coroutines) 72 | implementation(libs.mongodb.sync) 73 | implementation(libs.coil.compose) 74 | implementation(libs.kotlinx.serialization) 75 | implementation(project(":shared")) 76 | } -------------------------------------------------------------------------------- /androidapp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /androidapp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.navigation.compose.rememberNavController 7 | import com.stevdza.san.androidapp.navigation.SetupNavGraph 8 | import com.stevdza.san.androidapp.ui.theme.BlogMultiplatformTheme 9 | 10 | class MainActivity : ComponentActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContent { 14 | BlogMultiplatformTheme { 15 | val navController = rememberNavController() 16 | SetupNavGraph(navController = navController) 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/components/EmptyUI.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | 11 | @Composable 12 | fun EmptyUI( 13 | loading: Boolean = false, 14 | hideMessage: Boolean = false, 15 | message: String = "No posts to show." 16 | ) { 17 | Box( 18 | modifier = Modifier.fillMaxSize(), 19 | contentAlignment = Alignment.Center 20 | ) { 21 | if (loading) { 22 | CircularProgressIndicator() 23 | } else { 24 | if (!hideMessage) { 25 | Text(text = message) 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/components/NavigationDrawer.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.DrawerState 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.ModalDrawerSheet 10 | import androidx.compose.material3.ModalNavigationDrawer 11 | import androidx.compose.material3.NavigationDrawerItem 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.alpha 17 | import com.stevdza.san.androidapp.R 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.unit.dp 20 | import com.example.shared.Category 21 | 22 | @Composable 23 | fun NavigationDrawer( 24 | drawerState: DrawerState, 25 | onCategorySelect: (Category) -> Unit, 26 | content: @Composable () -> Unit 27 | ) { 28 | ModalNavigationDrawer( 29 | drawerState = drawerState, 30 | drawerContent = { 31 | ModalDrawerSheet( 32 | content = { 33 | Box( 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .padding(vertical = 100.dp), 37 | contentAlignment = Alignment.Center 38 | ) { 39 | Image( 40 | painter = painterResource(id = R.drawable.logo), 41 | contentDescription = "Logo Image" 42 | ) 43 | } 44 | Text( 45 | modifier = Modifier 46 | .fillMaxWidth() 47 | .alpha(0.5f) 48 | .padding(start = 20.dp, bottom = 12.dp), 49 | text = "Categories" 50 | ) 51 | Category.entries.forEach { category -> 52 | NavigationDrawerItem( 53 | label = { 54 | Text( 55 | modifier = Modifier.padding(horizontal = 12.dp), 56 | text = category.name, 57 | color = MaterialTheme.colorScheme.onSurface 58 | ) 59 | }, 60 | selected = false, 61 | onClick = { onCategorySelect(category) } 62 | ) 63 | } 64 | } 65 | ) 66 | }, 67 | content = { content() } 68 | ) 69 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/components/PostCard.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.lazy.items 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.SuggestionChip 15 | import androidx.compose.material3.Surface 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.alpha 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.ui.layout.ContentScale 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.text.style.TextOverflow 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.dp 27 | import coil.compose.AsyncImage 28 | import coil.request.ImageRequest 29 | import com.example.shared.Category 30 | import com.stevdza.san.androidapp.models.Post 31 | import com.stevdza.san.androidapp.util.RequestState 32 | import com.stevdza.san.androidapp.util.convertLongToDate 33 | import com.stevdza.san.androidapp.util.decodeThumbnailImage 34 | 35 | @Composable 36 | fun PostCard( 37 | post: Post, 38 | onPostClick: (String) -> Unit 39 | ) { 40 | val context = LocalContext.current 41 | Surface( 42 | modifier = Modifier 43 | .fillMaxWidth() 44 | .clip(RoundedCornerShape(size = 12.dp)) 45 | .clickable { onPostClick(post._id) }, 46 | tonalElevation = 1.dp 47 | ) { 48 | Column(modifier = Modifier.fillMaxWidth()) { 49 | AsyncImage( 50 | modifier = Modifier.height(260.dp), 51 | model = ImageRequest 52 | .Builder(context) 53 | .data( 54 | if (post.thumbnail.contains("http")) post.thumbnail 55 | else post.thumbnail.decodeThumbnailImage() 56 | ) 57 | .build(), 58 | contentDescription = "Post Thumbnail", 59 | contentScale = ContentScale.Crop 60 | ) 61 | Column( 62 | modifier = Modifier 63 | .fillMaxWidth() 64 | .padding(all = 16.dp) 65 | ) { 66 | Text( 67 | modifier = Modifier 68 | .padding(bottom = 6.dp) 69 | .alpha(0.5f), 70 | text = post.date.convertLongToDate(), 71 | fontSize = MaterialTheme.typography.bodySmall.fontSize, 72 | fontWeight = FontWeight.Normal, 73 | overflow = TextOverflow.Ellipsis, 74 | maxLines = 1 75 | ) 76 | Text( 77 | modifier = Modifier.padding(bottom = 6.dp), 78 | text = post.title, 79 | fontSize = MaterialTheme.typography.titleLarge.fontSize, 80 | fontWeight = FontWeight.Bold, 81 | overflow = TextOverflow.Ellipsis, 82 | maxLines = 2 83 | ) 84 | Text( 85 | modifier = Modifier.padding(bottom = 6.dp), 86 | text = post.subtitle, 87 | fontSize = MaterialTheme.typography.bodyMedium.fontSize, 88 | fontWeight = FontWeight.Normal, 89 | overflow = TextOverflow.Ellipsis, 90 | maxLines = 3 91 | ) 92 | SuggestionChip( 93 | onClick = { }, 94 | label = { Text(text = Category.valueOf(post.category).name) } 95 | ) 96 | } 97 | } 98 | } 99 | } 100 | 101 | @Composable 102 | fun PostCardsView( 103 | posts: RequestState>, 104 | topMargin: Dp, 105 | hideMessage: Boolean = false, 106 | onPostClick: (String) -> Unit 107 | ) { 108 | when (posts) { 109 | is RequestState.Success -> { 110 | if(posts.data.isNotEmpty()) { 111 | LazyColumn( 112 | modifier = Modifier 113 | .fillMaxSize() 114 | .padding(top = topMargin) 115 | .padding(horizontal = 24.dp), 116 | verticalArrangement = Arrangement.spacedBy(12.dp) 117 | ) { 118 | items( 119 | items = posts.data, 120 | key = { post -> post._id } 121 | ) { post -> 122 | PostCard(post = post, onPostClick = onPostClick) 123 | } 124 | } 125 | } else { 126 | EmptyUI() 127 | } 128 | } 129 | 130 | is RequestState.Error -> { 131 | EmptyUI(message = posts.error.message.toString()) 132 | } 133 | 134 | is RequestState.Idle -> { 135 | EmptyUI(hideMessage = hideMessage) 136 | } 137 | 138 | is RequestState.Loading -> { 139 | EmptyUI(loading = true) 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/data/MongoSync.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.data 2 | 3 | import com.example.shared.Category 4 | import com.stevdza.san.androidapp.models.Post 5 | import com.stevdza.san.androidapp.util.Constants.APP_ID 6 | import com.stevdza.san.androidapp.util.RequestState 7 | import io.realm.kotlin.Realm 8 | import io.realm.kotlin.ext.query 9 | import io.realm.kotlin.log.LogLevel 10 | import io.realm.kotlin.mongodb.App 11 | import io.realm.kotlin.mongodb.sync.SyncConfiguration 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.flow.map 15 | 16 | object MongoSync : MongoSyncRepository { 17 | private val app = App.create(APP_ID) 18 | private val user = app.currentUser 19 | private lateinit var realm: Realm 20 | 21 | init { 22 | configureTheRealm() 23 | } 24 | 25 | override fun configureTheRealm() { 26 | if (user != null) { 27 | val config = SyncConfiguration.Builder(user, setOf(Post::class)) 28 | .initialSubscriptions { 29 | add( 30 | query = it.query(Post::class), 31 | name = "Blog Posts" 32 | ) 33 | } 34 | .log(LogLevel.ALL) 35 | .build() 36 | realm = Realm.open(config) 37 | } 38 | } 39 | 40 | override fun readAllPosts(): Flow>> { 41 | return if (user != null) { 42 | try { 43 | realm.query(Post::class) 44 | .asFlow() 45 | .map { result -> 46 | RequestState.Success(data = result.list) 47 | } 48 | } catch (e: Exception) { 49 | flow { emit(RequestState.Error(Exception(e.message))) } 50 | } 51 | } else { 52 | flow { emit(RequestState.Error(Exception("User not authenticated."))) } 53 | } 54 | } 55 | 56 | override fun searchPostsByTitle(query: String): Flow>> { 57 | return if (user != null) { 58 | try { 59 | realm.query(query = "title CONTAINS[c] $0", query) 60 | .asFlow() 61 | .map { result -> 62 | RequestState.Success(data = result.list) 63 | } 64 | } catch (e: Exception) { 65 | flow { emit(RequestState.Error(Exception(e.message))) } 66 | } 67 | } else { 68 | flow { emit(RequestState.Error(Exception("User not authenticated."))) } 69 | } 70 | } 71 | 72 | override fun searchPostsByCategory(category: Category): Flow>> { 73 | return if (user != null) { 74 | try { 75 | realm.query(query = "category == $0", category.name) 76 | .asFlow() 77 | .map { result -> 78 | RequestState.Success(data = result.list) 79 | } 80 | } catch (e: Exception) { 81 | flow { emit(RequestState.Error(Exception(e.message))) } 82 | } 83 | } else { 84 | flow { emit(RequestState.Error(Exception("User not authenticated."))) } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/data/MongoSyncRepository.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.data 2 | 3 | import com.example.shared.Category 4 | import com.stevdza.san.androidapp.models.Post 5 | import com.stevdza.san.androidapp.util.RequestState 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface MongoSyncRepository { 9 | fun configureTheRealm() 10 | fun readAllPosts(): Flow>> 11 | fun searchPostsByTitle(query: String): Flow>> 12 | fun searchPostsByCategory(category: Category): Flow>> 13 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/models/Post.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.models 2 | 3 | import com.example.shared.Category 4 | import io.realm.kotlin.types.RealmObject 5 | import io.realm.kotlin.types.annotations.PrimaryKey 6 | 7 | open class Post: RealmObject { 8 | @PrimaryKey 9 | var _id: String = "" 10 | var author: String = "" 11 | var date: Long = 0L 12 | var title: String = "" 13 | var subtitle: String = "" 14 | var thumbnail: String = "" 15 | var category: String = Category.Programming.name 16 | } 17 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/navigation/NavGraph.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.compose.NavHost 6 | import com.stevdza.san.androidapp.navigation.destinations.categoryRoute 7 | import com.stevdza.san.androidapp.navigation.destinations.detailsRoute 8 | import com.stevdza.san.androidapp.navigation.destinations.homeRoute 9 | 10 | @Composable 11 | fun SetupNavGraph(navController: NavHostController) { 12 | NavHost( 13 | navController = navController, 14 | startDestination = Screen.Home.route 15 | ) { 16 | homeRoute( 17 | onCategorySelect = { category -> 18 | navController.navigate(Screen.Category.passCategory(category)) 19 | }, 20 | onPostClick = { postId -> 21 | navController.navigate(Screen.Details.passPostId(postId)) 22 | } 23 | ) 24 | categoryRoute( 25 | onBackPress = { navController.popBackStack() }, 26 | onPostClick = { postId -> 27 | navController.navigate(Screen.Details.passPostId(postId)) 28 | } 29 | ) 30 | detailsRoute(onBackPress = { navController.popBackStack() }) 31 | } 32 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.navigation 2 | 3 | import com.stevdza.san.androidapp.util.Constants.CATEGORY_ARGUMENT 4 | import com.stevdza.san.androidapp.util.Constants.POST_ID_ARGUMENT 5 | import com.example.shared.Category as PostCategory 6 | 7 | sealed class Screen(val route: String) { 8 | data object Home : Screen(route = "home_screen") 9 | data object Category : Screen(route = "category_screen/{${CATEGORY_ARGUMENT}}") { 10 | fun passCategory(category: PostCategory) = "category_screen/${category.name}" 11 | } 12 | 13 | data object Details : Screen(route = "details_screen/{${POST_ID_ARGUMENT}}") { 14 | fun passPostId(id: String) = "details_screen/${id}" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/navigation/destinations/Category.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.navigation.destinations 2 | 3 | import androidx.lifecycle.viewmodel.compose.viewModel 4 | import androidx.navigation.NavGraphBuilder 5 | import androidx.navigation.NavType 6 | import androidx.navigation.compose.composable 7 | import androidx.navigation.navArgument 8 | import com.example.shared.Category 9 | import com.stevdza.san.androidapp.navigation.Screen 10 | import com.stevdza.san.androidapp.screens.category.CategoryScreen 11 | import com.stevdza.san.androidapp.screens.category.CategoryViewModel 12 | import com.stevdza.san.androidapp.util.Constants.CATEGORY_ARGUMENT 13 | 14 | fun NavGraphBuilder.categoryRoute( 15 | onBackPress: () -> Unit, 16 | onPostClick: (String) -> Unit 17 | ) { 18 | composable( 19 | route = Screen.Category.route, 20 | arguments = listOf(navArgument(name = CATEGORY_ARGUMENT) { 21 | type = NavType.StringType 22 | }) 23 | ) { 24 | val viewModel: CategoryViewModel = viewModel() 25 | val selectedCategory = it.arguments?.getString(CATEGORY_ARGUMENT) ?: Category.Programming.name 26 | CategoryScreen( 27 | posts = viewModel.categoryPosts.value, 28 | category = Category.valueOf(selectedCategory), 29 | onBackPress = onBackPress, 30 | onPostClick = onPostClick 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/navigation/destinations/Details.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.navigation.destinations 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.NavType 5 | import androidx.navigation.compose.composable 6 | import androidx.navigation.navArgument 7 | import com.example.shared.Constants 8 | import com.stevdza.san.androidapp.navigation.Screen 9 | import com.stevdza.san.androidapp.screens.details.DetailsScreen 10 | import com.stevdza.san.androidapp.util.Constants.POST_ID_ARGUMENT 11 | 12 | fun NavGraphBuilder.detailsRoute( 13 | onBackPress: () -> Unit 14 | ) { 15 | composable( 16 | route = Screen.Details.route, 17 | arguments = listOf(navArgument(name = POST_ID_ARGUMENT) { 18 | type = NavType.StringType 19 | }) 20 | ) { 21 | val postId = it.arguments?.getString(POST_ID_ARGUMENT) 22 | DetailsScreen( 23 | url = "http://10.0.2.2:8080/posts/post?${POST_ID_ARGUMENT}=$postId&${Constants.SHOW_SECTIONS_PARAM}=false", 24 | onBackPress = onBackPress 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/navigation/destinations/Home.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.navigation.destinations 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.viewmodel.compose.viewModel 8 | import androidx.navigation.NavGraphBuilder 9 | import androidx.navigation.compose.composable 10 | import com.example.shared.Category 11 | import com.stevdza.san.androidapp.navigation.Screen 12 | import com.stevdza.san.androidapp.screens.home.HomeScreen 13 | import com.stevdza.san.androidapp.screens.home.HomeViewModel 14 | 15 | fun NavGraphBuilder.homeRoute( 16 | onCategorySelect: (Category) -> Unit, 17 | onPostClick: (String) -> Unit 18 | ) { 19 | composable(route = Screen.Home.route) { 20 | val viewModel: HomeViewModel = viewModel() 21 | var query by remember { mutableStateOf("") } 22 | var searchBarOpened by remember { mutableStateOf(false) } 23 | var active by remember { mutableStateOf(false) } 24 | 25 | HomeScreen( 26 | posts = viewModel.allPosts.value, 27 | searchedPosts = viewModel.searchedPosts.value, 28 | query = query, 29 | searchBarOpened = searchBarOpened, 30 | active = active, 31 | onActiveChange = { active = it }, 32 | onQueryChange = { query = it }, 33 | onCategorySelect = onCategorySelect, 34 | onSearchBarChange = { opened -> 35 | searchBarOpened = opened 36 | if (!opened) { 37 | query = "" 38 | active = false 39 | viewModel.resetSearchedPosts() 40 | } 41 | }, 42 | onSearch = viewModel::searchPostsByTitle, 43 | onPostClick = onPostClick 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/screens/category/CategoryScreen.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.screens.category 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.ArrowBack 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.material3.Text 11 | import androidx.compose.material3.TopAppBar 12 | import androidx.compose.runtime.Composable 13 | import com.stevdza.san.androidapp.components.PostCardsView 14 | import com.example.shared.Category 15 | import com.stevdza.san.androidapp.models.Post 16 | import com.stevdza.san.androidapp.util.RequestState 17 | 18 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") 19 | @OptIn(ExperimentalMaterial3Api::class) 20 | @Composable 21 | fun CategoryScreen( 22 | posts: RequestState>, 23 | category: Category, 24 | onBackPress: () -> Unit, 25 | onPostClick: (String) -> Unit 26 | ) { 27 | Scaffold( 28 | topBar = { 29 | TopAppBar( 30 | title = { Text(text = category.name) }, 31 | navigationIcon = { 32 | IconButton(onClick = { onBackPress() }) { 33 | Icon( 34 | imageVector = Icons.Default.ArrowBack, 35 | contentDescription = "Back Arrow Icon" 36 | ) 37 | } 38 | } 39 | ) 40 | } 41 | ) { 42 | PostCardsView( 43 | posts = posts, 44 | topMargin = it.calculateTopPadding(), 45 | onPostClick = onPostClick 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/screens/category/CategoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.screens.category 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.SavedStateHandle 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.stevdza.san.androidapp.data.MongoSync 10 | import com.example.shared.Category 11 | import com.stevdza.san.androidapp.models.Post 12 | import com.stevdza.san.androidapp.util.RequestState 13 | import kotlinx.coroutines.flow.collectLatest 14 | import kotlinx.coroutines.launch 15 | 16 | class CategoryViewModel( 17 | savedStateHandle: SavedStateHandle 18 | ) : ViewModel() { 19 | private val _categoryPosts: MutableState>> = 20 | mutableStateOf(RequestState.Idle) 21 | val categoryPosts: State>> = _categoryPosts 22 | 23 | init { 24 | _categoryPosts.value = RequestState.Loading 25 | val selectedCategory = savedStateHandle.get("category") 26 | if (selectedCategory != null) { 27 | viewModelScope.launch { 28 | MongoSync.searchPostsByCategory( 29 | category = Category.valueOf(selectedCategory) 30 | ).collectLatest { _categoryPosts.value = it } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/screens/details/DetailsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.screens.details 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.ViewGroup 5 | import android.webkit.WebView 6 | import android.webkit.WebViewClient 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TopAppBar 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.viewinterop.AndroidView 19 | 20 | @SuppressLint("SetJavaScriptEnabled") 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | fun DetailsScreen( 24 | url: String, 25 | onBackPress: () -> Unit 26 | ) { 27 | Scaffold( 28 | topBar = { 29 | TopAppBar( 30 | title = { Text(text = "Details") }, 31 | navigationIcon = { 32 | IconButton(onClick = { onBackPress() }) { 33 | Icon( 34 | imageVector = Icons.AutoMirrored.Default.ArrowBack, 35 | contentDescription = "Back Arrow Icon" 36 | ) 37 | } 38 | } 39 | ) 40 | } 41 | ) { 42 | AndroidView( 43 | modifier = Modifier.padding(top = it.calculateTopPadding()), 44 | factory = { context -> 45 | WebView(context).apply { 46 | layoutParams = ViewGroup.LayoutParams( 47 | ViewGroup.LayoutParams.MATCH_PARENT, 48 | ViewGroup.LayoutParams.MATCH_PARENT 49 | ) 50 | settings.javaScriptEnabled = true 51 | webViewClient = WebViewClient() 52 | loadUrl(url) 53 | } 54 | } 55 | ) 56 | } 57 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/screens/home/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.screens.home 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.ArrowBack 6 | import androidx.compose.material.icons.filled.Close 7 | import androidx.compose.material.icons.filled.Menu 8 | import androidx.compose.material.icons.filled.Search 9 | import androidx.compose.material3.CenterAlignedTopAppBar 10 | import androidx.compose.material3.DrawerValue 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.IconButton 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Scaffold 16 | import androidx.compose.material3.SearchBar 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.rememberDrawerState 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.rememberCoroutineScope 21 | import androidx.compose.ui.unit.dp 22 | import com.stevdza.san.androidapp.components.NavigationDrawer 23 | import com.stevdza.san.androidapp.components.PostCardsView 24 | import com.example.shared.Category 25 | import com.stevdza.san.androidapp.models.Post 26 | import com.stevdza.san.androidapp.util.RequestState 27 | import kotlinx.coroutines.launch 28 | 29 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") 30 | @OptIn(ExperimentalMaterial3Api::class) 31 | @Composable 32 | fun HomeScreen( 33 | posts: RequestState>, 34 | searchedPosts: RequestState>, 35 | query: String, 36 | searchBarOpened: Boolean, 37 | active: Boolean, 38 | onActiveChange: (Boolean) -> Unit, 39 | onQueryChange: (String) -> Unit, 40 | onCategorySelect: (Category) -> Unit, 41 | onSearchBarChange: (Boolean) -> Unit, 42 | onSearch: (String) -> Unit, 43 | onPostClick: (String) -> Unit 44 | ) { 45 | val scope = rememberCoroutineScope() 46 | val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) 47 | NavigationDrawer( 48 | drawerState = drawerState, 49 | onCategorySelect = { 50 | scope.launch { 51 | drawerState.close() 52 | } 53 | onCategorySelect(it) 54 | } 55 | ) { 56 | Scaffold( 57 | topBar = { 58 | CenterAlignedTopAppBar( 59 | title = { 60 | Text(text = "Blog") 61 | }, 62 | navigationIcon = { 63 | IconButton( 64 | onClick = { 65 | scope.launch { 66 | drawerState.open() 67 | } 68 | } 69 | ) { 70 | Icon( 71 | imageVector = Icons.Default.Menu, 72 | contentDescription = "Drawer Icon" 73 | ) 74 | } 75 | }, 76 | actions = { 77 | IconButton( 78 | onClick = { 79 | onSearchBarChange(true) 80 | onActiveChange(true) 81 | } 82 | ) { 83 | Icon( 84 | imageVector = Icons.Default.Search, 85 | contentDescription = "Search Icon", 86 | tint = MaterialTheme.colorScheme.onSurface 87 | ) 88 | } 89 | } 90 | ) 91 | if (searchBarOpened) { 92 | SearchBar( 93 | query = query, 94 | onQueryChange = onQueryChange, 95 | onSearch = onSearch, 96 | active = active, 97 | onActiveChange = onActiveChange, 98 | placeholder = { Text(text = "Search here...") }, 99 | leadingIcon = { 100 | IconButton(onClick = { onSearchBarChange(false) }) { 101 | Icon( 102 | imageVector = Icons.Default.ArrowBack, 103 | contentDescription = "Back Arrow Icon", 104 | tint = MaterialTheme.colorScheme.onSurface 105 | ) 106 | } 107 | }, 108 | trailingIcon = { 109 | IconButton(onClick = { onQueryChange("") }) { 110 | Icon( 111 | imageVector = Icons.Default.Close, 112 | contentDescription = "Close Icon", 113 | tint = MaterialTheme.colorScheme.onSurface 114 | ) 115 | } 116 | } 117 | ) { 118 | PostCardsView( 119 | posts = searchedPosts, 120 | topMargin = 12.dp, 121 | onPostClick = onPostClick 122 | ) 123 | } 124 | } 125 | } 126 | ) { 127 | PostCardsView( 128 | posts = posts, 129 | topMargin = it.calculateTopPadding(), 130 | hideMessage = true, 131 | onPostClick = onPostClick 132 | ) 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/screens/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.screens.home 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.stevdza.san.androidapp.data.MongoSync 9 | import com.stevdza.san.androidapp.models.Post 10 | import com.stevdza.san.androidapp.util.Constants.APP_ID 11 | import com.stevdza.san.androidapp.util.RequestState 12 | import io.realm.kotlin.mongodb.App 13 | import io.realm.kotlin.mongodb.Credentials 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.flow.collectLatest 16 | import kotlinx.coroutines.launch 17 | import kotlinx.coroutines.withContext 18 | 19 | class HomeViewModel : ViewModel() { 20 | private val _allPosts: MutableState>> = 21 | mutableStateOf(RequestState.Idle) 22 | val allPosts: State>> = _allPosts 23 | private val _searchedPosts: MutableState>> = 24 | mutableStateOf(RequestState.Idle) 25 | val searchedPosts: State>> = _searchedPosts 26 | 27 | init { 28 | viewModelScope.launch(Dispatchers.IO) { 29 | App.create(APP_ID).login(Credentials.anonymous()) 30 | fetchAllPosts() 31 | } 32 | } 33 | 34 | private suspend fun fetchAllPosts() { 35 | withContext(Dispatchers.Main) { 36 | _allPosts.value = RequestState.Loading 37 | } 38 | MongoSync.readAllPosts().collectLatest { 39 | _allPosts.value = it 40 | } 41 | } 42 | 43 | fun searchPostsByTitle(query: String) { 44 | viewModelScope.launch { 45 | withContext(Dispatchers.Main) { 46 | _searchedPosts.value = RequestState.Loading 47 | } 48 | MongoSync.searchPostsByTitle(query = query).collectLatest { 49 | _searchedPosts.value = it 50 | } 51 | } 52 | } 53 | 54 | fun resetSearchedPosts() { 55 | _searchedPosts.value = RequestState.Idle 56 | } 57 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val md_theme_light_primary = Color(0xFF00629D) 6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 7 | val md_theme_light_primaryContainer = Color(0xFFCFE5FF) 8 | val md_theme_light_onPrimaryContainer = Color(0xFF001D34) 9 | val md_theme_light_secondary = Color(0xFF006689) 10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 11 | val md_theme_light_secondaryContainer = Color(0xFFC2E8FF) 12 | val md_theme_light_onSecondaryContainer = Color(0xFF001E2C) 13 | val md_theme_light_tertiary = Color(0xFF00658E) 14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 15 | val md_theme_light_tertiaryContainer = Color(0xFFC7E7FF) 16 | val md_theme_light_onTertiaryContainer = Color(0xFF001E2E) 17 | val md_theme_light_error = Color(0xFFBA1A1A) 18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 19 | val md_theme_light_onError = Color(0xFFFFFFFF) 20 | val md_theme_light_onErrorContainer = Color(0xFF410002) 21 | val md_theme_light_background = Color(0xFFF8FDFF) 22 | val md_theme_light_onBackground = Color(0xFF001F25) 23 | val md_theme_light_surface = Color(0xFFF8FDFF) 24 | val md_theme_light_onSurface = Color(0xFF001F25) 25 | val md_theme_light_surfaceVariant = Color(0xFFDEE3EB) 26 | val md_theme_light_onSurfaceVariant = Color(0xFF42474E) 27 | val md_theme_light_outline = Color(0xFF72777F) 28 | val md_theme_light_inverseOnSurface = Color(0xFFD6F6FF) 29 | val md_theme_light_inverseSurface = Color(0xFF00363F) 30 | val md_theme_light_inversePrimary = Color(0xFF99CBFF) 31 | val md_theme_light_shadow = Color(0xFF000000) 32 | val md_theme_light_surfaceTint = Color(0xFF00629D) 33 | val md_theme_light_outlineVariant = Color(0xFFC2C7CF) 34 | val md_theme_light_scrim = Color(0xFF000000) 35 | 36 | val md_theme_dark_primary = Color(0xFF99CBFF) 37 | val md_theme_dark_onPrimary = Color(0xFF003355) 38 | val md_theme_dark_primaryContainer = Color(0xFF004A78) 39 | val md_theme_dark_onPrimaryContainer = Color(0xFFCFE5FF) 40 | val md_theme_dark_secondary = Color(0xFF77D1FF) 41 | val md_theme_dark_onSecondary = Color(0xFF003549) 42 | val md_theme_dark_secondaryContainer = Color(0xFF004D68) 43 | val md_theme_dark_onSecondaryContainer = Color(0xFFC2E8FF) 44 | val md_theme_dark_tertiary = Color(0xFF85CFFF) 45 | val md_theme_dark_onTertiary = Color(0xFF00344C) 46 | val md_theme_dark_tertiaryContainer = Color(0xFF004C6C) 47 | val md_theme_dark_onTertiaryContainer = Color(0xFFC7E7FF) 48 | val md_theme_dark_error = Color(0xFFFFB4AB) 49 | val md_theme_dark_errorContainer = Color(0xFF93000A) 50 | val md_theme_dark_onError = Color(0xFF690005) 51 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 52 | val md_theme_dark_background = Color(0xFF001F25) 53 | val md_theme_dark_onBackground = Color(0xFFA6EEFF) 54 | val md_theme_dark_surface = Color(0xFF001F25) 55 | val md_theme_dark_onSurface = Color(0xFFA6EEFF) 56 | val md_theme_dark_surfaceVariant = Color(0xFF42474E) 57 | val md_theme_dark_onSurfaceVariant = Color(0xFFC2C7CF) 58 | val md_theme_dark_outline = Color(0xFF8C9199) 59 | val md_theme_dark_inverseOnSurface = Color(0xFF001F25) 60 | val md_theme_dark_inverseSurface = Color(0xFFA6EEFF) 61 | val md_theme_dark_inversePrimary = Color(0xFF00629D) 62 | val md_theme_dark_shadow = Color(0xFF000000) 63 | val md_theme_dark_surfaceTint = Color(0xFF99CBFF) 64 | val md_theme_dark_outlineVariant = Color(0xFF42474E) 65 | val md_theme_dark_scrim = Color(0xFF000000) 66 | 67 | 68 | val seed = Color(0xFF00A2FF) -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val lightColors = lightColorScheme( 19 | primary = md_theme_light_primary, 20 | onPrimary = md_theme_light_onPrimary, 21 | primaryContainer = md_theme_light_primaryContainer, 22 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 23 | secondary = md_theme_light_secondary, 24 | onSecondary = md_theme_light_onSecondary, 25 | secondaryContainer = md_theme_light_secondaryContainer, 26 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 27 | tertiary = md_theme_light_tertiary, 28 | onTertiary = md_theme_light_onTertiary, 29 | tertiaryContainer = md_theme_light_tertiaryContainer, 30 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 31 | error = md_theme_light_error, 32 | errorContainer = md_theme_light_errorContainer, 33 | onError = md_theme_light_onError, 34 | onErrorContainer = md_theme_light_onErrorContainer, 35 | background = md_theme_light_background, 36 | onBackground = md_theme_light_onBackground, 37 | surface = md_theme_light_surface, 38 | onSurface = md_theme_light_onSurface, 39 | surfaceVariant = md_theme_light_surfaceVariant, 40 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 41 | outline = md_theme_light_outline, 42 | inverseOnSurface = md_theme_light_inverseOnSurface, 43 | inverseSurface = md_theme_light_inverseSurface, 44 | inversePrimary = md_theme_light_inversePrimary, 45 | surfaceTint = md_theme_light_surfaceTint, 46 | outlineVariant = md_theme_light_outlineVariant, 47 | scrim = md_theme_light_scrim, 48 | ) 49 | 50 | private val darkColors = darkColorScheme( 51 | primary = md_theme_dark_primary, 52 | onPrimary = md_theme_dark_onPrimary, 53 | primaryContainer = md_theme_dark_primaryContainer, 54 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 55 | secondary = md_theme_dark_secondary, 56 | onSecondary = md_theme_dark_onSecondary, 57 | secondaryContainer = md_theme_dark_secondaryContainer, 58 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 59 | tertiary = md_theme_dark_tertiary, 60 | onTertiary = md_theme_dark_onTertiary, 61 | tertiaryContainer = md_theme_dark_tertiaryContainer, 62 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 63 | error = md_theme_dark_error, 64 | errorContainer = md_theme_dark_errorContainer, 65 | onError = md_theme_dark_onError, 66 | onErrorContainer = md_theme_dark_onErrorContainer, 67 | background = md_theme_dark_background, 68 | onBackground = md_theme_dark_onBackground, 69 | surface = md_theme_dark_surface, 70 | onSurface = md_theme_dark_onSurface, 71 | surfaceVariant = md_theme_dark_surfaceVariant, 72 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 73 | outline = md_theme_dark_outline, 74 | inverseOnSurface = md_theme_dark_inverseOnSurface, 75 | inverseSurface = md_theme_dark_inverseSurface, 76 | inversePrimary = md_theme_dark_inversePrimary, 77 | surfaceTint = md_theme_dark_surfaceTint, 78 | outlineVariant = md_theme_dark_outlineVariant, 79 | scrim = md_theme_dark_scrim, 80 | ) 81 | 82 | @Composable 83 | fun BlogMultiplatformTheme( 84 | darkTheme: Boolean = isSystemInDarkTheme(), 85 | // Dynamic color is available on Android 12+ 86 | dynamicColor: Boolean = false, 87 | content: @Composable () -> Unit 88 | ) { 89 | val colorScheme = when { 90 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 91 | val context = LocalContext.current 92 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 93 | } 94 | 95 | darkTheme -> darkColors 96 | else -> lightColors 97 | } 98 | val view = LocalView.current 99 | if (!view.isInEditMode) { 100 | SideEffect { 101 | val window = (view.context as Activity).window 102 | window.statusBarColor = colorScheme.primary.toArgb() 103 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 104 | } 105 | } 106 | 107 | MaterialTheme( 108 | colorScheme = colorScheme, 109 | typography = Typography, 110 | content = content 111 | ) 112 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/util/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.util 2 | 3 | object Constants { 4 | const val APP_ID = "myblogapp-bwscb" 5 | const val CATEGORY_ARGUMENT = "category" 6 | const val POST_ID_ARGUMENT = "postId" 7 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/util/Functions.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.util 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.util.Base64 6 | import java.lang.Exception 7 | import java.text.DateFormat 8 | import java.util.Date 9 | 10 | fun String.decodeThumbnailImage(): Bitmap? { 11 | return try { 12 | val byteArray = Base64.decode(this.cleanupImageString(), Base64.NO_WRAP) 13 | return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) 14 | } catch (e: Exception) { 15 | null 16 | } 17 | } 18 | 19 | fun String.cleanupImageString(): String { 20 | return this.replace("data:image/png;base64,", "") 21 | .replace("data:image/jpeg;base64,", "") 22 | } 23 | 24 | fun Long.convertLongToDate(): String { 25 | return DateFormat.getDateInstance().format(Date(this)) 26 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/stevdza/san/androidapp/util/RequestState.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.androidapp.util 2 | 3 | sealed class RequestState { 4 | data object Idle : RequestState() 5 | data object Loading : RequestState() 6 | data class Success(val data: T) : RequestState() 7 | data class Error(val error: Throwable) : RequestState() 8 | } 9 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/BlogMultiplatform/efea59b0865d3f454e71c58e304c1051d61d479b/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidapp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidApp 3 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |