diff --git a/README.md b/README.md index 29e3098..9b0fa8a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ -# starter-for-android -Appwrite's starter kit for Android 🤖 \ No newline at end of file +# Android Starter Kit with Appwrite + +Kickstart your Android development with this ready-to-use starter project integrated +with [Appwrite](https://appwrite.io). + +This guide will help you quickly set up, customize, and build your Android app using **Jetpack +Compose**. + +--- + +## 🚀 Getting Started + +### Clone the Project + +Clone this repository to your local machine using Git or directly from `Android Studio`: + +```bash +git clone https://github.com/appwrite/starter-for-android +``` + +Alternatively, open the repository URL in `Android Studio` to clone it directly. + +--- + +## 🛠️ Development Guide + +1. **Configure Appwrite** + Navigate to `constants/AppwriteConfig.kt` and update the values to match your Appwrite project + credentials. + +2. **Customize as Needed** + Modify the starter kit to suit your app's requirements. Adjust UI, features, or backend + integrations as per your needs. + +3. **Run the App** + Select a target device (emulator or connected physical Android device) in `Android Studio`, and + click **Run** to start the app. + +--- + +## 📦 Building for Production + +To create a production build of your app: + +1. Open **Build variants** > **app** in the menu bar. +2. Choose **release** +3. Build and deploy your app in release mode. + +--- + +## 💡 Additional Notes + +- This starter project is designed to streamline your Android development with Appwrite. +- Refer to the [Appwrite Documentation](https://appwrite.io/docs) for detailed integration guidance. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..26a084f --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +.idea +build +gradle +local.properties +.externalNativeBuild + +/captures +.DS_Store diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..3b91603 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "io.appwrite.starterkit" + compileSdk = 35 + + defaultConfig { + minSdk = 21 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + applicationId = "io.appwrite.starterkit" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } +} + +dependencies { + // appwrite + implementation(libs.appwrite) + + // splashscreen + implementation(libs.androidx.core.splashscreen) + + // core, compose and runtime + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + + // ui, preview & material + implementation(libs.androidx.ui) + implementation(libs.androidx.material3) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + + // compose platform + implementation(platform(libs.androidx.compose.bom)) + + // debug libraries + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/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/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..193fe94 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <uses-permission android:name="android.permission.INTERNET" /> + + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="AppwriteStarterKit" + android:supportsRtl="true" + android:theme="@style/Theme.AppwriteStarterKit.SplashTheme" + tools:targetApi="31"> + <activity + android:name=".MainActivity" + android:exported="true" + android:windowSoftInputMode="adjustResize"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/MainActivity.kt b/app/src/main/java/io/appwrite/starterkit/MainActivity.kt new file mode 100644 index 0000000..d13eade --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/MainActivity.kt @@ -0,0 +1,138 @@ +package io.appwrite.starterkit + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.annotation.RestrictTo +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import androidx.lifecycle.viewmodel.compose.viewModel +import io.appwrite.starterkit.data.models.Status +import io.appwrite.starterkit.data.models.mockProjectInfo +import io.appwrite.starterkit.extensions.copy +import io.appwrite.starterkit.extensions.edgeToEdgeWithStyle +import io.appwrite.starterkit.ui.components.CollapsibleBottomSheet +import io.appwrite.starterkit.ui.components.ConnectionStatusView +import io.appwrite.starterkit.ui.components.GettingStartedCards +import io.appwrite.starterkit.ui.components.TopPlatformView +import io.appwrite.starterkit.ui.components.addCheckeredBackground +import io.appwrite.starterkit.ui.theme.AppwriteStarterKitTheme +import io.appwrite.starterkit.viewmodels.AppwriteViewModel +import kotlinx.coroutines.delay + +/** + * MainActivity serves as the entry point for the application. + * It configures the system's edge-to-edge settings, splash screen, and initializes the composable layout. + */ +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + + edgeToEdgeWithStyle() + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { AppwriteStarter() } + } +} + +/** + * AppwriteStarter is the root composable function that sets up the main UI layout. + * It manages the logs, status, and project information using the provided [AppwriteViewModel]. + */ +@Composable +fun AppwriteStarter( + viewModel: AppwriteViewModel = viewModel(), +) { + val logs by viewModel.logs.collectAsState() + val status by viewModel.status.collectAsState() + + // data doesn't change, so no `remember`. + val projectInfo = viewModel.getProjectInfo() + + AppwriteStarterKitTheme { + Scaffold(bottomBar = { + CollapsibleBottomSheet( + logs = logs, + projectInfo = projectInfo + ) + }) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .addCheckeredBackground() + .padding(innerPadding.copy(top = 16.dp, bottom = 0.dp)) + .windowInsetsPadding(WindowInsets.systemBars) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TopPlatformView( + status = status + ) + + ConnectionStatusView(status) { + viewModel.ping() + } + + GettingStartedCards() + } + } + } +} + +@Preview +@Composable +@RestrictTo(RestrictTo.Scope.TESTS) +private fun AppwriteStarterPreview() { + val status = remember { mutableStateOf<Status>(Status.Idle) } + + AppwriteStarterKitTheme { + Scaffold(bottomBar = { + CollapsibleBottomSheet( + logs = emptyList(), + projectInfo = mockProjectInfo + ) + }) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .addCheckeredBackground() + .padding(innerPadding.copy(top = 16.dp, bottom = 0.dp)) + .windowInsetsPadding(WindowInsets.systemBars) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TopPlatformView( + status = status.value + ) + + ConnectionStatusView(status.value) { + // simulate a success ping + status.value = Status.Loading + delay(1000) + status.value = Status.Success + } + + GettingStartedCards() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/constants/AppwriteConfig.kt b/app/src/main/java/io/appwrite/starterkit/constants/AppwriteConfig.kt new file mode 100644 index 0000000..313927b --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/constants/AppwriteConfig.kt @@ -0,0 +1,29 @@ +package io.appwrite.starterkit.constants + +/** + * Appwrite integration constants. + * + * This object holds values related to the Appwrite server setup, + * including version, project details, and API endpoint. + */ +object AppwriteConfig { + /** + * Appwrite Server version. + */ + const val APPWRITE_VERSION = "1.6.0" + + /** + * Appwrite project id. + */ + const val APPWRITE_PROJECT_ID = "my-project-id" + + /** + * Appwrite project name. + */ + const val APPWRITE_PROJECT_NAME = "My project" + + /** + * Appwrite server endpoint url. + */ + const val APPWRITE_PUBLIC_ENDPOINT = "https://cloud.appwrite.io/v1" +} diff --git a/app/src/main/java/io/appwrite/starterkit/data/models/Log.kt b/app/src/main/java/io/appwrite/starterkit/data/models/Log.kt new file mode 100644 index 0000000..022fc55 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/data/models/Log.kt @@ -0,0 +1,12 @@ +package io.appwrite.starterkit.data.models + +/** + * A data model for holding log entries. + */ +data class Log( + val date: String, + val status: String, + val method: String, + val path: String, + val response: String, +) \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/data/models/ProjectInfo.kt b/app/src/main/java/io/appwrite/starterkit/data/models/ProjectInfo.kt new file mode 100644 index 0000000..327206e --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/data/models/ProjectInfo.kt @@ -0,0 +1,24 @@ +package io.appwrite.starterkit.data.models + +import androidx.annotation.RestrictTo + +/** + * A data model for holding appwrite project information. + */ +data class ProjectInfo( + val endpoint: String, + val projectId: String, + val projectName: String, + val version: String, +) + +/** + * A mock `ProjectInfo` model, just for **previews**. + */ +@RestrictTo(RestrictTo.Scope.TESTS) +internal val mockProjectInfo = ProjectInfo( + endpoint = "https://mock.api/v1", + projectId = "sample-project-id", + projectName = "AppwriteStarter", + version = "1.6.0", +) \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/data/models/Status.kt b/app/src/main/java/io/appwrite/starterkit/data/models/Status.kt new file mode 100644 index 0000000..e4e72e6 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/data/models/Status.kt @@ -0,0 +1,27 @@ +package io.appwrite.starterkit.data.models + +/** + * Represents the various states of a process or operation. + * This sealed class ensures that only predefined statuses are used. + */ +sealed class Status { + /** + * Represents the idle state. + */ + data object Idle : Status() + + /** + * Represents a loading state. + */ + data object Loading : Status() + + /** + * Represents a successful operation. + */ + data object Success : Status() + + /** + * Represents an error state. + */ + data object Error : Status() +} diff --git a/app/src/main/java/io/appwrite/starterkit/data/repository/AppwriteRepository.kt b/app/src/main/java/io/appwrite/starterkit/data/repository/AppwriteRepository.kt new file mode 100644 index 0000000..cff35dd --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/data/repository/AppwriteRepository.kt @@ -0,0 +1,88 @@ +package io.appwrite.starterkit.data.repository + +import android.content.Context +import io.appwrite.Client +import io.appwrite.exceptions.AppwriteException +import io.appwrite.services.Account +import io.appwrite.services.Databases +import io.appwrite.starterkit.constants.AppwriteConfig +import io.appwrite.starterkit.data.models.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * [AppwriteRepository] is responsible for handling network interactions with the Appwrite server. + * + * It provides a helper method to ping the server. + * + * **NOTE: This repository will be removed once the Appwrite SDK includes a native `client.ping()` method.**\ + * TODO: remove this repository once sdk has `client.ping()` + */ +class AppwriteRepository private constructor(context: Context) { + + // Appwrite Client and Services + private val client = Client(context.applicationContext) + .setProject(AppwriteConfig.APPWRITE_PROJECT_ID) + .setEndpoint(AppwriteConfig.APPWRITE_PUBLIC_ENDPOINT) + + private val account: Account = Account(client) + private val databases: Databases = Databases(client) + + /** + * Pings the Appwrite server. + * Captures the response or any errors encountered during the request. + * + * @return [Log] A log object containing details of the request and response. + */ + suspend fun fetchPingLog(): Log { + val date = getCurrentDate() + + return try { + val response = withContext(Dispatchers.IO) { client.ping() } + + Log( + date = date, + status = "200", + method = "GET", + path = "/ping", + response = response + ) + } catch (exception: AppwriteException) { + Log( + date = date, + method = "GET", + path = "/ping", + status = "${exception.code}", + response = "${exception.message}" + ) + } + } + + /** + * Retrieves the current date in the format "MMM dd, HH:mm". + * + * @return [String] A formatted date. + */ + private fun getCurrentDate(): String { + val formatter = SimpleDateFormat("MMM dd, HH:mm", Locale.getDefault()) + return formatter.format(Date()) + } + + companion object { + @Volatile + private var INSTANCE: AppwriteRepository? = null + + /** + * Singleton factory method to get the instance of AppwriteRepository. + * Ensures thread safety + */ + fun getInstance(context: Context): AppwriteRepository { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: AppwriteRepository(context).also { INSTANCE = it } + } + } + } +} diff --git a/app/src/main/java/io/appwrite/starterkit/extensions/EdgeToEdge.kt b/app/src/main/java/io/appwrite/starterkit/extensions/EdgeToEdge.kt new file mode 100644 index 0000000..4c8276d --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/extensions/EdgeToEdge.kt @@ -0,0 +1,20 @@ +package io.appwrite.starterkit.extensions + +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb + +/** + * Enables edge-to-edge display with a custom status bar style . + * This sets the status bar to a light style with a semi-transparent black scrim. + */ +fun ComponentActivity.edgeToEdgeWithStyle() { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.light( + scrim = Color.Black.copy(alpha = 0.15f).toArgb(), + darkScrim = Color.Black.copy(alpha = 0.15f).toArgb() + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/extensions/PaddingValues.kt b/app/src/main/java/io/appwrite/starterkit/extensions/PaddingValues.kt new file mode 100644 index 0000000..08e2db9 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/extensions/PaddingValues.kt @@ -0,0 +1,37 @@ +package io.appwrite.starterkit.extensions + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp + +/** + * A utility function to create a copy of the current [PaddingValues] with the option to override + * specific padding values for start, top, end, or bottom. + * + * This is useful to modify only certain padding values while retaining the rest. + * + * @param start The new start padding value, or `null` to retain the existing start padding. + * @param top The new top padding value, or `null` to retain the existing top padding. + * @param end The new end padding value, or `null` to retain the existing end padding. + * @param bottom The new bottom padding value, or `null` to retain the existing bottom padding. + * + * @return [PaddingValues] A new instance with the specified padding overrides applied. + */ +@Composable +fun PaddingValues.copy( + start: Dp? = null, + top: Dp? = null, + end: Dp? = null, + bottom: Dp? = null, +): PaddingValues { + val layoutDirection = LocalLayoutDirection.current + return PaddingValues( + top = top ?: this.calculateTopPadding(), + bottom = bottom ?: this.calculateBottomPadding(), + end = end ?: this.calculateEndPadding(layoutDirection), + start = start ?: this.calculateStartPadding(layoutDirection), + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/ui/components/CheckeredBackground.kt b/app/src/main/java/io/appwrite/starterkit/ui/components/CheckeredBackground.kt new file mode 100644 index 0000000..2f86f2e --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/components/CheckeredBackground.kt @@ -0,0 +1,110 @@ +package io.appwrite.starterkit.ui.components + +import androidx.annotation.RestrictTo +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlin.math.min + +// Background color for gradients, blur, etc. +val checkeredBackgroundColor = Color(0xFFFAFAFB) + +// Max height factor for background. +const val heightConstraintFactor = 0.5f + +/** + * A custom view modifier that adds a checkered background pattern with a gradient effect + * and a radial gradient overlay. The checkered pattern consists of gray vertical and horizontal lines + * drawn over the view's background. The modifier also includes a mask and a radial gradient + * to create a layered visual effect. + */ +fun Modifier.addCheckeredBackground() = this.drawBehind { drawCheckeredBackground() } + +/** + * Draws a checkered background pattern on the canvas with vertical and horizontal grid lines. + * Also applies linear and radial gradient overlays for additional visual effects. + */ +fun DrawScope.drawCheckeredBackground() { + val lineThickness = 0.75f + val gridSize = min(size.width * 0.1f, 64.dp.toPx()) + val heightConstraint = size.height * heightConstraintFactor + + // Draw vertical lines + for (x in generateSequence(0f) { it + gridSize }.takeWhile { it <= size.width }) { + drawRect( + color = Color.Gray.copy(alpha = 0.3f), + topLeft = Offset(x, 0f), + size = Size(lineThickness, heightConstraint), + style = Fill + ) + } + + // Draw horizontal lines + for (y in generateSequence(0f) { it + gridSize }.takeWhile { it <= heightConstraint }) { + drawRect( + color = Color.Gray.copy(alpha = 0.3f), + topLeft = Offset(0f, y), + size = Size(size.width, lineThickness), + style = Fill + ) + } + + drawRadialGradientOverlay() + + drawLinearGradientOverlay() +} + +/** + * Draws a vertical gradient overlay over the canvas to enhance the checkered background's appearance. + */ +fun DrawScope.drawLinearGradientOverlay() { + val heightConstraint = size.height * heightConstraintFactor + drawRect( + brush = Brush.verticalGradient( + colors = listOf( + checkeredBackgroundColor, + checkeredBackgroundColor.copy(alpha = 0.3f), + checkeredBackgroundColor.copy(alpha = 0.5f), + checkeredBackgroundColor.copy(alpha = 0.7f), + checkeredBackgroundColor, + ), endY = heightConstraint + ), + ) +} + +/** + * Draws a radial gradient overlay over the canvas to create a smooth blend effect from the center outward. + */ +fun DrawScope.drawRadialGradientOverlay() { + drawRect( + brush = Brush.radialGradient( + colors = listOf( + checkeredBackgroundColor.copy(alpha = 0f), + checkeredBackgroundColor.copy(alpha = 0.4f), + checkeredBackgroundColor.copy(alpha = 0.2f), + Color.Transparent + ), center = center, radius = maxOf(size.width, size.height) * 2 + ), + ) +} + +@Composable +@Preview(showBackground = true) +@RestrictTo(RestrictTo.Scope.TESTS) +private fun CheckeredBackgroundPreview() { + Box( + modifier = Modifier + .fillMaxSize() + .addCheckeredBackground() + ) +} diff --git a/app/src/main/java/io/appwrite/starterkit/ui/components/CollapsibleBottomSheet.kt b/app/src/main/java/io/appwrite/starterkit/ui/components/CollapsibleBottomSheet.kt new file mode 100644 index 0000000..f68674c --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/components/CollapsibleBottomSheet.kt @@ -0,0 +1,588 @@ +package io.appwrite.starterkit.ui.components + +import androidx.annotation.RestrictTo +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.appwrite.starterkit.data.models.Log +import io.appwrite.starterkit.data.models.ProjectInfo +import io.appwrite.starterkit.data.models.mockProjectInfo + +/** + * Column widths for the custom response table. + */ +val columns = listOf( + "Date" to 150f, "Status" to 80f, + "Method" to 100f, "Path" to 125f, "Response" to 125f +) + +/** + * A view that displays a collapsible bottom sheet showing logs. It includes a header with a + * title and a count of logs, and the content of the bottom sheet can be expanded or collapsed + * based on user interaction. + * + * @param title The title displayed at the top of the bottom sheet. + * @param logs The list of logs to be displayed in the bottom sheet. + * @param projectInfo Contains project details like endpoint, project ID, project name, and version. + */ +@Composable +fun CollapsibleBottomSheet( + title: String = "Logs", + logs: List<Log> = emptyList(), + projectInfo: ProjectInfo, +) { + val isExpanded = remember { mutableStateOf(false) } + val rotateAnimation = animateFloatAsState( + label = "", + animationSpec = tween(durationMillis = 250), + targetValue = if (isExpanded.value) 180f else 0f, + ) + + BoxWithConstraints { + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color.White) + .heightIn(0.dp, maxHeight * 0.575f) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = Color(0xffEDEDF0), + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1.dp.toPx() + ) + }, + verticalArrangement = Arrangement.spacedBy(0.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + indication = ripple(bounded = true), + interactionSource = remember { MutableInteractionSource() } + ) { + isExpanded.value = !isExpanded.value + } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = title, + style = TextStyle( + fontSize = 14.sp, + color = Color(0xFF56565C), + fontWeight = FontWeight.SemiBold, + ) + ) + + Text( + text = logs.size.toString(), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Color(0xFF56565C), + textAlign = TextAlign.Center + ), + modifier = Modifier + .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) + .background( + shape = RoundedCornerShape(6.dp), + color = Color.Black.copy(alpha = 0.1f), + ) + .padding( + vertical = 2.dp, + horizontal = when { + logs.size > 99 -> 5.dp + logs.size > 9 -> 3.dp + else -> 2.dp + }, + ) + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + // Chevron Icon + Icon( + tint = Color(0xff97979B), + contentDescription = null, + imageVector = Icons.Default.KeyboardArrowDown, + modifier = Modifier.rotate(rotateAnimation.value), + ) + } + + LogsBottomSheet( + logs = logs, + projectInfo = projectInfo, + isExpanded = isExpanded.value, + ) + } + } + } +} + +/** + * A view to display project information like endpoint, project ID, name, and version. + * + * @param projectInfo Contains project details like endpoint, project ID, project name, and version. + */ +@Composable +fun ProjectSection( + projectInfo: ProjectInfo, +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + fontSize = 14.sp, + text = "Project", + color = Color(0xFF97979B), + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFFFAFAFB)) + .drawBehind { + drawLine( + color = Color(0xFFEDEDF0), + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1.dp.toPx() + ) + drawLine( + color = Color(0xFFEDEDF0), + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1.dp.toPx() + ) + } + .padding(vertical = 12.dp, horizontal = 16.dp) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 0.dp, max = 200.dp) + ) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + items = listOf( + "Endpoint" to projectInfo.endpoint, + "Project ID" to projectInfo.projectId, + "Project name" to projectInfo.projectName, + "Version" to projectInfo.version + ) + ) { (title, value) -> + ProjectRow(title = title, value = value) + } + } + } + } +} + +/** + * A reusable component to display individual project details like endpoint, project ID, etc. + * + * @param title The title of the project row. + * @param value The value corresponding to the project row. + */ +@Composable +fun ProjectRow( + title: String, + value: String, +) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top), + ) { + Text( + text = title, + fontSize = 12.sp, + lineHeight = 18.sp, + color = Color(0xFF97979B), + ) + + Text( + text = value, + maxLines = 1, + fontSize = 14.sp, + lineHeight = 21.sp, + color = Color(0xFF56565C), + overflow = TextOverflow.Ellipsis + ) + } +} + +/** + * A composable that displays a placeholder section when there are no logs available. + */ +@Composable +fun EmptyLogsSection() { + Column { + Text( + fontSize = 14.sp, + text = "Logs", + color = Color(0xFF97979B), + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFFFAFAFB)) + .drawBehind { + drawLine( + color = Color(0xFFEDEDF0), + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1.dp.toPx() + ) + drawLine( + color = Color(0xFFEDEDF0), + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1.dp.toPx() + ) + } + .padding(vertical = 12.dp, horizontal = 16.dp) + ) + + Text( + fontSize = 14.sp, + color = Color(0xFF56565C), + text = "There are no logs to show", + fontFamily = FontFamily.Monospace, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) + } +} + +/** + * A view displaying logs in a table format, including a project section and a list of logs. + * If there are no logs, a placeholder message is shown. + * + * @param logs The list of logs to be displayed in the bottom sheet. + * @param isExpanded Boolean to indicate if the bottom sheet is expanded or collapsed. + * @param projectInfo Contains project details like endpoint, project ID, project name, and version. + */ +@Composable +fun LogsBottomSheet( + logs: List<Log>, + isExpanded: Boolean, + projectInfo: ProjectInfo, +) { + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn() + expandVertically( + animationSpec = tween(durationMillis = 500) + ), + exit = fadeOut() + shrinkVertically( + animationSpec = tween(durationMillis = 500) + ) + ) { + Column( + modifier = Modifier + .padding(bottom = 16.dp) + .animateContentSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ProjectSection(projectInfo = projectInfo) + + if (logs.isEmpty()) { + EmptyLogsSection() + } else { + Column( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + ) { + LogsTableHeader() + LazyColumn( + modifier = Modifier + .animateContentSize() + .heightIn(max = 300.dp) + ) { + items(logs) { log -> + LogsTableRow(log = log) + } + } + } + } + } + } +} + +/** + * A view to display the header of the logs table, with column names as headers. + */ +@Composable +fun LogsTableHeader() { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFFFAFAFB)) + .drawBehind { + drawLine( + color = Color(0xFFEDEDF0), + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1.dp.toPx() + ) + drawLine( + color = Color(0xFFEDEDF0), + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1.dp.toPx() + ) + } + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.Start + ) { + columns.forEach { (name, width) -> + Text( + text = name, + fontSize = 14.sp, + color = Color(0xFF97979B), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .width(width.dp) + .background(Color(0xFFF9F9FA)) + ) + } + } + +} + +/** + * A component that displays a log row in the table, with dynamic content based on the column name. + * + * @param log The log entry containing details like date, status, method, path, and response. + * + */ +@Composable +fun LogsTableRow(log: Log) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color.White) + .drawBehind { + drawLine( + color = Color(0xFFEDEDF0), + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 0.5.dp.toPx() + ) + } + .padding(horizontal = 16.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + columns.forEach { (name, width) -> + when (name) { + "Date" -> Text( + text = log.date, + fontSize = 14.sp, + color = Color(0xFF56565C), + fontFamily = FontFamily.Monospace, + modifier = Modifier.width(width.dp) + ) + + "Status" -> StatusTag( + status = log.status, + modifier = Modifier.width(width.dp) + ) + + "Method" -> Text( + text = log.method, + fontSize = 14.sp, + fontFamily = FontFamily.Monospace, + color = Color(0xFF56565C), + modifier = Modifier.width(width.dp) + ) + + "Path" -> Text( + text = log.path, + fontSize = 14.sp, + fontFamily = FontFamily.Monospace, + color = Color(0xFF56565C), + modifier = Modifier.width(width.dp) + ) + + "Response" -> + Box(modifier = Modifier.width(width.dp)) { + Text( + maxLines = 1, + fontSize = 12.sp, + text = log.response, + color = Color(0xFF56565C), + fontFamily = FontFamily.Monospace, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .background( + Color.Gray.copy(alpha = 0.25f), + RoundedCornerShape(6.dp) + ) + .padding(horizontal = 5.dp, vertical = 2.dp) + ) + } + + else -> Spacer(modifier = Modifier.width(width.dp)) + } + } + } + HorizontalDivider(color = Color(0xFFEDEDF0), thickness = 1.dp) +} + +/** + * A view to display a status tag with conditional styling based on the status value. + * + * @param status The status value to display. + * @param modifier Modifier to style or adjust the layout of the status tag. + */ +@Composable +fun StatusTag( + status: String, + modifier: Modifier = Modifier, +) { + val isSuccessful = status.toIntOrNull() in 200..399 + val textColor = if (isSuccessful) Color(0xFF0A714F) else Color(0xFFB31212) + val backgroundColor = if (isSuccessful) Color(0x4010B981) else Color(0x40FF453A) + + Box(modifier = modifier) { + Text( + text = status, + style = TextStyle( + fontSize = 12.sp, + color = textColor, + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + ), + modifier = Modifier + .background( + shape = RoundedCornerShape(6.dp), + color = backgroundColor.copy(alpha = 0.1f), + ) + .padding( + vertical = 2.dp, + horizontal = 5.dp, + ) + ) + } +} + +@Composable +@Preview(showBackground = true) +@RestrictTo(RestrictTo.Scope.TESTS) +private fun CollapsibleBottomSheetPreview() { + val isEmptyState = remember { mutableStateOf(false) } + val logItems = List(10) { + listOf( + Log( + date = "Dec 10, 02:51", + status = "200", + method = "GET", + path = "/v1/ping", + response = "Success" + ), + ) + }.flatten() + + Scaffold( + bottomBar = { + CollapsibleBottomSheet( + logs = if (isEmptyState.value) listOf() else logItems, + projectInfo = mockProjectInfo + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + border = BorderStroke(1.dp, Color(0xFFFD366E)), + colors = ButtonDefaults.buttonColors().copy(containerColor = Color.Transparent), + onClick = { + isEmptyState.value = !isEmptyState.value + }) { + Text( + fontSize = 16.sp, + color = Color.Black, + text = "Logs state: ${if (isEmptyState.value) "Empty" else "Full"}", + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/ui/components/ConnectionLine.kt b/app/src/main/java/io/appwrite/starterkit/ui/components/ConnectionLine.kt new file mode 100644 index 0000000..b59b632 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/components/ConnectionLine.kt @@ -0,0 +1,182 @@ +package io.appwrite.starterkit.ui.components + +import androidx.annotation.RestrictTo +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +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.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +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.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * A view that animates a connection line with a checkmark in the middle. The left and right + * lines expand and contract based on the `show` state, with a tick appearing after a delay. + * + * @param show Controls whether the connection line animation and the tick are visible. + * + */ +@Composable +fun ConnectionLine(show: Boolean) { + val tickAlpha by animateFloatAsState( + targetValue = if (show) 1f else 0f, + animationSpec = tween( + durationMillis = if (show) 500 else 50, + easing = FastOutSlowInEasing + ), label = "TickAlpha" + ) + + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + // Left line + Sidelines(left = true, show = show) + + // Tick icon + Box( + modifier = Modifier + .padding(2.dp) + .size(30.dp) + .alpha(tickAlpha) + .clip(CircleShape) + .background(Color(0x14F02E65)) + .border(width = 1.8.dp, color = Color(0x80F02E65), shape = CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Checkmark", + tint = Color(0xFFFD366E), + modifier = Modifier.size(15.dp) + ) + } + + // Right line + Sidelines(left = false, show = show) + } +} + +/** + * A composable function that animates horizontal sidelines with a gradient effect. + * + * @param left Indicates whether the sideline is on the left side (true) or right side (false). + * @param show Controls the visibility and animation of the sideline. + */ +@Composable +fun RowScope.Sidelines( + left: Boolean, + show: Boolean, +) { + val delay = if (show) 500 else 0 + val duration = if (show) 1250 else 0 + + AnimatedVisibility( + visible = show, + enter = slideInHorizontally( + initialOffsetX = { fullWidth -> + if (left) fullWidth / 2 else -fullWidth / 2 + }, + animationSpec = tween(durationMillis = duration, delayMillis = delay) + ) + expandHorizontally( + expandFrom = Alignment.CenterHorizontally, + animationSpec = tween(durationMillis = duration, delayMillis = delay) + ) + fadeIn( + animationSpec = tween(durationMillis = duration, delayMillis = delay) + ), + exit = slideOutHorizontally( + targetOffsetX = { fullWidth -> + if (left) fullWidth / 2 else -fullWidth / 2 + }, + animationSpec = tween(durationMillis = duration) + ) + shrinkHorizontally( + shrinkTowards = Alignment.CenterHorizontally, + animationSpec = tween(durationMillis = duration) + ) + fadeOut( + animationSpec = tween(delayMillis = delay, durationMillis = duration) + ), + modifier = Modifier.weight(1f) + ) { + Box( + modifier = Modifier + .height(1.5.dp) + .background( + Brush.horizontalGradient( + colors = if (!left) { + listOf(Color(0xFFF02E65), Color(0x26FE9567)) + } else { + listOf(Color(0x26FE9567), Color(0xFFF02E65)) + } + ) + ) + ) + } +} + + +@Composable +@Preview(showBackground = true) +@RestrictTo(RestrictTo.Scope.TESTS) +private fun AnimatedConnectionLinePreview() { + var showConnection by remember { mutableStateOf(false) } + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + // Title + Button( + colors = ButtonDefaults.buttonColors().copy(containerColor = Color.Transparent), + border = BorderStroke(1.dp, Color(0xFFFD366E)), + onClick = { + showConnection = !showConnection + }) { + Text( + text = "Connection Line Animation", + color = Color.Black, fontSize = 16.sp + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + ConnectionLine(show = showConnection) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/ui/components/ConnectionStatusView.kt b/app/src/main/java/io/appwrite/starterkit/ui/components/ConnectionStatusView.kt new file mode 100644 index 0000000..1986470 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/components/ConnectionStatusView.kt @@ -0,0 +1,177 @@ +package io.appwrite.starterkit.ui.components + +import androidx.annotation.RestrictTo +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.appwrite.starterkit.data.models.Status +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * A view that displays the current connection status and allows the user to send a ping + * to verify the connection. It shows different messages based on the connection status. + * + * @param status The current status of the connection, represented by the [Status]. + * @param onSendPingClick A suspend function that is triggered when the "Send a ping" button is clicked. + */ +@Composable +fun ConnectionStatusView( + status: Status, + onSendPingClick: suspend () -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + + Column( + modifier = Modifier + .padding(16.dp) + .animateContentSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AnimatedContent( + label = "", + targetState = status, + transitionSpec = { fadeIn() togetherWith fadeOut() } + ) { statusValue -> + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (statusValue) { + Status.Loading -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.Gray, + strokeWidth = 2.dp + ) + Text( + text = "Waiting for connection...", + fontSize = 14.sp, + lineHeight = 19.6.sp, + fontWeight = FontWeight(400), + color = Color(0xFF2D2D31) + ) + } + } + + Status.Success -> { + // header + Text( + fontSize = 24.sp, + text = "Congratulations!", + color = Color(0xFF2D2D31), + fontWeight = FontWeight(400), + ) + + Text( + fontSize = 14.sp, + lineHeight = 19.6.sp, + color = Color(0xFF56565C), + text = "You connected your app successfully.", + ) + } + + Status.Error, Status.Idle -> { + // header + Text( + text = "Check connection", + fontSize = 24.sp, + lineHeight = 28.8.sp, + fontWeight = FontWeight(400), + color = Color(0xFF2D2D31) + ) + Text( + text = "Send a ping to verify the connection.", + fontSize = 14.sp, + lineHeight = 19.6.sp, + color = Color(0xFF56565C) + ) + } + } + } + } + + // Ping Button + AnimatedVisibility( + exit = fadeOut(), + enter = fadeIn(), + modifier = Modifier.padding(top = 24.dp), + visible = status != Status.Loading, + ) { + val haptic = LocalHapticFeedback.current + + Button( + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { onSendPingClick() } + }, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFD366E)), + ) { + Text( + fontSize = 14.sp, + color = Color.White, + text = "Send a ping", + fontWeight = FontWeight.Medium, + ) + } + } + } +} + +@Composable +@Preview(showBackground = true) +@RestrictTo(RestrictTo.Scope.TESTS) +private fun ConnectionStatusViewPreview() { + val status = remember { mutableStateOf<Status>(Status.Idle) } + + Box( + modifier = Modifier + .fillMaxSize() + .safeContentPadding() + ) { + ConnectionStatusView( + status = status.value, + onSendPingClick = { + status.value = Status.Loading + delay(2500) + status.value = Status.Success + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/ui/components/GettingStartedCards.kt b/app/src/main/java/io/appwrite/starterkit/ui/components/GettingStartedCards.kt new file mode 100644 index 0000000..d23ea44 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/components/GettingStartedCards.kt @@ -0,0 +1,190 @@ +package io.appwrite.starterkit.ui.components + +import androidx.annotation.RestrictTo +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * A view that contains a list of informational cards displayed vertically. + */ +@Composable +fun GettingStartedCards() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + GeneralInfoCard( + title = "Edit your app", + link = null, + subtitle = { + HighlightedText() + } + ) + + // Second Card: Head to Appwrite Cloud + GeneralInfoCard( + title = "Head to Appwrite Cloud", + link = "https://cloud.appwrite.io", + subtitle = { + Text( + fontSize = 14.sp, + color = Color(0xFF56565C), + fontWeight = FontWeight(400), + text = "Start managing your project from the Appwrite console", + ) + } + ) + + // Third Card: Explore docs + GeneralInfoCard( + title = "Explore docs", + link = "https://appwrite.io/docs", + subtitle = { + Text( + fontSize = 14.sp, + color = Color(0xFF56565C), + fontWeight = FontWeight(400), + text = "Discover the full power of Appwrite by diving into our documentation", + ) + } + ) + } +} + +/** + * A reusable card component that displays a title and subtitle with optional link functionality. + * The card becomes clickable if a link is provided and opens the destination URL when clicked. + * + * @param title The title text displayed at the top of the card. + * @param link An optional URL; if provided, the card becomes clickable. + * @param subtitle A composable lambda that defines the subtitle content of the card. + */ +@Composable +fun GeneralInfoCard( + title: String, + link: String?, + subtitle: @Composable () -> Unit, +) { + val indication = ripple(bounded = true) + val uriHandler = LocalUriHandler.current + val interactionSource = remember { MutableInteractionSource() } + + Card( + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, Color(0xFFEDEDF0)), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background( + color = Color.White, + shape = RoundedCornerShape(size = 8.dp) + ) + .clickable( + enabled = link != null, + indication = indication, + interactionSource = interactionSource, + ) { + uriHandler.openUri(link.toString()) + }, + colors = CardDefaults.cardColors(containerColor = Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = title, + style = TextStyle( + fontSize = 20.sp, + lineHeight = 26.sp, + fontWeight = FontWeight(400), + ) + ) + if (link != null) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + tint = Color(0xFFD8D8DB), + contentDescription = null, + modifier = Modifier.size(18.dp), + imageVector = Icons.AutoMirrored.Default.ArrowForward, + ) + } + } + subtitle() + } + } +} + +/** + * A composable function that displays highlighted text with a specific word or phrase styled differently. + * Useful for drawing attention to specific content in a sentence. + */ +@Composable +fun HighlightedText() { + val text = buildAnnotatedString { + append("Edit ") + withStyle( + style = SpanStyle( + background = Color(0xFFEDEDF0), + fontWeight = FontWeight(500) + ) + ) { append(" MainActivity.kt ") } + append(" to get started with building your app") + } + + Text( + text = text, + fontSize = 14.sp, + color = Color(0xFF56565C), + fontWeight = FontWeight(400), + ) +} + +@Composable +@Preview(showBackground = true) +@RestrictTo(RestrictTo.Scope.TESTS) +private fun GettingStartedCardsPreview() { + GettingStartedCards() +} diff --git a/app/src/main/java/io/appwrite/starterkit/ui/components/TopPlatformView.kt b/app/src/main/java/io/appwrite/starterkit/ui/components/TopPlatformView.kt new file mode 100644 index 0000000..8ec0c24 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/components/TopPlatformView.kt @@ -0,0 +1,174 @@ +package io.appwrite.starterkit.ui.components + +import androidx.annotation.RestrictTo +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.appwrite.starterkit.data.models.Status +import io.appwrite.starterkit.ui.icons.AndroidIcon +import io.appwrite.starterkit.ui.icons.AppwriteIcon +import kotlinx.coroutines.delay + +/** + * A composable function that displays a row containing platform icons and a connection line between them. + * The connection line indicates the status of the platform connection. + * + * @param status A [Status] indicating the current connection status. + */ +@Composable +fun TopPlatformView(status: Status) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .padding(horizontal = 40.dp) + ) { + // First Platform Icon + Box( + contentAlignment = Alignment.Center + ) { + PlatformIcon { + Icon( + tint = Color(0xff3ddc84), + modifier = Modifier + .width(45.86047.dp) + .height(45.86047.dp), + painter = rememberVectorPainter(AndroidIcon), + contentDescription = "Android Icon" + ) + } + } + + // ConnectionLine + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center + ) { + ConnectionLine(show = status == Status.Success) + } + + // Second Platform Icon + Box( + contentAlignment = Alignment.Center + ) { + PlatformIcon { + Icon( + tint = Color(0xffFD366E), + modifier = Modifier + .width(35.86047.dp) + .height(35.86047.dp), + painter = rememberVectorPainter(AppwriteIcon), + contentDescription = "Appwrite Icon" + ) + } + } + } +} + +/** + * A composable function that displays a stylized platform icon with a customizable content block. + * The icon is rendered with shadows, rounded corners, and a layered background. + * + * @param modifier Modifier for additional customizations like size and padding. + * @param content A composable lambda that defines the inner content of the icon (e.g., an image or vector asset). + */ +@Composable +fun PlatformIcon( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Box { + Box( + modifier = Modifier + .shadow( + elevation = 9.360000610351562.dp, + spotColor = Color(0x08000000), + ambientColor = Color(0x08000000) + ) + .border( + width = 1.dp, + color = Color(0x0A19191C), + shape = RoundedCornerShape(size = 24.dp) + ) + .width(100.dp) + .height(100.dp) + .background(color = Color(0xFFFAFAFD), shape = RoundedCornerShape(size = 24.dp)) + ) { + Box( + modifier = Modifier + .shadow( + elevation = 8.dp, + spotColor = Color(0x05000000), + ambientColor = Color(0x05000000) + ) + .shadow( + elevation = 12.dp, + spotColor = Color(0x05000000), + ambientColor = Color(0x05000000) + ) + .border( + width = 1.dp, + color = Color(0xFFFAFAFB), + shape = RoundedCornerShape(size = 16.dp) + ) + .width(86.04652.dp) + .height(86.04652.dp) + .align(Alignment.Center) + .background(color = Color.White, shape = RoundedCornerShape(size = 16.dp)) + ) { + // Content + Box( + modifier = Modifier.align(Alignment.Center), + content = content + ) + } + } + } +} + +@Composable +@Preview(showBackground = true) +@RestrictTo(RestrictTo.Scope.TESTS) +private fun PlatformIconPreview() { + val status = remember { mutableStateOf<Status>(Status.Idle) } + + Column( + modifier = Modifier + .fillMaxSize() + .addCheckeredBackground(), + ) { + TopPlatformView(status = status.value) + + ConnectionStatusView( + status = status.value, + onSendPingClick = { + status.value = Status.Loading + delay(500) + status.value = Status.Success + } + ) + } +} diff --git a/app/src/main/java/io/appwrite/starterkit/ui/icons/AndroidIcon.kt b/app/src/main/java/io/appwrite/starterkit/ui/icons/AndroidIcon.kt new file mode 100644 index 0000000..bae4896 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/icons/AndroidIcon.kt @@ -0,0 +1,78 @@ +package io.appwrite.starterkit.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val AndroidIcon: ImageVector + get() { + if (_Android != null) { + return _Android!! + } + _Android = ImageVector.Builder( + name = "Android", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1.0f, + stroke = null, + strokeAlpha = 1.0f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(40f, 720f) + quadToRelative(9f, -107f, 65.5f, -197f) + reflectiveQuadTo(256f, 380f) + lineToRelative(-74f, -128f) + quadToRelative(-6f, -9f, -3f, -19f) + reflectiveQuadToRelative(13f, -15f) + quadToRelative(8f, -5f, 18f, -2f) + reflectiveQuadToRelative(16f, 12f) + lineToRelative(74f, 128f) + quadToRelative(86f, -36f, 180f, -36f) + reflectiveQuadToRelative(180f, 36f) + lineToRelative(74f, -128f) + quadToRelative(6f, -9f, 16f, -12f) + reflectiveQuadToRelative(18f, 2f) + quadToRelative(10f, 5f, 13f, 15f) + reflectiveQuadToRelative(-3f, 19f) + lineToRelative(-74f, 128f) + quadToRelative(94f, 53f, 150.5f, 143f) + reflectiveQuadTo(920f, 720f) + close() + moveToRelative(240f, -110f) + quadToRelative(21f, 0f, 35.5f, -14.5f) + reflectiveQuadTo(330f, 560f) + reflectiveQuadToRelative(-14.5f, -35.5f) + reflectiveQuadTo(280f, 510f) + reflectiveQuadToRelative(-35.5f, 14.5f) + reflectiveQuadTo(230f, 560f) + reflectiveQuadToRelative(14.5f, 35.5f) + reflectiveQuadTo(280f, 610f) + moveToRelative(400f, 0f) + quadToRelative(21f, 0f, 35.5f, -14.5f) + reflectiveQuadTo(730f, 560f) + reflectiveQuadToRelative(-14.5f, -35.5f) + reflectiveQuadTo(680f, 510f) + reflectiveQuadToRelative(-35.5f, 14.5f) + reflectiveQuadTo(630f, 560f) + reflectiveQuadToRelative(14.5f, 35.5f) + reflectiveQuadTo(680f, 610f) + } + }.build() + return _Android!! + } + +private var _Android: ImageVector? = null diff --git a/app/src/main/java/io/appwrite/starterkit/ui/icons/AppwriteIcon.kt b/app/src/main/java/io/appwrite/starterkit/ui/icons/AppwriteIcon.kt new file mode 100644 index 0000000..9da4836 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/icons/AppwriteIcon.kt @@ -0,0 +1,79 @@ +package io.appwrite.starterkit.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val AppwriteIcon: ImageVector + get() { + if (_Appwrite != null) { + return _Appwrite!! + } + _Appwrite = ImageVector.Builder( + name = "Logo", + defaultWidth = 112.dp, + defaultHeight = 98.dp, + viewportWidth = 112f, + viewportHeight = 98f + ).apply { + path( + fill = SolidColor(Color(0xFFFD366E)), + fillAlpha = 1.0f, + stroke = null, + strokeAlpha = 1.0f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(111.1f, 73.4729f) + verticalLineTo(97.9638f) + horizontalLineTo(48.8706f) + curveTo(30.74060f, 97.96380f, 14.91050f, 88.1140f, 6.44110f, 73.47290f) + curveTo(5.20990f, 71.34440f, 4.13230f, 69.11130f, 3.22830f, 66.79350f) + curveTo(1.45390f, 62.25160f, 0.33840f, 57.37790f, 00f, 52.29260f) + verticalLineTo(45.6712f) + curveTo(0.07350f, 44.53790f, 0.18920f, 43.41350f, 0.34060f, 42.30250f) + curveTo(0.65010f, 40.02270f, 1.11770f, 37.79180f, 1.73220f, 35.62320f) + curveTo(7.54540f, 15.06410f, 26.4480f, 00f, 48.87060f, 00f) + curveTo(71.29320f, 00f, 90.19350f, 15.06410f, 96.00680f, 35.62320f) + horizontalLineTo(69.3985f) + curveTo(65.03020f, 28.92160f, 57.46920f, 24.4910f, 48.87060f, 24.4910f) + curveTo(40.2720f, 24.4910f, 32.7110f, 28.92160f, 28.34270f, 35.62320f) + curveTo(27.01130f, 37.66040f, 25.97820f, 39.90690f, 25.30140f, 42.30250f) + curveTo(24.70020f, 44.42660f, 24.37960f, 46.66640f, 24.37960f, 48.98190f) + curveTo(24.37960f, 56.00190f, 27.33190f, 62.32950f, 32.06530f, 66.79350f) + curveTo(36.45150f, 70.93690f, 42.36490f, 73.47290f, 48.87060f, 73.47290f) + horizontalLineTo(111.1f) + close() + } + path( + fill = SolidColor(Color(0xFFFD366E)), + fillAlpha = 1.0f, + stroke = null, + strokeAlpha = 1.0f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(111.1f, 42.3027f) + verticalLineTo(66.7937f) + horizontalLineTo(65.6759f) + curveTo(70.40940f, 62.32970f, 73.36160f, 56.00210f, 73.36160f, 48.98210f) + curveTo(73.36160f, 46.66660f, 73.0410f, 44.42680f, 72.43990f, 42.30270f) + horizontalLineTo(111.1f) + close() + } + }.build() + return _Appwrite!! + } + +private var _Appwrite: ImageVector? = null diff --git a/app/src/main/java/io/appwrite/starterkit/ui/theme/Theme.kt b/app/src/main/java/io/appwrite/starterkit/ui/theme/Theme.kt new file mode 100644 index 0000000..96585c1 --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/ui/theme/Theme.kt @@ -0,0 +1,49 @@ +package io.appwrite.starterkit.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +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 + +// Light color scheme +private val LightColorScheme = lightColorScheme( + primary = Color(0xFF6650a4), // Purple40 + secondary = Color(0xFF625b71), // PurpleGrey40 + tertiary = Color(0xFF7D5260) // Pink40 +) + +// Typography +private val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) + +/** + * A custom theme composable for the Appwrite Starter Kit application. + * + * This function sets the app's overall typography, color scheme, and material theme, + * providing consistent styling throughout the app. The `content` composable + * is wrapped within the custom [MaterialTheme]. + * + * @param content The composable content to be styled with the Appwrite Starter Kit theme. + */ +@Composable +fun AppwriteStarterKitTheme( + content: @Composable () -> Unit, +) { + MaterialTheme( + content = content, + typography = Typography, + colorScheme = LightColorScheme, + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/appwrite/starterkit/viewmodels/AppwriteViewModel.kt b/app/src/main/java/io/appwrite/starterkit/viewmodels/AppwriteViewModel.kt new file mode 100644 index 0000000..636ed3b --- /dev/null +++ b/app/src/main/java/io/appwrite/starterkit/viewmodels/AppwriteViewModel.kt @@ -0,0 +1,63 @@ +package io.appwrite.starterkit.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import io.appwrite.starterkit.constants.AppwriteConfig +import io.appwrite.starterkit.data.models.Log +import io.appwrite.starterkit.data.models.ProjectInfo +import io.appwrite.starterkit.data.models.Status +import io.appwrite.starterkit.data.repository.AppwriteRepository +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * A ViewModel class that serves as the central hub for managing and storing the state + * related to Appwrite operations, such as project information, connection status, and logs. + */ +class AppwriteViewModel(application: Application) : AndroidViewModel(application) { + + private val repository = AppwriteRepository.getInstance(application) + + private val _status = MutableStateFlow<Status>(Status.Idle) + private val _logs = MutableStateFlow<List<Log>>(emptyList()) + + val logs: StateFlow<List<Log>> = _logs.asStateFlow() + val status: StateFlow<Status> = _status.asStateFlow() + + /** + * Retrieves project information such as version, project ID, endpoint, and project name. + * + * @return [ProjectInfo] An object containing project details. + */ + fun getProjectInfo(): ProjectInfo { + return ProjectInfo( + version = AppwriteConfig.APPWRITE_VERSION, + projectId = AppwriteConfig.APPWRITE_PROJECT_ID, + endpoint = AppwriteConfig.APPWRITE_PUBLIC_ENDPOINT, + projectName = AppwriteConfig.APPWRITE_PROJECT_NAME + ) + } + + /** + * Executes a ping operation to verify connectivity and logs the result. + * + * Updates the [status] to [Status.Loading] during the operation and then updates it + * based on the success or failure of the ping. Appends the result to [logs]. + */ + suspend fun ping() { + _status.value = Status.Loading + val log = repository.fetchPingLog() + + _logs.value += log + + delay(1000) + + _status.value = if (log.status.toIntOrNull() in 200..399) { + Status.Success + } else { + Status.Error + } + } +} diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..af0f523 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..5d0c4f2 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..2700fd7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..20d38d4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..374c756 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1283042 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="Theme.AppwriteStarterKit" parent="android:Theme.Material.Light.NoActionBar"> + <item name="android:windowTranslucentStatus">true</item> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:navigationBarColor">@android:color/transparent</item> + <item name="windowSplashScreenIconBackgroundColor">@android:color/black</item> + </style> + + <style name="Theme.AppwriteStarterKit.SplashTheme" parent="Theme.SplashScreen.IconBackground"> + <item name="windowSplashScreenBackground">#FFF</item> + <item name="postSplashScreenTheme">@style/Theme.AppwriteStarterKit</item> + </style> +</resources> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..952b930 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +// 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.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..deab996 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..b5e6ee3 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,30 @@ +[versions] +agp = "8.7.3" +kotlin = "2.0.0" +coreKtx = "1.15.0" +splashScreen = "1.2.0-alpha02" +lifecycleRuntimeKtx = "2.8.7" +activityCompose = "1.10.0" +composeBom = "2025.01.01" +appwrite = "7.0.0" + +[libraries] +appwrite = { group = "io.appwrite", name = "sdk-for-android", version.ref = "appwrite" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } +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-material3 = { group = "androidx.compose.material3", name = "material3" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f0a7fbc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Dec 18 13:34:27 IST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/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/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..0c94d46 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "AppwriteStarterKit" +include(":app")