Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat Starter Kit #1

Merged
merged 4 commits into from
Feb 8, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: android starter kit.
ItzNotABug committed Jan 16, 2025
commit 8ba18e15665f7a3941cee4eac865fa63456c556a
10 changes: 10 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.iml
.gradle
.idea
build
gradle
local.properties
.externalNativeBuild

/captures
.DS_Store
68 changes: 68 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
21 changes: 21 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -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>
138 changes: 138 additions & 0 deletions app/src/main/java/io/appwrite/starterkit/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/io/appwrite/starterkit/data/models/Log.kt
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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",
)
27 changes: 27 additions & 0 deletions app/src/main/java/io/appwrite/starterkit/data/models/Status.kt
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.appwrite.starterkit.data.repository

import android.content.Context
import io.appwrite.Client
import io.appwrite.services.Account
import io.appwrite.services.Databases
import io.appwrite.starterkit.data.models.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.net.HttpURLConnection
import java.net.URL
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(APPWRITE_PROJECT_ID)
.setEndpoint(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 = withContext(Dispatchers.IO) {
val url = URL("${client.endpoint}$PING_PATH")
val connection = (url.openConnection() as HttpURLConnection).apply {
requestMethod = "GET"

readTimeout = DEFAULT_TIMEOUT
connectTimeout = DEFAULT_TIMEOUT

setRequestProperty("Content-Type", "application/json")
setRequestProperty("x-appwrite-response-format", APPWRITE_VERSION)
setRequestProperty("x-appwrite-project", APPWRITE_PROJECT_ID)
}

try {
val statusCode = connection.responseCode
val response = if (statusCode == HttpURLConnection.HTTP_OK) {
connection.inputStream.bufferedReader().use(BufferedReader::readText)
} else {
"Request failed with status code $statusCode"
}

Log(
date = getCurrentDate(),
status = statusCode.toString(),
method = "GET",
path = PING_PATH,
response = response
)
} catch (e: Exception) {
Log(
date = getCurrentDate(),
status = "Error",
method = "GET",
path = PING_PATH,
response = "Error occurred: ${e.message}"
)
} finally {
connection.disconnect()
}
}

/**
* 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

/**
* 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"

/**
* The path to use for ping.
*/
const val PING_PATH = "/ping"

/**
* Default network timeout.
*/
const val DEFAULT_TIMEOUT = 5_000

/**
* 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 }
}
}
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/io/appwrite/starterkit/extensions/EdgeToEdge.kt
Original file line number Diff line number Diff line change
@@ -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()
)
)
}
Original file line number Diff line number Diff line change
@@ -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),
)
}
Original file line number Diff line number Diff line change
@@ -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()
)
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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
}
)
}
}
78 changes: 78 additions & 0 deletions app/src/main/java/io/appwrite/starterkit/ui/icons/AndroidIcon.kt
Original file line number Diff line number Diff line change
@@ -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
79 changes: 79 additions & 0 deletions app/src/main/java/io/appwrite/starterkit/ui/icons/AppwriteIcon.kt
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions app/src/main/java/io/appwrite/starterkit/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.appwrite.starterkit.viewmodels

import android.app.Application
import androidx.lifecycle.AndroidViewModel
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 io.appwrite.starterkit.data.repository.AppwriteRepository.Companion.APPWRITE_PROJECT_ID
import io.appwrite.starterkit.data.repository.AppwriteRepository.Companion.APPWRITE_PROJECT_NAME
import io.appwrite.starterkit.data.repository.AppwriteRepository.Companion.APPWRITE_PUBLIC_ENDPOINT
import io.appwrite.starterkit.data.repository.AppwriteRepository.Companion.APPWRITE_VERSION
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 = APPWRITE_VERSION,
projectId = APPWRITE_PROJECT_ID,
endpoint = APPWRITE_PUBLIC_ENDPOINT,
projectName = 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
}
}
}
Binary file added app/src/main/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -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>
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
30 changes: 30 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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.9.3"
composeBom = "2024.12.01"
appwrite = "6.1.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" }

Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
6 changes: 6 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
185 changes: 185 additions & 0 deletions gradlew
Original file line number Diff line number Diff line change
@@ -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" "$@"
89 changes: 89 additions & 0 deletions gradlew.bat
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")