diff --git a/build.gradle.kts b/build.gradle.kts
index 01aa79b16..e795bc8d6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -9,6 +9,7 @@ plugins {
id("com.google.firebase.firebase-perf") version "1.4.2" apply false
id("androidx.navigation.safeargs") version "2.8.0" apply false
id("com.github.ben-manes.versions") version "0.51.0" apply true
+ id("org.jetbrains.kotlin.plugin.compose") version "2.0.20" apply false
}
allprojects {
diff --git a/copy_mock_google_services_json.sh b/copy_mock_google_services_json.sh
index 56d42eb4c..c33f738b3 100755
--- a/copy_mock_google_services_json.sh
+++ b/copy_mock_google_services_json.sh
@@ -12,6 +12,7 @@ cp mock-google-services.json auth/app/google-services.json
cp mock-google-services.json config/app/google-services.json
cp mock-google-services.json crash/app/google-services.json
cp mock-google-services.json database/app/google-services.json
+cp mock-google-services.json dataconnect/app/google-services.json
cp mock-google-services.json dynamiclinks/app/google-services.json
cp mock-google-services.json firestore/app/google-services.json
cp mock-google-services.json functions/app/google-services.json
diff --git a/dataconnect/.gitignore b/dataconnect/.gitignore
new file mode 100644
index 000000000..5b4ab9b06
--- /dev/null
+++ b/dataconnect/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+.dataconnect/
+.firebaserc
diff --git a/dataconnect/README.md b/dataconnect/README.md
new file mode 100644
index 000000000..761f54539
--- /dev/null
+++ b/dataconnect/README.md
@@ -0,0 +1,97 @@
+# Firebase Data Connect Quickstart
+
+## Introduction
+
+This quickstart is a movie review app to demonstrate the use of Firebase Data Connect
+ with a Cloud SQL database.
+For more information about Firebase Data Connect visit [the docs](https://firebase.google.com/docs/data-connect/).
+
+## Getting Started
+
+Follow these steps to get up and running with Firebase Data Connect. For more detailed instructions,
+check out the [official documentation](https://firebase.google.com/docs/data-connect/quickstart).
+
+### 1. Connect to your Firebase project
+
+1. If you haven't already, create a Firebase project.
+ 1. In the [Firebase console](https://console.firebase.google.com), click
+ **Add project**, then follow the on-screen instructions.
+
+2. Upgrade your project to the Blaze plan. This lets you create a Cloud SQL
+ for PostgreSQL instance.
+
+ > Note: Though you set up billing in your Blaze upgrade, you won't be
+ charged for usage of Firebase Data Connect or the
+ [default Cloud SQL for PostgreSQL configuration](https://firebase.google.com/docs/data-connect/#pricing)
+ during the preview.
+
+3. Navigate to the [Data Connect section](https://console.firebase.google.com/u/0/project/_/dataconnect)
+ of the Firebase console, click on the "Get Started" button and follow the setup workflow:
+ - Select a location for your Cloud SQL for PostgreSQL database (this sample uses `us-central1`). If you choose a different location, you'll also need to change the `quickstart-android/dataconnect/dataconnect/dataconnect.yaml` file.
+ - Select the option to create a new Cloud SQL instance and fill in the following fields:
+ - Service ID: `dataconnect`
+ - Cloud SQL Instance ID: `fdc-sql`
+ - Database name: `fdcdb`
+4. Allow some time for the Cloud SQL instance to be provisioned. After it's provisioned, the instance
+ can be managed in the [Cloud Console](https://console.cloud.google.com/sql).
+
+5. If you haven’t already, add an Android app to your Firebase project, with the android package name `com.google.firebase.example.dataconnect`. Download and then add the Firebase Android configuration file (`google-services.json`) to your app:
+ 1. Click **Download google-services.json** to obtain your Firebase Android config file.
+ 2. Move your config file into the `quickstart-android/dataconnect/app` directory.
+
+### 2. Set Up Firebase CLI
+
+Ensure the Firebase CLI is installed and up to date:
+
+```bash
+npm install -g firebase-tools
+```
+
+### 3. Cloning the repository
+This repository contains the quickstart to get started with the functionalities of Data Connect.
+
+1. Clone this repository to your local machine:
+ ```sh
+ git clone https://github.com/firebase/quickstart-android.git
+ ```
+
+2. (Private Preview only) Checkout the `fdc-quickstart` branch (`git checkout fdc-quickstart`)
+ and open the project in Android Studio.
+
+### 4. Deploy the service to Firebase and generate SDKs
+
+1. Open the `quickstart-android/dataconnect/dataconnect` directory and deploy the schema with
+ the following command:
+ ```bash
+ firebase deploy
+ ```
+2. Once the deploy is complete, you should be able to see the movie schema in the
+ [Data Connect section](https://console.firebase.google.com/u/0/project/_/dataconnect)
+ of the Firebase console.
+
+3. Generate the Kotlin SDK by running:
+ ```bash
+ firebase dataconnect:sdk:generate
+ ```
+
+### 5. Populating the database
+1. Run `1_movie_insert.gql`, `2_actor_insert.gql`, `3_movie_actor_insert.gql`, and `4_user_favorites_review_insert.gql` files in the `./dataconnect` directory in order using the VS code extension,
+
+### 6. Running the app
+
+Press the Run button in Android Studio to run the sample app on your device.
+
+## 🚧 Work in Progress
+
+This app is still missing some features which will be added before Public Preview:
+
+- [ ] Search
+- [ ] Movie review
+ - [x] Add a new review
+ - [ ] Update a review
+ - [ ] Delete a review
+- [x] Actors
+ - [x] Show actor profile
+ - [x] Mark actor as favorite
+- [ ] Error handling
+ Some errors may cause the app to crash, especially if there's no user logged in.
diff --git a/dataconnect/app/.gitignore b/dataconnect/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/dataconnect/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/dataconnect/app/build.gradle.kts b/dataconnect/app/build.gradle.kts
new file mode 100644
index 000000000..7484eb7c2
--- /dev/null
+++ b/dataconnect/app/build.gradle.kts
@@ -0,0 +1,84 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.google.services)
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ namespace = "com.google.firebase.example.dataconnect"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.google.firebase.example.dataconnect"
+ minSdk = 23
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.13"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ sourceSets.getByName("main") {
+ java.srcDirs("build/generated/sources")
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.compose.navigation)
+ implementation(libs.androidx.lifecycle.runtime.compose.android)
+ implementation(libs.coil.compose)
+
+ // Firebase dependencies
+ implementation(libs.firebase.auth)
+ implementation(libs.firebase.dataconnect)
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
diff --git a/dataconnect/app/proguard-rules.pro b/dataconnect/app/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/dataconnect/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/dataconnect/app/src/main/AndroidManifest.xml b/dataconnect/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..230081caf
--- /dev/null
+++ b/dataconnect/app/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/MainActivity.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/MainActivity.kt
new file mode 100644
index 000000000..632b9ac78
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/MainActivity.kt
@@ -0,0 +1,135 @@
+package com.google.firebase.example.dataconnect
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavDestination.Companion.hasRoute
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.google.firebase.dataconnect.movies.MoviesConnector
+import com.google.firebase.dataconnect.movies.instance
+import com.google.firebase.example.dataconnect.feature.actordetail.ActorDetailRoute
+import com.google.firebase.example.dataconnect.feature.actordetail.ActorDetailScreen
+import com.google.firebase.example.dataconnect.feature.genredetail.GenreDetailRoute
+import com.google.firebase.example.dataconnect.feature.genredetail.GenreDetailScreen
+import com.google.firebase.example.dataconnect.feature.genres.GenresRoute
+import com.google.firebase.example.dataconnect.feature.genres.GenresScreen
+import com.google.firebase.example.dataconnect.feature.moviedetail.MovieDetailRoute
+import com.google.firebase.example.dataconnect.feature.moviedetail.MovieDetailScreen
+import com.google.firebase.example.dataconnect.feature.movies.MoviesRoute
+import com.google.firebase.example.dataconnect.feature.movies.MoviesScreen
+import com.google.firebase.example.dataconnect.feature.profile.ProfileRoute
+import com.google.firebase.example.dataconnect.feature.profile.ProfileScreen
+import com.google.firebase.example.dataconnect.feature.search.searchScreen
+import com.google.firebase.example.dataconnect.ui.theme.FirebaseDataConnectTheme
+
+data class TopLevelRoute(val labelResId: Int, val route: T, val icon: ImageVector)
+
+val TOP_LEVEL_ROUTES = listOf(
+ TopLevelRoute(R.string.label_movies, MoviesRoute, Icons.Filled.Home),
+ TopLevelRoute(R.string.label_genres, GenresRoute, Icons.Filled.Menu),
+ TopLevelRoute(R.string.label_profile, ProfileRoute, Icons.Filled.Person)
+)
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ // Comment the line below to use a production environment instead
+ MoviesConnector.instance.dataConnect.useEmulator("10.0.2.2", 9399)
+ setContent {
+ FirebaseDataConnectTheme {
+ val navController = rememberNavController()
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ bottomBar = {
+ NavigationBar {
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentDestination = navBackStackEntry?.destination
+
+ TOP_LEVEL_ROUTES.forEach { topLevelRoute ->
+ val label = stringResource(topLevelRoute.labelResId)
+ NavigationBarItem(
+ icon = { Icon(topLevelRoute.icon, contentDescription = label) },
+ label = { Text(label) },
+ selected = currentDestination?.hierarchy?.any {
+ it.hasRoute(topLevelRoute.route::class)
+ } == true,
+ onClick = {
+ navController.navigate(
+ topLevelRoute.route,
+ { launchSingleTop = true }
+ )
+ }
+ )
+ }
+ }
+ }
+ ) { innerPadding ->
+ NavHost(
+ navController,
+ startDestination = MoviesRoute,
+ Modifier
+ .padding(innerPadding)
+ .consumeWindowInsets(innerPadding),
+ ) {
+ composable() {
+ MoviesScreen(
+ onMovieClicked = { movieId ->
+ navController.navigate(
+ route = MovieDetailRoute(movieId),
+ builder = {
+ launchSingleTop = true
+ }
+ )
+ }
+ )
+ }
+ composable {
+ MovieDetailScreen(
+ onActorClicked = { actorId ->
+ navController.navigate(
+ ActorDetailRoute(actorId),
+ { launchSingleTop = true }
+ )
+ }
+ )
+ }
+ composable() { ActorDetailScreen() }
+ composable {
+ GenresScreen(onGenreClicked = { genre ->
+ navController.navigate(
+ GenreDetailRoute(genre),
+ { launchSingleTop = true }
+ )
+ })
+ }
+ composable { GenreDetailScreen() }
+ searchScreen()
+ composable { ProfileScreen() }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt
new file mode 100644
index 000000000..0441dbdc6
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt
@@ -0,0 +1,149 @@
+package com.google.firebase.example.dataconnect.feature.actordetail
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.google.firebase.dataconnect.movies.GetActorByIdQuery
+import com.google.firebase.example.dataconnect.R
+import com.google.firebase.example.dataconnect.ui.components.ErrorCard
+import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
+import com.google.firebase.example.dataconnect.ui.components.Movie
+import com.google.firebase.example.dataconnect.ui.components.MoviesList
+import com.google.firebase.example.dataconnect.ui.components.ToggleButton
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+data class ActorDetailRoute(val actorId: String)
+
+@Composable
+fun ActorDetailScreen(
+ actorDetailViewModel: ActorDetailViewModel = viewModel()
+) {
+ val uiState by actorDetailViewModel.uiState.collectAsState()
+ ActorDetailScreen(
+ uiState = uiState,
+ onMovieClicked = {
+ // TODO
+ },
+ onFavoriteToggled = {
+ actorDetailViewModel.toggleFavorite(it)
+ }
+ )
+}
+
+@Composable
+fun ActorDetailScreen(
+ uiState: ActorDetailUIState,
+ onMovieClicked: (actorId: String) -> Unit,
+ onFavoriteToggled: (newValue: Boolean) -> Unit
+) {
+ when (uiState) {
+ is ActorDetailUIState.Error -> ErrorCard(uiState.errorMessage)
+
+ is ActorDetailUIState.Loading -> LoadingScreen()
+
+ is ActorDetailUIState.Success -> {
+ Scaffold { innerPadding ->
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .verticalScroll(scrollState)
+ ) {
+ ActorInformation(
+ actor = uiState.actor,
+ isActorFavorite = uiState.isFavorite,
+ onFavoriteToggled = onFavoriteToggled
+ )
+ MoviesList(
+ listTitle = stringResource(R.string.title_main_roles),
+ movies = uiState.actor?.mainActors?.mapNotNull {
+ Movie(it.id.toString(), it.imageUrl, it.title)
+ },
+ onMovieClicked = onMovieClicked
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ MoviesList(
+ listTitle = stringResource(R.string.title_supporting_actors),
+ movies = uiState.actor?.supportingActors?.mapNotNull {
+ Movie(it.id.toString(), it.imageUrl, it.title)
+ },
+ onMovieClicked = onMovieClicked
+ )
+ }
+ }
+
+ }
+ }
+}
+
+@Composable
+fun ActorInformation(
+ modifier: Modifier = Modifier,
+ actor: GetActorByIdQuery.Data.Actor?,
+ isActorFavorite: Boolean,
+ onFavoriteToggled: (newValue: Boolean) -> Unit
+) {
+ if (actor == null) {
+ ErrorCard(stringResource(R.string.error_actor_not_found))
+ } else {
+ Column(
+ modifier = modifier
+ .padding(16.dp)
+ ) {
+ Text(
+ text = actor.name,
+ style = MaterialTheme.typography.headlineLarge
+ )
+ Row {
+ AsyncImage(
+ model = actor.imageUrl,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .width(150.dp)
+ .aspectRatio(9f / 16f)
+ .padding(vertical = 8.dp)
+ )
+ Text(
+ text = actor.biography ?: stringResource(R.string.biography_not_available),
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ )
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ ToggleButton(
+ iconEnabled = Icons.Filled.Favorite,
+ iconDisabled = Icons.Outlined.FavoriteBorder,
+ textEnabled = stringResource(R.string.button_remove_favorite),
+ textDisabled = stringResource(R.string.button_favorite),
+ isEnabled = isActorFavorite,
+ onToggle = onFavoriteToggled
+ )
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailUIState.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailUIState.kt
new file mode 100644
index 000000000..7f8ca2ff5
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailUIState.kt
@@ -0,0 +1,18 @@
+package com.google.firebase.example.dataconnect.feature.actordetail
+
+import com.google.firebase.dataconnect.movies.GetActorByIdQuery
+import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
+
+
+sealed class ActorDetailUIState {
+ data object Loading: ActorDetailUIState()
+
+ data class Error(val errorMessage: String?): ActorDetailUIState()
+
+ data class Success(
+ // Actor is null if it can't be found on the DB
+ val actor: GetActorByIdQuery.Data.Actor?,
+ val isUserSignedIn: Boolean = false,
+ var isFavorite: Boolean = false
+ ) : ActorDetailUIState()
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailViewModel.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailViewModel.kt
new file mode 100644
index 000000000..0ea3b4a2f
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailViewModel.kt
@@ -0,0 +1,83 @@
+package com.google.firebase.example.dataconnect.feature.actordetail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.google.firebase.Firebase
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.auth
+import com.google.firebase.dataconnect.movies.MoviesConnector
+import com.google.firebase.dataconnect.movies.execute
+import com.google.firebase.dataconnect.movies.instance
+import java.util.UUID
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+class ActorDetailViewModel(
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+ private val actorDetailRoute = savedStateHandle.toRoute()
+ private val actorId: String = actorDetailRoute.actorId
+
+ private val firebaseAuth: FirebaseAuth = Firebase.auth
+ private val moviesConnector: MoviesConnector = MoviesConnector.instance
+
+ private val _uiState = MutableStateFlow(ActorDetailUIState.Loading)
+ val uiState: StateFlow
+ get() = _uiState
+
+ init {
+ fetchActor()
+ }
+
+ private fun fetchActor() {
+ viewModelScope.launch {
+ try {
+ val user = firebaseAuth.currentUser
+ val actor = moviesConnector.getActorById.execute(
+ id = UUID.fromString(actorId)
+ ).data.actor
+
+ _uiState.value = if (user == null) {
+ ActorDetailUIState.Success(actor, isUserSignedIn = false)
+ } else {
+ val favoriteActor = moviesConnector.getIfFavoritedActor.execute(
+ id = user.uid,
+ actorId = UUID.fromString(actorId)
+ ).data.favoriteActor
+
+ val isFavorite = favoriteActor != null
+
+ ActorDetailUIState.Success(
+ actor,
+ isUserSignedIn = true,
+ isFavorite = isFavorite
+ )
+ }
+ } catch (e: Exception) {
+ _uiState.value = ActorDetailUIState.Error(e.message)
+ }
+ }
+ }
+
+ fun toggleFavorite(newValue: Boolean) {
+ viewModelScope.launch {
+ try {
+ if (newValue) {
+ moviesConnector.addFavoritedActor.execute(UUID.fromString(actorId))
+ } else {
+ moviesConnector.deleteFavoriteActor.execute(
+ userId = firebaseAuth.currentUser?.uid ?: "",
+ actorId = UUID.fromString(actorId)
+ )
+ }
+ // Re-run the query to fetch the actor details
+ fetchActor()
+ } catch (e: Exception) {
+ _uiState.value = ActorDetailUIState.Error(e.message)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailScreen.kt
new file mode 100644
index 000000000..c7e9e0e4b
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailScreen.kt
@@ -0,0 +1,76 @@
+package com.google.firebase.example.dataconnect.feature.genredetail
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.firebase.example.dataconnect.R
+import com.google.firebase.example.dataconnect.ui.components.ErrorCard
+import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
+import com.google.firebase.example.dataconnect.ui.components.Movie
+import com.google.firebase.example.dataconnect.ui.components.MoviesList
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GenreDetailRoute(val genre: String)
+
+@Composable
+fun GenreDetailScreen(
+ moviesViewModel: GenreDetailViewModel = viewModel()
+) {
+ val movies by moviesViewModel.uiState.collectAsState()
+ GenreDetailScreen(movies)
+}
+
+@Composable
+fun GenreDetailScreen(
+ uiState: GenreDetailUIState
+) {
+ when (uiState) {
+ GenreDetailUIState.Loading -> LoadingScreen()
+
+ is GenreDetailUIState.Error -> ErrorCard(uiState.errorMessage)
+
+ is GenreDetailUIState.Success -> {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .verticalScroll(scrollState)
+ ) {
+ Text(
+ text = stringResource(R.string.title_genre_detail, uiState.genreName),
+ style = MaterialTheme.typography.headlineLarge,
+ modifier = Modifier.padding(8.dp)
+ )
+ MoviesList(
+ listTitle = stringResource(R.string.title_most_popular),
+ movies = uiState.mostPopular.mapNotNull {
+ Movie(it.id.toString(), it.imageUrl, it.title, it.rating?.toFloat())
+ },
+ onMovieClicked = {
+ // TODO
+ }
+ )
+ MoviesList(
+ listTitle = stringResource(R.string.title_most_recent),
+ movies = uiState.mostRecent.mapNotNull {
+ Movie(it.id.toString(), it.imageUrl, it.title, it.rating?.toFloat())
+ },
+ onMovieClicked = {
+ // TODO
+ }
+ )
+ }
+ }
+ }
+}
+
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailUIState.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailUIState.kt
new file mode 100644
index 000000000..442776c3c
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailUIState.kt
@@ -0,0 +1,16 @@
+package com.google.firebase.example.dataconnect.feature.genredetail
+
+import com.google.firebase.dataconnect.movies.ListMoviesByGenreQuery
+
+sealed class GenreDetailUIState {
+
+ data object Loading: GenreDetailUIState()
+
+ data class Error(val errorMessage: String?): GenreDetailUIState()
+
+ data class Success(
+ val genreName: String,
+ val mostPopular: List,
+ val mostRecent: List
+ ) : GenreDetailUIState()
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailViewModel.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailViewModel.kt
new file mode 100644
index 000000000..2d327d101
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailViewModel.kt
@@ -0,0 +1,44 @@
+package com.google.firebase.example.dataconnect.feature.genredetail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.google.firebase.dataconnect.movies.MoviesConnector
+import com.google.firebase.dataconnect.movies.execute
+import com.google.firebase.dataconnect.movies.instance
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+class GenreDetailViewModel(
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+ private val genre = savedStateHandle.toRoute().genre
+ private val moviesConnector: MoviesConnector = MoviesConnector.instance
+
+ private val _uiState = MutableStateFlow(GenreDetailUIState.Loading)
+ val uiState: StateFlow
+ get() = _uiState
+
+ init {
+ fetchGenre()
+ }
+
+ private fun fetchGenre() {
+ viewModelScope.launch {
+ try {
+ val data = moviesConnector.listMoviesByGenre.execute(genre.lowercase()).data
+ val mostPopular = data.mostPopular
+ val mostRecent = data.mostRecent
+ _uiState.value = GenreDetailUIState.Success(
+ genreName = genre,
+ mostPopular = mostPopular,
+ mostRecent = mostRecent
+ )
+ } catch (e: Exception) {
+ _uiState.value = GenreDetailUIState.Error(e.message)
+ }
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genres/GenresScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genres/GenresScreen.kt
new file mode 100644
index 000000000..efa98a37b
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genres/GenresScreen.kt
@@ -0,0 +1,44 @@
+package com.google.firebase.example.dataconnect.feature.genres
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlinx.serialization.Serializable
+
+@Serializable
+object GenresRoute
+
+@Composable
+fun GenresScreen(
+ onGenreClicked: (genre: String) -> Unit = {}
+) {
+ // Hardcoding genres for now
+ val genres = arrayOf("Action", "Crime", "Drama", "Sci-Fi")
+
+ LazyColumn {
+ items(genres) { genre ->
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp, horizontal = 16.dp)
+ .clickable {
+ onGenreClicked(genre)
+ }
+ ) {
+ Text(
+ text = genre,
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt
new file mode 100644
index 000000000..9ca23be69
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt
@@ -0,0 +1,206 @@
+package com.google.firebase.example.dataconnect.feature.moviedetail
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.Check
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material.icons.outlined.Star
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SuggestionChip
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
+import com.google.firebase.example.dataconnect.R
+import com.google.firebase.example.dataconnect.ui.components.Actor
+import com.google.firebase.example.dataconnect.ui.components.ActorsList
+import com.google.firebase.example.dataconnect.ui.components.ErrorCard
+import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
+import com.google.firebase.example.dataconnect.ui.components.ReviewCard
+import com.google.firebase.example.dataconnect.ui.components.ToggleButton
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MovieDetailRoute(val movieId: String)
+
+@Composable
+fun MovieDetailScreen(
+ onActorClicked: (actorId: String) -> Unit,
+ movieDetailViewModel: MovieDetailViewModel = viewModel()
+) {
+ val uiState by movieDetailViewModel.uiState.collectAsState()
+ Scaffold { padding ->
+ when (uiState) {
+ is MovieDetailUIState.Error -> {
+ ErrorCard((uiState as MovieDetailUIState.Error).errorMessage)
+ }
+
+ MovieDetailUIState.Loading -> LoadingScreen()
+
+ is MovieDetailUIState.Success -> {
+ val ui = uiState as MovieDetailUIState.Success
+ val movie = ui.movie
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier.verticalScroll(scrollState)
+ ) {
+ MovieInformation(
+ modifier = Modifier.padding(padding),
+ movie = movie,
+ isMovieWatched = ui.isWatched,
+ isMovieFavorite = ui.isFavorite,
+ onFavoriteToggled = { newValue ->
+ movieDetailViewModel.toggleFavorite(newValue)
+ },
+ onWatchToggled = { newValue ->
+ movieDetailViewModel.toggleWatched(newValue)
+ }
+ )
+ // Main Actors list
+ ActorsList(
+ listTitle = stringResource(R.string.title_main_actors),
+ actors = movie?.mainActors?.mapNotNull {
+ Actor(it.id.toString(), it.name, it.imageUrl)
+ },
+ onActorClicked = { onActorClicked(it) }
+ )
+ // Supporting Actors list
+ ActorsList(
+ listTitle = stringResource(R.string.title_supporting_actors),
+ actors = movie?.supportingActors?.mapNotNull {
+ Actor(it.id.toString(), it.name, it.imageUrl)
+ },
+ onActorClicked = { onActorClicked(it) }
+ )
+ UserReviews(
+ onReviewSubmitted = { rating, text ->
+ movieDetailViewModel.addRating(rating, text)
+ },
+ movie?.reviews
+ )
+ }
+
+ }
+ }
+ }
+}
+
+@Composable
+fun MovieInformation(
+ modifier: Modifier = Modifier,
+ movie: GetMovieByIdQuery.Data.Movie?,
+ isMovieWatched: Boolean,
+ isMovieFavorite: Boolean,
+ onWatchToggled: (newValue: Boolean) -> Unit,
+ onFavoriteToggled: (newValue: Boolean) -> Unit
+) {
+ if (movie == null) {
+ ErrorCard(stringResource(R.string.error_movie_not_found))
+ } else {
+ Column(
+ modifier = modifier
+ .padding(16.dp)
+ ) {
+ Text(
+ text = movie.title,
+ style = MaterialTheme.typography.headlineLarge
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = movie.releaseYear.toString(),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(end = 4.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Icon(Icons.Outlined.Star, "Favorite")
+ Text(
+ text = movie.rating?.toString() ?: "0.0",
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(start = 2.dp)
+ )
+ }
+ Row {
+ AsyncImage(
+ model = movie.imageUrl,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .width(150.dp)
+ .aspectRatio(9f / 16f)
+ .padding(vertical = 8.dp)
+ )
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Row {
+ movie.tags?.let { movieTags ->
+ movieTags.filterNotNull().forEach { tag ->
+ SuggestionChip(
+ onClick = { },
+ label = { Text(tag) },
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ )
+ }
+ }
+ }
+ Text(
+ text = movie.description ?: stringResource(R.string.description_not_available),
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Row {
+ ToggleButton(
+ iconEnabled = Icons.Filled.CheckCircle,
+ iconDisabled = Icons.Outlined.Check,
+ textEnabled = stringResource(R.string.button_unmark_watched),
+ textDisabled = stringResource(R.string.button_mark_watched),
+ isEnabled = isMovieWatched,
+ onToggle = onWatchToggled
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ ToggleButton(
+ iconEnabled = Icons.Filled.Favorite,
+ iconDisabled = Icons.Outlined.FavoriteBorder,
+ textEnabled = stringResource(R.string.button_remove_favorite),
+ textDisabled = stringResource(R.string.button_favorite),
+ isEnabled = isMovieFavorite,
+ onToggle = onFavoriteToggled
+ )
+ }
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailUIState.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailUIState.kt
new file mode 100644
index 000000000..1f6e04028
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailUIState.kt
@@ -0,0 +1,18 @@
+package com.google.firebase.example.dataconnect.feature.moviedetail
+
+import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
+
+
+sealed class MovieDetailUIState {
+ data object Loading: MovieDetailUIState()
+
+ data class Error(val errorMessage: String?): MovieDetailUIState()
+
+ data class Success(
+ // Movie is null if it can't be found on the DB
+ val movie: GetMovieByIdQuery.Data.Movie?,
+ val isUserSignedIn: Boolean = false,
+ var isWatched: Boolean = false,
+ var isFavorite: Boolean = false
+ ) : MovieDetailUIState()
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailViewModel.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailViewModel.kt
new file mode 100644
index 000000000..fc19a4c36
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailViewModel.kt
@@ -0,0 +1,131 @@
+package com.google.firebase.example.dataconnect.feature.moviedetail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.google.firebase.Firebase
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.auth
+import com.google.firebase.dataconnect.movies.MoviesConnector
+import com.google.firebase.dataconnect.movies.execute
+import com.google.firebase.dataconnect.movies.instance
+import java.util.UUID
+import kotlin.math.roundToInt
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class MovieDetailViewModel(
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+ private val movieDetailRoute = savedStateHandle.toRoute()
+ private val movieId: String = movieDetailRoute.movieId
+
+ private val firebaseAuth: FirebaseAuth = Firebase.auth
+ private val moviesConnector: MoviesConnector = MoviesConnector.instance
+
+ private val _uiState = MutableStateFlow(MovieDetailUIState.Loading)
+ val uiState: StateFlow
+ get() = _uiState
+
+ init {
+ fetchMovie()
+ }
+
+ private fun fetchMovie() {
+ viewModelScope.launch {
+ try {
+ val user = firebaseAuth.currentUser
+ val movie = moviesConnector.getMovieById.execute(
+ id = UUID.fromString(movieId)
+ ).data.movie
+
+ _uiState.value = if (user == null) {
+ MovieDetailUIState.Success(movie, isUserSignedIn = false)
+ } else {
+ val isWatched = moviesConnector.getIfWatched.execute(
+ id = user.uid,
+ movieId = UUID.fromString(movieId)
+ ).data.watchedMovie != null
+
+ val isFavorite = moviesConnector.getIfFavoritedMovie.execute(
+ id = user.uid,
+ movieId = UUID.fromString(movieId)
+ ).data.favoriteMovie != null
+
+ MovieDetailUIState.Success(
+ movie = movie,
+ isUserSignedIn = true,
+ isWatched = isWatched,
+ isFavorite = isFavorite
+ )
+ }
+ } catch (e: Exception) {
+ _uiState.value = MovieDetailUIState.Error(e.message)
+ }
+ }
+ }
+
+ fun toggleFavorite(newValue: Boolean) {
+ viewModelScope.launch {
+ try {
+ if (newValue) {
+ moviesConnector.addFavoritedMovie.execute(UUID.fromString(movieId))
+ } else {
+ // TODO(thatfiredev): investigate whether this is a schema error
+ // userId probably shouldn't be here.
+ moviesConnector.deleteFavoritedMovie.execute(
+ userId = firebaseAuth.currentUser?.uid ?: "",
+ movieId = UUID.fromString(movieId)
+ )
+ }
+ // Re-run the query to fetch movie
+ fetchMovie()
+ } catch (e: Exception) {
+ _uiState.value = MovieDetailUIState.Error(e.message)
+ }
+ }
+ }
+
+ fun toggleWatched(newValue: Boolean) {
+ viewModelScope.launch {
+ try {
+ if (newValue) {
+ moviesConnector.addWatchedMovie.execute(UUID.fromString(movieId))
+ } else {
+ // TODO(thatfiredev): investigate whether this is a schema error
+ // userId probably shouldn't be here.
+ moviesConnector.deleteWatchedMovie.execute(
+ userId = firebaseAuth.currentUser?.uid ?: "",
+ movieId = UUID.fromString(movieId)
+ )
+ }
+ // Re-run the query to fetch movie
+ fetchMovie()
+ } catch (e: Exception) {
+ _uiState.value = MovieDetailUIState.Error(e.message)
+ }
+ }
+ }
+
+ fun addRating(rating: Float, text: String) {
+ viewModelScope.launch {
+ try {
+ moviesConnector.addReview.execute(
+ movieId = UUID.fromString(movieId),
+ // TODO(thatfiredev): this might have been an error in the mutation definition
+ // rating shouldn't be an Int!!
+ rating = rating.roundToInt(),
+ reviewText = text
+ )
+ // TODO(thatfiredev): should we have a way of only refetching the reviews?
+ // Re-run the query to fetch movie
+ fetchMovie()
+ } catch (e: Exception) {
+ _uiState.value = MovieDetailUIState.Error(e.message)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/UserReviews.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/UserReviews.kt
new file mode 100644
index 000000000..0b677d48d
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/UserReviews.kt
@@ -0,0 +1,84 @@
+package com.google.firebase.example.dataconnect.feature.moviedetail
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
+import com.google.firebase.example.dataconnect.R
+import com.google.firebase.example.dataconnect.ui.components.ReviewCard
+
+@Composable
+fun UserReviews(
+ onReviewSubmitted: (rating: Float, text: String) -> Unit,
+ reviews: List? = emptyList()
+) {
+ var reviewText by remember { mutableStateOf("") }
+ Text(
+ text = "User Reviews",
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ var rating by remember { mutableFloatStateOf(3f) }
+ Text("Rating: ${rating}")
+ Slider(
+ value = rating,
+ // Round the value to the nearest 0.5
+ onValueChange = { rating = (Math.round(it * 2) / 2.0).toFloat() },
+ steps = 9,
+ valueRange = 1f..5f
+ )
+ TextField(
+ value = reviewText,
+ onValueChange = { reviewText = it },
+ label = { Text(stringResource(R.string.hint_write_review)) },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ onClick = {
+ onReviewSubmitted(rating, reviewText)
+ reviewText = ""
+ }
+ ) {
+ Text(stringResource(R.string.button_submit_review))
+ }
+ }
+ Column {
+ // TODO(thatfiredev): Handle cases where the list is too long to display
+ reviews.orEmpty().forEach {
+ ReviewCard(
+ userName = it.user.username,
+ date = it.reviewDate,
+ rating = it.rating?.toDouble() ?: 0.0,
+ text = it.reviewText ?: ""
+ )
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesScreen.kt
new file mode 100644
index 000000000..c76090264
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesScreen.kt
@@ -0,0 +1,66 @@
+package com.google.firebase.example.dataconnect.feature.movies
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.firebase.example.dataconnect.R
+import com.google.firebase.example.dataconnect.ui.components.ErrorCard
+import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
+import com.google.firebase.example.dataconnect.ui.components.Movie
+import com.google.firebase.example.dataconnect.ui.components.MoviesList
+import kotlinx.serialization.Serializable
+
+@Serializable
+object MoviesRoute
+
+@Composable
+fun MoviesScreen(
+ onMovieClicked: (movie: String) -> Unit,
+ moviesViewModel: MoviesViewModel = viewModel()
+) {
+ val movies by moviesViewModel.uiState.collectAsState()
+ MoviesScreen(movies, onMovieClicked)
+}
+
+@Composable
+fun MoviesScreen(
+ uiState: MoviesUIState,
+ onMovieClicked: (movie: String) -> Unit
+) {
+ when (uiState) {
+ MoviesUIState.Loading -> LoadingScreen()
+ is MoviesUIState.Error -> ErrorCard(uiState.errorMessage)
+ is MoviesUIState.Success -> {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .verticalScroll(scrollState)
+ ) {
+ MoviesList(
+ listTitle = stringResource(R.string.title_top_10_movies),
+ movies = uiState.top10movies.mapNotNull {
+ Movie(it.id.toString(), it.imageUrl, it.title, it.rating?.toFloat())
+ },
+ onMovieClicked = onMovieClicked
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ MoviesList(
+ listTitle = stringResource(R.string.title_latest_movies),
+ movies = uiState.latestMovies.mapNotNull {
+ Movie(it.id.toString(), it.imageUrl, it.title, it.rating?.toFloat())
+ },
+ onMovieClicked = onMovieClicked
+ )
+ }
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesUIState.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesUIState.kt
new file mode 100644
index 000000000..3f428b2e5
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesUIState.kt
@@ -0,0 +1,16 @@
+package com.google.firebase.example.dataconnect.feature.movies
+
+import com.google.firebase.dataconnect.movies.MoviesRecentlyReleasedQuery
+import com.google.firebase.dataconnect.movies.MoviesTop10Query
+
+sealed class MoviesUIState {
+
+ data object Loading: MoviesUIState()
+
+ data class Error(val errorMessage: String?): MoviesUIState()
+
+ data class Success(
+ val top10movies: List,
+ val latestMovies: List
+ ) : MoviesUIState()
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesViewModel.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesViewModel.kt
new file mode 100644
index 000000000..932dca76e
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesViewModel.kt
@@ -0,0 +1,32 @@
+package com.google.firebase.example.dataconnect.feature.movies
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.firebase.dataconnect.movies.MoviesConnector
+import com.google.firebase.dataconnect.movies.execute
+import com.google.firebase.dataconnect.movies.instance
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+class MoviesViewModel(
+ private val moviesConnector: MoviesConnector = MoviesConnector.instance
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(MoviesUIState.Loading)
+ val uiState: StateFlow
+ get() = _uiState
+
+ init {
+ viewModelScope.launch {
+ try {
+ val top10Movies = moviesConnector.moviesTop10.execute().data.movies
+ val latestMovies = moviesConnector.moviesRecentlyReleased.execute().data.movies
+
+ _uiState.value = MoviesUIState.Success(top10Movies, latestMovies)
+ } catch (e: Exception) {
+ _uiState.value = MoviesUIState.Error(e.localizedMessage)
+ }
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/AuthScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/AuthScreen.kt
new file mode 100644
index 000000000..a9cf6e1df
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/AuthScreen.kt
@@ -0,0 +1,94 @@
+package com.google.firebase.example.dataconnect.feature.profile
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun AuthScreen(
+ onSignUp: (email: String, password: String, displayName: String) -> Unit,
+ onSignIn: (email: String, password: String) -> Unit,
+) {
+ var isSignUp by remember { mutableStateOf(false) }
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var displayName by remember { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ OutlinedTextField(
+ value = email,
+ onValueChange = { email = it },
+ label = { Text("Email") }
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ OutlinedTextField(
+ value = password,
+ onValueChange = { password = it },
+ label = { Text("Password") },
+ visualTransformation = PasswordVisualTransformation()
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ if (isSignUp) {
+ OutlinedTextField(
+ value = displayName,
+ onValueChange = { displayName = it },
+ label = { Text("Name") }
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(onClick = {
+ if (isSignUp) {
+ onSignUp(email, password, displayName)
+ } else {
+ onSignIn(email, password)
+ }
+ }) {
+ Text(
+ text = if (isSignUp) {
+ "Sign up"
+ } else {
+ "Sign in"
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = if (isSignUp) {
+ "Already have an account?"
+ } else {
+ "Don't have an account?"
+ }
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(onClick = {
+ isSignUp = !isSignUp
+ }) {
+ Text(
+ text = if (isSignUp) {
+ "Sign in"
+ } else {
+ "Sign up"
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileScreen.kt
new file mode 100644
index 000000000..c020552b6
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileScreen.kt
@@ -0,0 +1,173 @@
+package com.google.firebase.example.dataconnect.feature.profile
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.firebase.dataconnect.movies.GetUserByIdQuery
+import com.google.firebase.example.dataconnect.R
+import com.google.firebase.example.dataconnect.ui.components.Actor
+import com.google.firebase.example.dataconnect.ui.components.ActorsList
+import com.google.firebase.example.dataconnect.ui.components.ErrorCard
+import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
+import com.google.firebase.example.dataconnect.ui.components.Movie
+import com.google.firebase.example.dataconnect.ui.components.MoviesList
+import com.google.firebase.example.dataconnect.ui.components.ReviewCard
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ProfileRoute
+
+@Composable
+fun ProfileScreen(
+ profileViewModel: ProfileViewModel = viewModel()
+) {
+ val uiState by profileViewModel.uiState.collectAsState()
+ when (uiState) {
+ is ProfileUIState.Error -> {
+ ErrorCard((uiState as ProfileUIState.Error).errorMessage)
+ }
+
+ is ProfileUIState.AuthState -> {
+ AuthScreen(
+ onSignUp = { email, password, displayName ->
+ profileViewModel.signUp(email, password, displayName)
+ },
+ onSignIn = { email, password ->
+ profileViewModel.signIn(email, password)
+ }
+ )
+ }
+
+ is ProfileUIState.ProfileState -> {
+ val ui = uiState as ProfileUIState.ProfileState
+ ProfileScreen(
+ ui.username ?: "User",
+ ui.reviews.orEmpty(),
+ ui.watchedMovies.orEmpty(),
+ ui.favoriteMovies.orEmpty(),
+ ui.favoriteActors.orEmpty(),
+ onSignOut = {
+ profileViewModel.signOut()
+ }
+ )
+ }
+
+ ProfileUIState.Loading -> LoadingScreen()
+ }
+}
+
+@Composable
+fun ProfileScreen(
+ name: String,
+ reviews: List,
+ watchedMovies: List,
+ favoriteMovies: List,
+ favoriteActors: List,
+ onSignOut: () -> Unit
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .padding(vertical = 16.dp)
+ .verticalScroll(scrollState)
+ ) {
+ Text(
+ text = "Welcome back, $name!",
+ style = MaterialTheme.typography.displaySmall,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ TextButton(
+ onClick = {
+ onSignOut()
+ },
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Text("Sign out")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ MoviesList(
+ listTitle = stringResource(R.string.title_watched_movies),
+ movies = watchedMovies.mapNotNull {
+ Movie(it.movie.id.toString(), it.movie.imageUrl, it.movie.title, it.movie.rating?.toFloat())
+ },
+ onMovieClicked = {
+ // TODO
+ }
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ MoviesList(
+ listTitle = stringResource(R.string.title_favorite_movies),
+ movies = favoriteMovies.mapNotNull {
+ Movie(it.movie.id.toString(), it.movie.imageUrl, it.movie.title, it.movie.rating?.toFloat())
+ },
+ onMovieClicked = {
+ // TODO
+ }
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ ActorsList(
+ listTitle = stringResource(R.string.title_favorite_actors),
+ actors = favoriteActors.mapNotNull {
+ Actor(it.actor.id.toString(), it.actor.name, it.actor.imageUrl)
+ },
+ onActorClicked = {
+ // TODO
+ }
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ ProfileSection(title = "Reviews", content = { ReviewsList(name, reviews) })
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+}
+
+@Composable
+fun ProfileSection(title: String, content: @Composable () -> Unit) {
+ Column {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ content()
+ }
+}
+
+@Composable
+fun ReviewsList(
+ userName: String,
+ reviews: List
+) {
+ Column {
+ // TODO(thatfiredev): Handle cases where the list is too long to display
+ reviews.forEach { review ->
+ ReviewCard(
+ userName = userName,
+ date = review.reviewDate,
+ rating = review.rating?.toDouble() ?: 0.0,
+ text = review.reviewText ?: ""
+ )
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileUIState.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileUIState.kt
new file mode 100644
index 000000000..660c62abe
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileUIState.kt
@@ -0,0 +1,19 @@
+package com.google.firebase.example.dataconnect.feature.profile
+
+import com.google.firebase.dataconnect.movies.GetUserByIdQuery
+
+sealed class ProfileUIState {
+ data object Loading: ProfileUIState()
+
+ data class Error(val errorMessage: String?): ProfileUIState()
+
+ data object AuthState: ProfileUIState()
+
+ data class ProfileState(
+ val username: String?,
+ val reviews: List? = emptyList(),
+ val watchedMovies: List? = emptyList(),
+ val favoriteMovies: List? = emptyList(),
+ val favoriteActors: List? = emptyList()
+ ) : ProfileUIState()
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileViewModel.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileViewModel.kt
new file mode 100644
index 000000000..22a85ea90
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileViewModel.kt
@@ -0,0 +1,101 @@
+package com.google.firebase.example.dataconnect.feature.profile
+
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.firebase.Firebase
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.FirebaseAuth.AuthStateListener
+import com.google.firebase.auth.UserProfileChangeRequest
+import com.google.firebase.auth.auth
+import com.google.firebase.dataconnect.movies.MoviesConnector
+import com.google.firebase.dataconnect.movies.execute
+import com.google.firebase.dataconnect.movies.instance
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.tasks.await
+
+class ProfileViewModel(
+ private val auth: FirebaseAuth = Firebase.auth,
+ private val moviesConnector: MoviesConnector = MoviesConnector.instance
+) : ViewModel() {
+ private val _uiState = MutableStateFlow(ProfileUIState.Loading)
+ val uiState: StateFlow
+ get() = _uiState
+
+ private val authStateListener: AuthStateListener
+
+ init {
+ authStateListener = AuthStateListener {
+ val currentUser = auth.currentUser
+ if (currentUser != null) {
+ displayUser(currentUser.uid)
+ } else {
+ _uiState.value = ProfileUIState.AuthState
+ }
+ }
+ auth.addAuthStateListener(authStateListener)
+ }
+
+ fun signUp(
+ email: String,
+ password: String,
+ displayName: String
+ ) {
+ viewModelScope.launch {
+ try {
+ val signInResult = auth.createUserWithEmailAndPassword(email, password).await()
+ signInResult.user?.updateProfile(
+ UserProfileChangeRequest.Builder()
+ .setDisplayName(displayName)
+ .build()
+ )?.await()
+ moviesConnector.upsertUser.execute(username = displayName)
+ } catch (e: Exception) {
+ _uiState.value = ProfileUIState.Error(e.message)
+ e.printStackTrace()
+ }
+ }
+ }
+
+ fun signIn(email: String, password: String) {
+ viewModelScope.launch {
+ try {
+ auth.signInWithEmailAndPassword(email, password).await()
+ } catch (e: Exception) {
+ _uiState.value = ProfileUIState.Error(e.message)
+ }
+ }
+ }
+
+ fun signOut() {
+ auth.signOut()
+ }
+
+ private fun displayUser(
+ userId: String
+ ) {
+ viewModelScope.launch {
+ try {
+ val user = moviesConnector.getUserById.execute(id = userId).data.user
+ _uiState.value = ProfileUIState.ProfileState(
+ user?.username,
+ favoriteMovies = user?.favoriteMovies,
+ watchedMovies = user?.watched,
+ favoriteActors = user?.favoriteActors,
+ reviews = user?.reviews
+ )
+ Log.d("DisplayUser", "$user")
+ } catch (e: Exception) {
+ _uiState.value = ProfileUIState.Error(e.message)
+ }
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ auth.removeAuthStateListener(authStateListener)
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/search/Navigation.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/search/Navigation.kt
new file mode 100644
index 000000000..fee3bdf59
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/search/Navigation.kt
@@ -0,0 +1,21 @@
+package com.google.firebase.example.dataconnect.feature.search
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptionsBuilder
+import androidx.navigation.compose.composable
+
+const val SEARCH_ROUTE = "search_route"
+
+fun NavController.navigateToSearch(navOptions: NavOptionsBuilder.() -> Unit) =
+ navigate(SEARCH_ROUTE, navOptions)
+
+fun NavGraphBuilder.searchScreen(
+
+) {
+ composable(route = SEARCH_ROUTE) {
+ // TODO: Call composable
+ }
+}
+
+
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt
new file mode 100644
index 000000000..9e783df8d
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt
@@ -0,0 +1,107 @@
+package com.google.firebase.example.dataconnect.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+
+val ACTOR_CARD_SIZE = 64.dp
+
+/**
+ * Used to represent an actor in a list UI
+ */
+data class Actor(
+ val id: String,
+ val name: String,
+ val imageUrl: String
+)
+
+/**
+ * Displays a scrollable horizontal list of actors.
+ */
+@Composable
+fun ActorsList(
+ modifier: Modifier = Modifier,
+ listTitle: String,
+ actors: List? = emptyList(),
+ onActorClicked: (actorId: String) -> Unit
+) {
+ Column(
+ modifier = modifier.padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ ) {
+ Text(
+ text = listTitle,
+ style = MaterialTheme.typography.headlineMedium
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ LazyRow {
+ items(actors.orEmpty()) { actor ->
+ ActorTile(actor, onActorClicked)
+ }
+ }
+ }
+}
+
+/**
+ * Used to display each actor item in the list.
+ */
+@Composable
+fun ActorTile(
+ actor: Actor,
+ onActorClicked: (actorId: String) -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .clickable {
+ onActorClicked(actor.id)
+ }
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .sizeIn(
+ maxWidth = 160.dp,
+ maxHeight = ACTOR_CARD_SIZE + 16.dp
+ )
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ AsyncImage(
+ model = actor.imageUrl,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .size(ACTOR_CARD_SIZE)
+ .clip(CircleShape)
+ )
+ Text(
+ text = actor.name,
+ style = MaterialTheme.typography.bodyLarge,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ErrorCard.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ErrorCard.kt
new file mode 100644
index 000000000..f8c127124
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ErrorCard.kt
@@ -0,0 +1,37 @@
+package com.google.firebase.example.dataconnect.ui.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.google.firebase.example.dataconnect.R
+
+@Composable
+fun ErrorCard(
+ errorMessage: String?
+) {
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
+ modifier = Modifier.padding(16.dp)
+ .fillMaxWidth()
+ ) {
+ Text(
+ text = errorMessage ?: stringResource(R.string.unknown_error),
+ modifier = Modifier.padding(16.dp)
+ .fillMaxWidth()
+ )
+ }
+}
+
+@Composable
+@Preview
+fun ErrorCardPreview() {
+ ErrorCard("Something went terribly wrong :(")
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/LoadingScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/LoadingScreen.kt
new file mode 100644
index 000000000..df9a5a438
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/LoadingScreen.kt
@@ -0,0 +1,21 @@
+package com.google.firebase.example.dataconnect.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+/**
+ * A screen that displays a loading spinner in the center.
+ */
+@Composable
+fun LoadingScreen() {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ CircularProgressIndicator()
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt
new file mode 100644
index 000000000..0be44d7ab
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt
@@ -0,0 +1,103 @@
+package com.google.firebase.example.dataconnect.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+
+/**
+ * Used to represent a movie in a list UI
+ */
+data class Movie(
+ val id: String,
+ val imageUrl: String,
+ val title: String,
+ val rating: Float? = null
+)
+
+/**
+ * Displays a scrollable horizontal list of movies.
+ */
+@Composable
+fun MoviesList(
+ modifier: Modifier = Modifier,
+ listTitle: String,
+ movies: List? = emptyList(),
+ onMovieClicked: (movieId: String) -> Unit
+) {
+ Column(
+ modifier = modifier.padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ ) {
+ Text(
+ text = listTitle,
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ LazyRow {
+ items(movies.orEmpty()) { movie ->
+ MovieTile(
+ movie = movie,
+ onMovieClicked = {
+ onMovieClicked(movie.id.toString())
+ }
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Used to display each movie item in the list.
+ */
+@Composable
+fun MovieTile(
+ modifier: Modifier = Modifier,
+ tileWidth: Dp = 150.dp,
+ movie: Movie,
+ onMovieClicked: (movieId: String) -> Unit
+) {
+ Card(
+ modifier = modifier
+ .padding(4.dp)
+ .sizeIn(maxWidth = tileWidth)
+ .clickable {
+ onMovieClicked(movie.id)
+ },
+ ) {
+ AsyncImage(
+ model = movie.imageUrl,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.aspectRatio(9f / 16f)
+ )
+ Text(
+ text = movie.title,
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(8.dp),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ movie.rating?.let {
+ Text(
+ text = "Rating: $it",
+ modifier = Modifier.padding(bottom = 8.dp, start = 8.dp, end = 8.dp),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ReviewCard.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ReviewCard.kt
new file mode 100644
index 000000000..4df00e3c0
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ReviewCard.kt
@@ -0,0 +1,68 @@
+package com.google.firebase.example.dataconnect.ui.components
+
+import android.widget.Space
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import java.text.SimpleDateFormat
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.util.Date
+import java.util.Locale
+
+
+@Composable
+fun ReviewCard(
+ userName: String,
+ date: Date,
+ rating: Double,
+ text: String
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .background(color = MaterialTheme.colorScheme.secondaryContainer)
+ .padding(16.dp)
+ ) {
+ Text(
+ text = userName,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Row(
+ modifier = Modifier.padding(bottom = 8.dp)
+ ) {
+ Text(
+ text = SimpleDateFormat(
+ "dd MMM, yyyy",
+ Locale.getDefault()
+ ).format(date)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "Rating: ")
+ Text(text = "$rating")
+ }
+ Text(
+ text = text,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ToggleButton.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ToggleButton.kt
new file mode 100644
index 000000000..0d73abb8b
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ToggleButton.kt
@@ -0,0 +1,36 @@
+package com.google.firebase.example.dataconnect.ui.components
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ToggleButton(
+ iconEnabled: ImageVector,
+ iconDisabled: ImageVector,
+ textEnabled: String,
+ textDisabled: String,
+ isEnabled: Boolean,
+ onToggle: (newValue: Boolean) -> Unit
+) {
+ val onClick = {
+ onToggle(!isEnabled)
+ }
+ if (isEnabled) {
+ FilledTonalButton(onClick) {
+ Icon(iconEnabled, textEnabled)
+ Text(textEnabled, modifier = Modifier.padding(horizontal = 4.dp))
+ }
+ } else {
+ OutlinedButton(onClick) {
+ Icon(iconDisabled, textDisabled)
+ Text(textDisabled, modifier = Modifier.padding(horizontal = 4.dp))
+ }
+ }
+}
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Color.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Color.kt
new file mode 100644
index 000000000..e4c2b612a
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.google.firebase.example.dataconnect.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Theme.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Theme.kt
new file mode 100644
index 000000000..b327e3af2
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package com.google.firebase.example.dataconnect.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun FirebaseDataConnectTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Type.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Type.kt
new file mode 100644
index 000000000..deec73173
--- /dev/null
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.google.firebase.example.dataconnect.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/dataconnect/app/src/main/res/drawable/firebase_data_connect.xml b/dataconnect/app/src/main/res/drawable/firebase_data_connect.xml
new file mode 100644
index 000000000..e2106454e
--- /dev/null
+++ b/dataconnect/app/src/main/res/drawable/firebase_data_connect.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/dataconnect/app/src/main/res/drawable/ic_launcher_background.xml b/dataconnect/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/dataconnect/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..8eeb203f9
--- /dev/null
+++ b/dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..8eeb203f9
--- /dev/null
+++ b/dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dataconnect/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dataconnect/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..40f804ae7
Binary files /dev/null and b/dataconnect/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/dataconnect/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dataconnect/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..64276c653
Binary files /dev/null and b/dataconnect/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/dataconnect/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/dataconnect/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..aa4fd7ba1
Binary files /dev/null and b/dataconnect/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/dataconnect/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dataconnect/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8abe93867
Binary files /dev/null and b/dataconnect/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/dataconnect/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dataconnect/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b7be5d464
Binary files /dev/null and b/dataconnect/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/dataconnect/app/src/main/res/values/colors.xml b/dataconnect/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..f8c6127d3
--- /dev/null
+++ b/dataconnect/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/dataconnect/app/src/main/res/values/strings.xml b/dataconnect/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..15c39cb40
--- /dev/null
+++ b/dataconnect/app/src/main/res/values/strings.xml
@@ -0,0 +1,45 @@
+
+ Firebase Data Connect
+ An unknown error occurred
+
+
+ Movies
+ Genres
+ Search
+ Profile
+
+
+ Top 10 Movies
+ Latest Movies
+
+
+ %s Movies
+ Most Popular
+ Most Recent
+
+
+ Couldn\'t find movie in the database
+ Description not available
+ Mark as watched
+ Watched
+ Add to favorites
+ Favorite
+ Main Actors
+ Supporting Actors
+ User Reviews
+ Write your review
+ Submit Review
+
+
+ Couldn\'t find actor in the database
+ Biography not available
+ Main Roles
+ Supporting Roles
+
+
+ Watched Movies
+ Favorite Movies
+ Favorite Actors
+ Reviews
+
+
\ No newline at end of file
diff --git a/dataconnect/app/src/main/res/values/themes.xml b/dataconnect/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..761a1fca9
--- /dev/null
+++ b/dataconnect/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/dataconnect/app/src/main/res/xml/backup_rules.xml b/dataconnect/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 000000000..fa0f996d2
--- /dev/null
+++ b/dataconnect/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/dataconnect/app/src/main/res/xml/data_extraction_rules.xml b/dataconnect/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..9ee9997b0
--- /dev/null
+++ b/dataconnect/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dataconnect/build.gradle.kts b/dataconnect/build.gradle.kts
new file mode 100644
index 000000000..8e1379dd6
--- /dev/null
+++ b/dataconnect/build.gradle.kts
@@ -0,0 +1,7 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.jetbrains.kotlin.android) apply false
+ alias(libs.plugins.google.services) apply false
+ alias(libs.plugins.compose.compiler) apply false
+}
diff --git a/dataconnect/dataconnect/connectors/connector.yaml b/dataconnect/dataconnect/connectors/connector.yaml
new file mode 100644
index 000000000..2ba51749d
--- /dev/null
+++ b/dataconnect/dataconnect/connectors/connector.yaml
@@ -0,0 +1,13 @@
+connectorId: movies
+# Required. Accepted values are either "PUBLIC" or "ADMIN" (only "PUBLIC" for gated private
+# preview). If "ADMIN", the connector in this directory is an AdminConnector and its operations
+# are gated by IAM.
+authMode: PUBLIC
+generate:
+ # (Web SDK generation omitted, but can be found in https://github.com/firebase/quickstart-js)
+ kotlinSdk:
+ # Create a custom package name for your generated SDK
+ package: com.google.firebase.dataconnect.movies
+ # Specify where to store the generated SDK
+ # We're using the build/ directory so that generated code doesn't get checked into git
+ outputDir: ../../app/build/generated/sources/com/google/firebase/dataconnect/movies
diff --git a/dataconnect/dataconnect/connectors/mutations.gql b/dataconnect/dataconnect/connectors/mutations.gql
new file mode 100644
index 000000000..324c223d8
--- /dev/null
+++ b/dataconnect/dataconnect/connectors/mutations.gql
@@ -0,0 +1,113 @@
+# Create a movie based on user input
+mutation createMovie(
+ $title: String!
+ $releaseYear: Int!
+ $genre: String!
+ $rating: Float
+ $description: String
+ $imageUrl: String!
+ $tags: [String!] = []
+) {
+ movie_insert(
+ data: {
+ title: $title
+ releaseYear: $releaseYear
+ genre: $genre
+ rating: $rating
+ description: $description
+ imageUrl: $imageUrl
+ tags: $tags
+ }
+ )
+}
+
+# Update movie information based on the provided ID
+mutation updateMovie(
+ $id: UUID!
+ $title: String
+ $releaseYear: Int
+ $genre: String
+ $rating: Float
+ $description: String
+ $imageUrl: String
+ $tags: [String!] = []
+) {
+ movie_update(
+ id: $id
+ data: {
+ title: $title
+ releaseYear: $releaseYear
+ genre: $genre
+ rating: $rating
+ description: $description
+ imageUrl: $imageUrl
+ tags: $tags
+ }
+ )
+}
+
+# Delete a movie by its ID
+mutation deleteMovie($id: UUID!) {
+ movie_delete(id: $id)
+}
+
+# Delete movies with a rating lower than the specified minimum rating
+mutation deleteUnpopularMovies($minRating: Float!) {
+ movie_deleteMany(where: { rating: { le: $minRating } })
+}
+
+# Add a movie to the user's watched list
+mutation addWatchedMovie($movieId: UUID!) @auth(level: USER) {
+ watchedMovie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
+}
+
+# Remove a movie from the user's watched list
+mutation deleteWatchedMovie($userId: String!, $movieId: UUID!) @auth(level: USER) {
+ watchedMovie_delete(key: { userId: $userId, movieId: $movieId })
+}
+
+# Add a movie to the user's favorites list
+mutation addFavoritedMovie($movieId: UUID!) @auth(level: USER) {
+ favoriteMovie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
+}
+
+# Remove a movie from the user's favorites list
+mutation deleteFavoritedMovie($userId: String!, $movieId: UUID!) @auth(level: USER) {
+ favoriteMovie_delete(key: { userId: $userId, movieId: $movieId })
+}
+
+# Add an actor to the user's favorites list
+mutation addFavoritedActor($actorId: UUID!) @auth(level: USER) {
+ favoriteActor_upsert(data: { userId_expr: "auth.uid", actorId: $actorId })
+}
+
+# Remove an actor from the user's favorites list
+mutation deleteFavoriteActor($userId: String!, $actorId: UUID!) @auth(level: USER) {
+ favoriteActor_delete(key: { userId: $userId, actorId: $actorId })
+}
+
+# Add a review for a movie
+mutation addReview($movieId: UUID!, $rating: Int!, $reviewText: String!) @auth(level: USER) {
+ review_upsert(
+ data: {
+ userId_expr: "auth.uid"
+ movieId: $movieId
+ rating: $rating
+ reviewText: $reviewText
+ reviewDate_date: { today: true }
+ }
+ )
+}
+
+# Delete a user's review for a movie
+mutation deleteReview($movieId: UUID!, $userId: String!) @auth(level: USER) {
+ review_delete(key: { userId: $userId, movieId: $movieId })
+}
+
+# Upsert (update or insert) a user based on their username
+mutation upsertUser($username: String!) @auth(level: USER) {
+ user_upsert(data: {
+ id_expr: "auth.uid",
+ username: $username
+ })
+}
\ No newline at end of file
diff --git a/dataconnect/dataconnect/connectors/queries.gql b/dataconnect/dataconnect/connectors/queries.gql
new file mode 100644
index 000000000..8f8653101
--- /dev/null
+++ b/dataconnect/dataconnect/connectors/queries.gql
@@ -0,0 +1,565 @@
+# List subset of fields for movies
+query ListMovies @auth(level: PUBLIC) {
+ movies {
+ id
+ title
+ imageUrl
+ releaseYear
+ genre
+ rating
+ tags
+ }
+}
+
+# List subset of fields for users
+query ListUsers @auth(level: PUBLIC) {
+ users {
+ id
+ username
+ favoriteActors_on_user {
+ actor {
+ id
+ name
+ imageUrl
+ }
+ }
+ favoriteMovies_on_user {
+ movie {
+ id
+ title
+ genre
+ imageUrl
+ tags
+ }
+ }
+ reviews_on_user {
+ id
+ rating
+ reviewText
+ reviewDate
+ movie {
+ id
+ title
+ }
+ }
+ watchedMovies_on_user {
+ movie {
+ id
+ title
+ genre
+ imageUrl
+ }
+ }
+ }
+}
+
+# List movies of a certain genre
+query ListMoviesByGenre($genre: String!) @auth(level: PUBLIC) {
+ mostPopular: movies(
+ where: { genre: { eq: $genre } }
+ orderBy: { rating: DESC }
+ ) {
+ id
+ title
+ imageUrl
+ rating
+ tags
+ }
+ mostRecent: movies(
+ where: { genre: { eq: $genre } }
+ orderBy: { releaseYear: DESC }
+ ) {
+ id
+ title
+ imageUrl
+ rating
+ tags
+ }
+}
+
+# List movies by the order of release
+query ListMoviesByReleaseYear @auth(level: PUBLIC) {
+ movies(orderBy: [{ releaseYear: DESC }]) {
+ id
+ title
+ imageUrl
+ }
+}
+
+# Get movie by id
+query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
+ movie(id: $id) {
+ id
+ title
+ imageUrl
+ releaseYear
+ genre
+ rating
+ description
+ tags
+ metadata: movieMetadatas_on_movie {
+ director
+ }
+ mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
+ id
+ name
+ imageUrl
+ }
+ supportingActors: actors_via_MovieActor(
+ where: { role: { eq: "supporting" } }
+ ) {
+ id
+ name
+ imageUrl
+ }
+ sequelTo {
+ id
+ title
+ imageUrl
+ }
+ reviews: reviews_on_movie {
+ id
+ reviewText
+ reviewDate
+ rating
+ user {
+ id
+ username
+ }
+ }
+ }
+}
+
+# Get actor by id
+query GetActorById($id: UUID!) @auth(level: PUBLIC) {
+ actor(id: $id) {
+ id
+ name
+ imageUrl
+ biography
+ mainActors: movies_via_MovieActor(where: { role: { eq: "main" } }) {
+ id
+ title
+ genre
+ tags
+ imageUrl
+ }
+ supportingActors: movies_via_MovieActor(
+ where: { role: { eq: "supporting" } }
+ ) {
+ id
+ title
+ genre
+ tags
+ imageUrl
+ }
+ }
+}
+
+# User movie preferences
+query UserMoviePreferences($username: String!) @auth(level: USER) {
+ users(where: { username: { eq: $username } }) {
+ likedMovies: movies_via_Review(where: { rating: { ge: 4 } }) {
+ title
+ imageUrl
+ genre
+ description
+ }
+ dislikedMovies: movies_via_Review(where: { rating: { le: 2 } }) {
+ title
+ imageUrl
+ genre
+ description
+ }
+ }
+}
+
+# Get movie metadata
+query GetMovieMetadata($id: UUID!) @auth(level: PUBLIC) {
+ movie(id: $id) {
+ movieMetadatas_on_movie {
+ director
+ }
+ }
+}
+
+# Get movie cast and actor roles
+query GetMovieCast($movieId: UUID!, $actorId: UUID!) @auth(level: PUBLIC) {
+ movie(id: $movieId) {
+ mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
+ id
+ name
+ imageUrl
+ }
+ supportingActors: actors_via_MovieActor(
+ where: { role: { eq: "supporting" } }
+ ) {
+ id
+ name
+ imageUrl
+ }
+ }
+ actor(id: $actorId) {
+ mainRoles: movies_via_MovieActor(where: { role: { eq: "main" } }) {
+ id
+ title
+ imageUrl
+ }
+ supportingRoles: movies_via_MovieActor(
+ where: { role: { eq: "supporting" } }
+ ) {
+ id
+ title
+ imageUrl
+ }
+ }
+}
+
+# List movies by partial title match
+query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) {
+ movies(where: { title: { contains: $input } }) {
+ id
+ title
+ genre
+ rating
+ imageUrl
+ }
+}
+
+# Fetch a single movie using key scalars (same as get movie by id)
+query MovieByKey($key: Movie_Key!) @auth(level: PUBLIC) {
+ movie(key: $key) {
+ title
+ imageUrl
+ }
+}
+
+# Fetch movies by title
+query MovieByTitle($title: String!) @auth(level: PUBLIC) {
+ movies(where: { title: { eq: $title } }) {
+ id
+ title
+ imageUrl
+ genre
+ rating
+ }
+}
+
+# Fetch top-rated movies by genre
+query MovieByTopRating($genre: String) @auth(level: PUBLIC) {
+ mostPopular: movies(
+ where: { genre: { eq: $genre } }
+ orderBy: { rating: DESC }
+ ) {
+ id
+ title
+ imageUrl
+ rating
+ tags
+ }
+}
+
+# List movies by tag
+query ListMoviesByTag($tag: String!) @auth(level: PUBLIC) {
+ movies(where: { tags: { includes: $tag } }) {
+ id
+ title
+ imageUrl
+ genre
+ rating
+ }
+}
+
+# List top 10 movies
+query MoviesTop10 @auth(level: PUBLIC) {
+ movies(orderBy: [{ rating: DESC }], limit: 10) {
+ id
+ title
+ imageUrl
+ rating
+ genre
+ tags
+ metadata: movieMetadatas_on_movie {
+ director
+ }
+ mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
+ id
+ name
+ imageUrl
+ }
+ supportingActors: actors_via_MovieActor(where: { role: { eq: "supporting" } }) {
+ id
+ name
+ imageUrl
+ }
+ }
+}
+
+# List movies by release year range
+query MoviesByReleaseYear($min: Int, $max: Int) @auth(level: PUBLIC) {
+ movies(
+ where: { releaseYear: { le: $max, ge: $min } }
+ orderBy: [{ releaseYear: ASC }]
+ ) {
+ id
+ rating
+ title
+ imageUrl
+ }
+}
+
+# List recently released movies
+query MoviesRecentlyReleased @auth(level: PUBLIC) {
+ movies(where: { releaseYear: { ge: 2010 } }) {
+ id
+ title
+ rating
+ imageUrl
+ genre
+ tags
+ }
+}
+
+# List movies with filtering on fields
+query ListMoviesFilter($genre: String, $limit: Int) @auth(level: PUBLIC) {
+ movies(where: { genre: { eq: $genre } }, limit: $limit) {
+ title
+ imageUrl
+ }
+}
+
+# List movies by partial title string match
+query ListMoviesByTitleString(
+ $prefix: String
+ $suffix: String
+ $contained: String
+) @auth(level: PUBLIC) {
+ prefixed: movies(where: { description: { startsWith: $prefix } }) {
+ title
+ }
+ suffixed: movies(where: { description: { endsWith: $suffix } }) {
+ title
+ }
+ contained: movies(where: { description: { contains: $contained } }) {
+ title
+ }
+}
+
+# List movies by rating and genre with OR/AND filters
+query ListMoviesByRatingAndGenre($minRating: Float!, $genre: String)
+@auth(level: PUBLIC) {
+ movies(
+ where: { _or: [{ rating: { ge: $minRating } }, { genre: { eq: $genre } }] }
+ ) {
+ title
+ imageUrl
+ }
+}
+
+# Get favorite movies by user ID
+query GetFavoriteMoviesById($id: String!) @auth(level: USER) {
+ user(id: $id) {
+ favoriteMovies_on_user {
+ movie {
+ id
+ title
+ genre
+ imageUrl
+ releaseYear
+ rating
+ description
+ }
+ }
+ }
+}
+
+# Get favorite actors by user ID
+query GetFavoriteActorsById($id: String!) @auth(level: USER) {
+ user(id: $id) {
+ favoriteActors_on_user {
+ actor {
+ id
+ name
+ imageUrl
+ }
+ }
+ }
+}
+
+# Get watched movies by user ID
+query GetWatchedMoviesByAuthId($id: String!) @auth(level: USER) {
+ user(id: $id) {
+ watchedMovies_on_user {
+ movie {
+ id
+ title
+ genre
+ imageUrl
+ releaseYear
+ rating
+ description
+ }
+ }
+ }
+}
+
+# Get user by ID
+query GetUserById($id: String!) @auth(level: USER) {
+ user(id: $id) {
+ id
+ username
+ reviews: reviews_on_user {
+ id
+ rating
+ reviewDate
+ reviewText
+ movie {
+ id
+ title
+ }
+ }
+ watched: watchedMovies_on_user {
+ movie {
+ id
+ title
+ genre
+ imageUrl
+ releaseYear
+ rating
+ description
+ tags
+ metadata: movieMetadatas_on_movie {
+ director
+ }
+ }
+ }
+ favoriteMovies: favoriteMovies_on_user {
+ movie {
+ id
+ title
+ genre
+ imageUrl
+ releaseYear
+ rating
+ description
+ tags
+ metadata: movieMetadatas_on_movie {
+ director
+ }
+ }
+ }
+ favoriteActors: favoriteActors_on_user {
+ actor {
+ id
+ name
+ imageUrl
+ }
+ }
+ }
+}
+
+# Check if a movie is watched by user
+query GetIfWatched($id: String!, $movieId: UUID!) @auth(level: USER) {
+ watchedMovie(key: { userId: $id, movieId: $movieId }) {
+ movieId
+ }
+}
+
+# Check if a movie is favorited by user
+query GetIfFavoritedMovie($id: String!, $movieId: UUID!) @auth(level: USER) {
+ favoriteMovie(key: { userId: $id, movieId: $movieId }) {
+ movieId
+ }
+}
+
+# Check if an actor is favorited by user
+query GetIfFavoritedActor($id: String!, $actorId: UUID!) @auth(level: USER) {
+ favoriteActor(key: { userId: $id, actorId: $actorId }) {
+ actorId
+ }
+}
+
+# Fuzzy search for movies, actors, and reviews
+query fuzzySearch(
+ $input: String
+ $minYear: Int!
+ $maxYear: Int!
+ $minRating: Float!
+ $maxRating: Float!
+ $genre: String!
+) @auth(level: PUBLIC) {
+ moviesMatchingTitle: movies(
+ where: {
+ _and: [
+ { releaseYear: { ge: $minYear } }
+ { releaseYear: { le: $maxYear } }
+ { rating: { ge: $minRating } }
+ { rating: { le: $maxRating } }
+ { genre: { contains: $genre } }
+ { title: { contains: $input } }
+ ]
+ }
+ ) {
+ id
+ title
+ genre
+ rating
+ imageUrl
+ }
+ moviesMatchingDescription: movies(
+ where: {
+ _and: [
+ { releaseYear: { ge: $minYear } }
+ { releaseYear: { le: $maxYear } }
+ { rating: { ge: $minRating } }
+ { rating: { le: $maxRating } }
+ { genre: { contains: $genre } }
+ { description: { contains: $input } }
+ ]
+ }
+ ) {
+ id
+ title
+ genre
+ rating
+ imageUrl
+ }
+ actorsMatchingName: actors(where: { name: { contains: $input } }) {
+ id
+ name
+ imageUrl
+ }
+ reviewsMatchingText: reviews(where: { reviewText: { contains: $input } }) {
+ id
+ rating
+ reviewText
+ reviewDate
+ movie {
+ id
+ title
+ }
+ user {
+ id
+ username
+ }
+ }
+}
+
+# Search movie descriptions using L2 similarity with Vertex AI
+query searchMovieDescriptionUsingL2Similarity($query: String!)
+@auth(level: PUBLIC) {
+ movies_descriptionEmbedding_similarity(
+ compare_embed: { model: "textembedding-gecko@001", text: $query }
+ method: L2
+ within: 2
+ where: { description: { ne: "" } }
+ limit: 5
+ ) {
+ id
+ title
+ description
+ tags
+ rating
+ imageUrl
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/dataconnect/data_seed.gql b/dataconnect/dataconnect/data_seed.gql
new file mode 100644
index 000000000..4e912b7e3
--- /dev/null
+++ b/dataconnect/dataconnect/data_seed.gql
@@ -0,0 +1,546 @@
+mutation {
+ # Insert movies
+ movie_insertMany(data: [
+ {
+ id: "550e8400-e29b-41d4-a716-446655440000",
+ title: "Inception",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Finception.jpg?alt=media&token=07b09781-b302-4623-a5c3-1956d0143168",
+ releaseYear: 2010,
+ genre: "sci-fi",
+ rating: 8.8,
+ description: "Dom Cobb (Leonardo DiCaprio) is a thief with the rare ability to enter people's dreams and steal their secrets from their subconscious. His skill has made him a valuable player in the world of corporate espionage but has also cost him everything he loves. Cobb gets a chance at redemption when he is offered a seemingly impossible task: plant an idea in someone's mind. If he succeeds, it will be the perfect crime, but a dangerous enemy anticipates Cobb's every move.",
+ tags: ["thriller", "action"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440001",
+ title: "The Matrix",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_matrix.jpg?alt=media&token=4975645d-fef8-409e-84a5-bcc1046e2059",
+ releaseYear: 1999,
+ genre: "action",
+ rating: 8.7,
+ description: "Thomas Anderson, a computer programmer, discovers that the world is actually a simulation controlled by malevolent machines in a dystopian future. Known as Neo, he joins a group of underground rebels led by Morpheus to fight the machines and free humanity. Along the way, Neo learns to manipulate the simulated reality, uncovering his true destiny.",
+ tags: ["sci-fi", "adventure"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440002",
+ title: "John Wick 4",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fjohn_wick_4.jpg?alt=media&token=463ed467-9daa-4281-965d-44e7cc4172d5",
+ releaseYear: 2023,
+ genre: "action",
+ rating: 8.1,
+ description: "John Wick (Keanu Reeves) uncovers a path to defeating The High Table, but before he can earn his freedom, he must face off against a new enemy with powerful alliances across the globe. The film follows Wick as he battles through various international locations, facing relentless adversaries and forming new alliances.",
+ tags: ["action", "thriller"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440003",
+ title: "The Dark Knight",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_dark_knight.jpg?alt=media&token=a9803c59-40d5-4758-a6f4-9a7c274a1218",
+ releaseYear: 2008,
+ genre: "action",
+ rating: 9.0,
+ description: "When the menace known as the Joker (Heath Ledger) emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham. The Dark Knight (Christian Bale) must accept one of the greatest psychological and physical tests of his ability to fight injustice.",
+ tags: ["action", "drama"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440004",
+ title: "Fight Club",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Ffight_club.jpg?alt=media&token=a4bc1933-2607-42cd-a860-e44c4587fd9c",
+ releaseYear: 1999,
+ genre: "drama",
+ rating: 8.8,
+ description: "An insomniac office worker (Edward Norton) and a devil-may-care soapmaker (Brad Pitt) form an underground fight club that evolves into something much more. The story explores themes of consumerism, masculinity, and the search for identity.",
+ tags: ["drama", "thriller"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440005",
+ title: "Pulp Fiction",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fpulp_fiction.jpg?alt=media&token=0df86e18-5cb1-45b3-a6d9-3f41563c3465",
+ releaseYear: 1994,
+ genre: "crime",
+ rating: 8.9,
+ description: "The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption. The film is known for its eclectic dialogue, ironic mix of humor and violence, and a host of cinematic allusions and pop culture references.",
+ tags: ["crime", "drama"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440006",
+ title: "The Lord of the Rings: The Fellowship of the Ring",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Flotr_fellowship.jpg?alt=media&token=92641d2d-6c52-4172-bd66-95fb86b4b96b",
+ releaseYear: 2001,
+ genre: "fantasy",
+ rating: 8.8,
+ description: "A meek Hobbit from the Shire, Frodo Baggins, and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron. The epic adventure begins the quest that will test their courage and bond.",
+ tags: ["fantasy", "adventure"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440007",
+ title: "The Shawshank Redemption",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_shawshanks_redemption.jpg?alt=media&token=f67b5ab2-a435-48b2-8251-5bf866b183e9",
+ releaseYear: 1994,
+ genre: "drama",
+ rating: 9.3,
+ description: "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency. The film follows Andy Dufresne (Tim Robbins), a banker sentenced to life in Shawshank State Penitentiary, and his friendship with Red (Morgan Freeman).",
+ tags: ["drama", "crime"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440008",
+ title: "Forrest Gump",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fforrest_gump.jpeg?alt=media&token=f21e88ce-6fab-4218-aa55-94738acc9b8f",
+ releaseYear: 1994,
+ genre: "drama",
+ rating: 8.8,
+ description: "The presidencies of Kennedy and Johnson, the events of Vietnam, Watergate, and other historical moments unfold from the perspective of an Alabama man with a low IQ. Forrest Gump (Tom Hanks) becomes an unwitting participant in many key moments of 20th-century U.S. history, all while maintaining his love for his childhood sweetheart Jenny (Robin Wright).",
+ tags: ["drama", "romance"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440009",
+ title: "The Godfather",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_godfather.jpg?alt=media&token=5297fd94-ae87-4995-9de5-3755232bad52",
+ releaseYear: 1972,
+ genre: "crime",
+ rating: 9.2,
+ description: "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son. The story follows the powerful Corleone family as they navigate power, loyalty, and betrayal.",
+ tags: ["crime", "drama"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440010",
+ title: "The Silence of the Lambs",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_silence_of_the_lambs.jpg?alt=media&token=7ca6abeb-b15c-4f5e-9280-5a590e89fe54",
+ releaseYear: 1991,
+ genre: "thriller",
+ rating: 8.6,
+ description: "A young F.B.I. cadet must receive the help of an incarcerated and manipulative cannibal killer to help catch another serial killer. Clarice Starling (Jodie Foster) seeks the assistance of Hannibal Lecter (Anthony Hopkins) to understand the mind of a killer.",
+ tags: ["thriller", "crime"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440011",
+ title: "Saving Private Ryan",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fsaving_private_ryan.jpg?alt=media&token=58ed877e-7ae0-4e30-9aee-d45c2deb7a00",
+ releaseYear: 1998,
+ genre: "war",
+ rating: 8.6,
+ description: "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action. The harrowing journey of Captain John H. Miller (Tom Hanks) and his men highlights the brutal reality of war.",
+ tags: ["war", "drama"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440012",
+ title: "The Avengers",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_avengers.jpg?alt=media&token=3d68ccad-2fa1-48da-a83e-7941e246c9f9",
+ releaseYear: 2012,
+ genre: "action",
+ rating: 8.0,
+ description: "Earth's mightiest heroes, including Iron Man, Captain America, Thor, Hulk, Black Widow, and Hawkeye, must come together to stop Loki and his alien army from enslaving humanity. Directed by Joss Whedon, the film is known for its witty dialogue, intense action sequences, and the chemistry among its ensemble cast.",
+ tags: ["action", "sci-fi"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440013",
+ title: "Gladiator",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fgladiator.jpg?alt=media&token=61d75825-b79f-4add-afdb-7da5eed53407",
+ releaseYear: 2000,
+ genre: "action",
+ rating: 8.5,
+ description: "A former Roman General, Maximus Decimus Meridius, seeks vengeance against the corrupt emperor Commodus who murdered his family and sent him into slavery. Directed by Ridley Scott, the film is known for its epic scale, intense battle scenes, and Russell Crowe's powerful performance.",
+ tags: ["action", "drama"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440014",
+ title: "Titanic",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Ftitanic.png?alt=media&token=dd03dc83-486e-4b03-9b03-2f9ed83fd9d0",
+ releaseYear: 1997,
+ genre: "romance",
+ rating: 7.8,
+ description: "A romantic drama recounting the ill-fated voyage of the R.M.S. Titanic through the eyes of Jack Dawson, a poor artist, and Rose DeWitt Bukater, a wealthy aristocrat. Their forbidden romance unfolds aboard the luxurious ship, which tragically sinks after striking an iceberg. Directed by James Cameron, the film is known for its epic scale, emotional depth, and stunning visuals.",
+ tags: ["romance", "drama"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440015",
+ title: "Avatar",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Favatar.jpg?alt=media&token=1c75b09d-7c7a-44bf-b7ad-e7da4d0b7193",
+ releaseYear: 2009,
+ genre: "sci-fi",
+ rating: 7.8,
+ description: "A paraplegic Marine named Jake Sully is sent on a unique mission to Pandora, an alien world, to bridge relations with the native Na'vi people. Torn between following his orders and protecting the world he feels is his home, Jake's journey becomes a battle for survival. Directed by James Cameron, 'Avatar' is renowned for its groundbreaking special effects and immersive 3D experience.",
+ tags: ["sci-fi", "adventure"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440016",
+ title: "Jurassic Park",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fjurassic_park.jpg?alt=media&token=1731ce71-3384-4435-8a5b-821d4fd286d3",
+ releaseYear: 1993,
+ genre: "adventure",
+ rating: 8.1,
+ description: "During a preview tour, a theme park suffers a major power breakdown that allows its cloned dinosaur exhibits to run amok. Directed by Steven Spielberg, 'Jurassic Park' is known for its groundbreaking special effects, thrilling storyline, and the suspenseful chaos unleashed by its prehistoric creatures.",
+ tags: ["adventure", "sci-fi"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440017",
+ title: "The Lion King",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_lion_king.jpg?alt=media&token=3e4e4265-6ae7-47d6-a5ba-584de126ef00",
+ releaseYear: 1994,
+ genre: "animation",
+ rating: 8.5,
+ description: "A young lion prince, Simba, must overcome betrayal and tragedy to reclaim his rightful place as king. 'The Lion King' is a beloved animated musical known for its memorable songs, stunning animation, and a timeless tale of courage, loyalty, and redemption.",
+ tags: ["animation", "drama"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440018",
+ title: "Star Wars: Episode IV - A New Hope",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fstar_wars_4.jpg?alt=media&token=b4ea7e0c-707f-43dd-8633-9d962e77b5a4",
+ releaseYear: 1977,
+ genre: "sci-fi",
+ rating: 8.6,
+ description: "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee, and two droids to save the galaxy from the Empire's world-destroying battle station, the Death Star. Directed by George Lucas, 'A New Hope' revolutionized the sci-fi genre with its groundbreaking special effects and unforgettable characters.",
+ tags: ["sci-fi", "adventure"]
+ },
+ {
+ id: "550e8400-e29b-41d4-a716-446655440019",
+ title: "Blade Runner",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fblade_runner.jpg?alt=media&token=d8e94bdd-1477-49f3-b244-dd7a9c059fc1",
+ releaseYear: 1982,
+ genre: "sci-fi",
+ rating: 8.1,
+ description: "In a dystopian future, synthetic humans known as replicants are created by powerful corporations. A blade runner named Rick Deckard is tasked with hunting down and 'retiring' four replicants who have escaped to Earth. Directed by Ridley Scott, 'Blade Runner' is a seminal sci-fi thriller that explores themes of humanity and identity.",
+ tags: ["sci-fi", "thriller"]
+ }
+ ])
+
+ # Insert Actors
+ actor_insertMany(data: [
+ {
+ id: "123e4567-e89b-12d3-a456-426614174000",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fdicaprio.jpeg?alt=media&token=452e030a-efa5-4ef4-bb81-502b23241316",
+ name: "Leonardo DiCaprio"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174001",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fkeanu.jpg?alt=media&token=6056520c-ef3e-4823-aad0-108aab163115",
+ name: "Keanu Reeves"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174002",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fmcconaoghey.jpg?alt=media&token=d340ca18-ef51-44ac-a160-08e45b242cd7",
+ name: "Matthew McConaughey"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174003",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fbale.jpg?alt=media&token=666f1690-a550-458f-a1cf-9505b7d1af7d",
+ name: "Christian Bale"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174004",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fpitt.jpeg?alt=media&token=3a5283d4-f85c-4ba7-be72-51bc87ca4133",
+ name: "Brad Pitt"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174005",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fjackson.jpg?alt=media&token=07be0601-19fe-4b5d-b111-84fa71f32139",
+ name: "Samuel L. Jackson"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174006",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fmortensen.jpeg?alt=media&token=e3d1ec99-b8e7-42e9-9d1c-03f56f61ecf7",
+ name: "Viggo Mortensen"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174007",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Ffreeman.jpg?alt=media&token=94bc6227-119e-4ab0-b350-55fac7aeb062",
+ name: "Morgan Freeman"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174008",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fhanks.jpeg?alt=media&token=d92979ce-da62-4b28-afbe-b8740bbb9d32",
+ name: "Tom Hanks"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174009",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fpacino.jpg?alt=media&token=9c0c54b1-6913-48b5-8e5e-d6551dd2f182",
+ name: "Al Pacino"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174010",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Ffoster.jpg?alt=media&token=b429734c-0f2d-4840-b75b-6857eac7bb4f",
+ name: "Jodie Foster"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174011",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fcruise.jpg?alt=media&token=d34b0326-a8d1-4f01-86e5-f3f094594e5a",
+ name: "Tom Cruise"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174012",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fdowney.jpg?alt=media&token=dd291c96-6ef0-42fc-841c-902c80748b37",
+ name: "Robert Downey Jr."
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174013",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fcrowe.jpg?alt=media&token=46d413d5-ace8-404e-b018-8d7e6fe0d362",
+ name: "Russell Crowe"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174014",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fwinslet.jpg?alt=media&token=b675585e-356e-4361-a041-5ac1a6ee5922",
+ name: "Kate Winslet"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174015",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fweaver.jpeg?alt=media&token=263b5c3d-e0ee-43c3-854d-9b236c6df391",
+ name: "Sigourney Weaver"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174016",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fgoldblume.jpeg?alt=media&token=18277dd1-166c-4934-a02e-19ef141c86e2",
+ name: "Jeff Goldblum"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174017",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fjones.jpg?alt=media&token=f7ac9bc4-6e26-4b25-9a73-7a90f699424e",
+ name: "James Earl Jones"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174018",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fford.jpg?alt=media&token=928434c0-d492-4c8e-bdf0-0db585008d87",
+ name: "Harrison Ford"
+ },
+ {
+ id: "123e4567-e89b-12d3-a456-426614174019",
+ imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/actors%2Fschwarzenegger.jpeg?alt=media&token=c46fb249-41ef-4084-b4ad-9517bee6ab46",
+ name: "Arnold Schwarzenegger"
+ }
+ ])
+ # Insert Movie Actor Joins
+ movieMetadata_insertMany(data: [
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440000",
+ director: "Christopher Nolan"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440001",
+ director: "Lana Wachowski, Lilly Wachowski"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440002",
+ director: "Chad Stahelski"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440003",
+ director: "Christopher Nolan"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440004",
+ director: "David Fincher"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440005",
+ director: "Quentin Tarantino"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440006",
+ director: "Peter Jackson"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440007",
+ director: "Frank Darabont"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440008",
+ director: "Robert Zemeckis"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440009",
+ director: "Francis Ford Coppola"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440010",
+ director: "Jonathan Demme"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440011",
+ director: "Steven Spielberg"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440012",
+ director: "Joss Whedon"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440013",
+ director: "Ridley Scott"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440014",
+ director: "James Cameron"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440015",
+ director: "James Cameron"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440016",
+ director: "Steven Spielberg"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440017",
+ director: "Roger Allers, Rob Minkoff"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440018",
+ director: "George Lucas"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440019",
+ director: "Ridley Scott"
+ }
+ ])
+
+ movieActor_insertMany(data: [
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440000",
+ actorId: "123e4567-e89b-12d3-a456-426614174000",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440001",
+ actorId: "123e4567-e89b-12d3-a456-426614174001",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440002",
+ actorId: "123e4567-e89b-12d3-a456-426614174001",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440003",
+ actorId: "123e4567-e89b-12d3-a456-426614174003",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440004",
+ actorId: "123e4567-e89b-12d3-a456-426614174004",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440005",
+ actorId: "123e4567-e89b-12d3-a456-426614174005",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440006",
+ actorId: "123e4567-e89b-12d3-a456-426614174006",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440007",
+ actorId: "123e4567-e89b-12d3-a456-426614174007",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440008",
+ actorId: "123e4567-e89b-12d3-a456-426614174008",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440009",
+ actorId: "123e4567-e89b-12d3-a456-426614174009",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440010",
+ actorId: "123e4567-e89b-12d3-a456-426614174010",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440011",
+ actorId: "123e4567-e89b-12d3-a456-426614174011",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440012",
+ actorId: "123e4567-e89b-12d3-a456-426614174012",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440013",
+ actorId: "123e4567-e89b-12d3-a456-426614174013",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440014",
+ actorId: "123e4567-e89b-12d3-a456-426614174014",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440015",
+ actorId: "123e4567-e89b-12d3-a456-426614174015",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440016",
+ actorId: "123e4567-e89b-12d3-a456-426614174016",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440017",
+ actorId: "123e4567-e89b-12d3-a456-426614174017",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440018",
+ actorId: "123e4567-e89b-12d3-a456-426614174018",
+ role: "main"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440019",
+ actorId: "123e4567-e89b-12d3-a456-426614174019",
+ role: "main"
+ },
+ # Supporting actors
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440000",
+ actorId: "123e4567-e89b-12d3-a456-426614174001",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440001",
+ actorId: "123e4567-e89b-12d3-a456-426614174002",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440002",
+ actorId: "123e4567-e89b-12d3-a456-426614174003",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440003",
+ actorId: "123e4567-e89b-12d3-a456-426614174004",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440004",
+ actorId: "123e4567-e89b-12d3-a456-426614174005",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440005",
+ actorId: "123e4567-e89b-12d3-a456-426614174006",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440006",
+ actorId: "123e4567-e89b-12d3-a456-426614174007",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440007",
+ actorId: "123e4567-e89b-12d3-a456-426614174008",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440008",
+ actorId: "123e4567-e89b-12d3-a456-426614174009",
+ role: "supporting"
+ },
+ {
+ movieId: "550e8400-e29b-41d4-a716-446655440009",
+ actorId: "123e4567-e89b-12d3-a456-426614174010",
+ role: "supporting"
+ }
+ ])
+}
\ No newline at end of file
diff --git a/dataconnect/dataconnect/dataconnect.yaml b/dataconnect/dataconnect/dataconnect.yaml
new file mode 100644
index 000000000..1eea139ad
--- /dev/null
+++ b/dataconnect/dataconnect/dataconnect.yaml
@@ -0,0 +1,11 @@
+specVersion: "v1alpha"
+serviceId: "dataconnect"
+location: "us-central1"
+schema:
+ source: "./schema"
+ datasource:
+ postgresql:
+ database: "fdcdb"
+ cloudSql:
+ instanceId: "fdc-sql"
+connectorDirs: ["./connectors"]
diff --git a/dataconnect/dataconnect/schema/schema.gql b/dataconnect/dataconnect/schema/schema.gql
new file mode 100644
index 000000000..1c7f04c76
--- /dev/null
+++ b/dataconnect/dataconnect/schema/schema.gql
@@ -0,0 +1,103 @@
+# Movies
+type Movie
+ @table(name: "Movies", singular: "movie", plural: "movies", key: ["id"]) {
+ id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
+ title: String!
+ imageUrl: String! @col(name: "image_url")
+ releaseYear: Int @col(name: "release_year")
+ genre: String
+ rating: Float @col(name: "rating")
+ description: String @col(name: "description")
+ tags: [String] @col(name: "tags")
+ # Vectors
+ descriptionEmbedding: Vector @col(size:768) # vector
+}
+
+# Movie Metadata
+# Movie - MovieMetadata is a one-to-one relationship
+type MovieMetadata
+ @table(
+ name: "MovieMetadata"
+ ) {
+ # @ref creates a field in the current table (MovieMetadata) that holds the primary key of the referenced type
+ # In this case, @ref(fields: "id") is implied
+ movie: Movie! @ref
+ # movieId: UUID <- this is created by the above @ref
+ director: String @col(name: "director")
+ # TODO: optional other fields
+}
+
+# Actors
+# Suppose an actor can participate in multiple movies and movies can have multiple actors
+# Movie - Actors (or vice versa) is a many to many relationship
+type Actor @table(name: "Actors", singular: "actor", plural: "actors") {
+ id: UUID! @col(name: "actor_id") @default(expr: "uuidV4()")
+ imageUrl: String! @col(name: "image_url")
+ name: String! @col(name: "name", dataType: "varchar(30)")
+ biography: String @col(name: "biography")
+}
+
+# Join table for many-to-many relationship for movies and actors
+# The 'key' param signifies the primary key(s) of this table
+# In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor]
+type MovieActor @table(key: ["movie", "actor"]) {
+ # @ref creates a field in the current table (MovieActor) that holds the primary key of the referenced type
+ # In this case, @ref(fields: "id") is implied
+ movie: Movie! @ref
+ # movieId: UUID! <- this is created by the above @ref, see: implicit.gql
+
+ actor: Actor! @ref
+ # actorId: UUID! <- this is created by the above @ref, see: implicit.gql
+
+ role: String! @col(name: "role") # "main" or "supporting"
+ # TODO: optional other fields
+}
+
+# Users
+# Suppose a user can leave reviews for movies
+# user:reviews is a one to many relationship, movie:reviews is a one to many relationship, movie:user is a many to many relationship
+type User
+ @table(name: "Users", singular: "user", plural: "users", key: ["id"]) {
+ # id: UUID! @col(name: "user_id") @default(expr: "uuidV4()")
+ id: String! @col(name: "user_auth")
+ username: String! @col(name: "username", dataType: "varchar(50)")
+ # The following are generated from the @ref in the Review table
+ # reviews_on_user
+ # movies_via_Review
+}
+
+# Join table for many-to-many relationship for users and favorite movies
+type FavoriteMovie
+ @table(name: "FavoriteMovies", key: ["user", "movie"]) {
+ user: User! @ref
+ movie: Movie! @ref
+}
+
+# Join table for many-to-many relationship for users and favorite actors
+type FavoriteActor
+ @table(name: "FavoriteActors", key: ["user", "actor"]) {
+ user: User! @ref
+ actor: Actor! @ref
+}
+
+# Join table for many-to-many relationship for users and watched movies
+type WatchedMovie
+ @table(name: "WatchedMovies", key: ["user", "movie"]) {
+ user: User! @ref
+ movie: Movie! @ref
+}
+
+# Reviews
+type Review @table(name: "Reviews", key: ["movie", "user"]) {
+ id: UUID! @col(name: "review_id") @default(expr: "uuidV4()")
+ user: User! @ref
+ movie: Movie! @ref
+ rating: Int
+ reviewText: String
+ reviewDate: Date! @default(expr: "request.time")
+}
+
+# Self Joins
+extend type Movie {
+ sequelTo: Movie
+}
diff --git a/dataconnect/firebase.json b/dataconnect/firebase.json
new file mode 100644
index 000000000..73f599717
--- /dev/null
+++ b/dataconnect/firebase.json
@@ -0,0 +1,5 @@
+{
+ "dataconnect": {
+ "source": "dataconnect"
+ }
+}
diff --git a/dataconnect/gradle.properties b/dataconnect/gradle.properties
new file mode 100644
index 000000000..20e2a0152
--- /dev/null
+++ b/dataconnect/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/dataconnect/gradle/wrapper/gradle-wrapper.jar b/dataconnect/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..e708b1c02
Binary files /dev/null and b/dataconnect/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/dataconnect/gradle/wrapper/gradle-wrapper.properties b/dataconnect/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..367d4dffd
--- /dev/null
+++ b/dataconnect/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed May 08 19:29:05 BST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/dataconnect/gradlew b/dataconnect/gradlew
new file mode 100755
index 000000000..4f906e0c8
--- /dev/null
+++ b/dataconnect/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/dataconnect/gradlew.bat b/dataconnect/gradlew.bat
new file mode 100644
index 000000000..107acd32c
--- /dev/null
+++ b/dataconnect/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/dataconnect/settings.gradle.kts b/dataconnect/settings.gradle.kts
new file mode 100644
index 000000000..67a300440
--- /dev/null
+++ b/dataconnect/settings.gradle.kts
@@ -0,0 +1,31 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencyResolutionManagement {
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+ }
+}
+
+rootProject.name = "Firebase Data Connect"
+include(":app")
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..b4330e046
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,44 @@
+[versions]
+agp = "8.6.0"
+coilCompose = "2.6.0"
+firebaseAuth = "23.0.0"
+firebaseDataConnect = "16.0.0-alpha05"
+kotlin = "2.0.20"
+coreKtx = "1.13.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycle = "2.8.1"
+activityCompose = "1.9.0"
+composeBom = "2023.08.00"
+googleServices = "4.4.2"
+composeNavigation = "2.8.1"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-lifecycle-runtime-compose-android = { module = "androidx.lifecycle:lifecycle-runtime-compose-android", version.ref = "lifecycle" }
+androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
+coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
+firebase-auth = { module = "com.google.firebase:firebase-auth", version.ref = "firebaseAuth" }
+firebase-dataconnect = { module = "com.google.firebase:firebase-dataconnect", version.ref = "firebaseDataConnect" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"}
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/mock-google-services.json b/mock-google-services.json
index bbeae18b3..ec61f6ecb 100644
--- a/mock-google-services.json
+++ b/mock-google-services.json
@@ -958,6 +958,25 @@
}
}
},
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:474448463284:android:c76572afd2f0ba8d97e8e1",
+ "android_client_info": {
+ "package_name": "com.google.firebase.example.dataconnect"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCPndbsEs_QWumL5_B0BpNLuMkvVSecvL0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
{
"client_info": {
"mobilesdk_app_id": "1:474448463284:android:c76572afd2f0ba8d97e8e1",
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6c6e686cf..af004974b 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -13,6 +13,7 @@ include(":admob:app",
":config:app",
":crash:app",
":database:app",
+ ":dataconnect:app",
":dynamiclinks:app",
":firestore:app",
":functions:app",