- {#if $organization.billingPlan === BillingPlan.SCALE}
+ {#if $currentPlan?.backupPolicies > 1}
{#if title || subtitle}
{#if title}
@@ -195,7 +194,7 @@
{/if}
- {#if $organization.billingPlan === BillingPlan.PRO}
+ {#if $currentPlan?.backupPolicies === 1}
{@const dailyPolicy = $presetPolicies[1]}
{#if isFromBackupsTab}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte
index 07f9bd781..bc4f7f186 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte
@@ -74,6 +74,8 @@
import { isTabletViewport } from '$lib/stores/viewport';
import IndexesSuggestions from '../(suggestions)/indexes.svelte';
+ import ColumnsSuggestions from '../(suggestions)/columns.svelte';
+ import { showColumnsSuggestionsModal } from '../(suggestions)';
let editRow: EditRow;
let editRelatedRow: EditRelatedRow;
@@ -608,6 +610,8 @@
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte
new file mode 100644
index 000000000..deaaeb57b
--- /dev/null
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte
index 134a18b30..9f2313ae1 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/+page.svelte
@@ -23,6 +23,7 @@
Typography
} from '@appwrite.io/pink-svelte';
import {
+ IconBookOpen,
IconDotsHorizontal,
IconEye,
IconPlus,
@@ -37,6 +38,10 @@
import { showCreateColumnSheet } from '../store';
import { isSmallViewport } from '$lib/stores/viewport';
import { page } from '$app/state';
+ import { showIndexesSuggestions, showColumnsSuggestionsModal } from '../../(suggestions)';
+ import IconAI from '../../(suggestions)/icon/aiForButton.svelte';
+ import EmptySheetCards from '../layout/emptySheetCards.svelte';
+ import { isCloud } from '$lib/system';
import { realtime } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
@@ -66,14 +71,14 @@
const spreadsheetColumns = $derived([
{
id: 'key',
- width: getColumnWidth('key', $isSmallViewport ? 250 : 200),
- minimumWidth: $isSmallViewport ? 250 : 200,
+ width: getColumnWidth('key', 250),
+ minimumWidth: 250,
resizable: true
},
{
id: 'type',
- width: getColumnWidth('type', 120),
- minimumWidth: 120,
+ width: getColumnWidth('type', 200),
+ minimumWidth: 200,
resizable: true
},
{
@@ -296,27 +301,100 @@
{:else}
-
(showCreateIndex = true),
- disabled: !$table?.columns?.length
- }
- }} />
+
+ {#snippet subtitle()}
+ {#if isCloud}
+
+ Need a hand? Learn more in the
+
+ docs.
+
+
+ {/if}
+ {/snippet}
+
+ {#snippet actions()}
+ {#if isCloud}
+ {
+ showIndexesSuggestions.update(() => true);
+ }} />
+ {/if}
+
+ {
+ showCreateIndex = true;
+ }} />
+
+ {#if !isCloud}
+
+ {/if}
+ {/snippet}
+
{/if}
{:else}
- {
- $showCreateColumnSheet.show = true;
- }
- }
- }} />
+
+ {#snippet subtitle()}
+ {#if isCloud}
+
+ Need a hand? Learn more in the
+
+ docs.
+
+
+ {/if}
+ {/snippet}
+
+ {#snippet actions()}
+ {#if isCloud}
+ {
+ $showColumnsSuggestionsModal = true;
+ }} />
+
+ {
+ $showCreateColumnSheet.show = true;
+ }} />
+ {:else}
+ {
+ $showCreateColumnSheet.show = true;
+ }} />
+
+
+ {/if}
+ {/snippet}
+
{/if}
{#if selectedIndexes.length > 0}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte
index 88af9c257..f706ab739 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte
@@ -71,7 +71,8 @@
// spatial type selected -> reset column list to single empty column
// and the column already is not spatial type
$effect(() => {
- if (selectedType === IndexType.Spatial && !columnList.at(0).value) {
+ const firstColumn = $table.columns.find((col) => col.key === columnList.at(0)?.value);
+ if (selectedType === IndexType.Spatial && firstColumn && !isSpatialType(firstColumn)) {
columnList = [{ value: '', order: null, length: null }];
}
});
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte
index 8141cf0a7..c77dd08d8 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte
@@ -19,43 +19,57 @@
expandTabs
} from '../store';
import SpreadsheetContainer from './spreadsheet.svelte';
- import { onDestroy, onMount } from 'svelte';
+ import { onDestroy, onMount, type Snippet } from 'svelte';
import { debounce } from '$lib/helpers/debounce';
import { columnOptions } from '../columns/store';
type Mode = 'rows' | 'rows-filtered' | 'indexes';
- interface Action {
- text?: string;
- disabled?: boolean;
- onClick?: () => void;
- }
-
const {
mode,
- showActions = true,
customColumns = [],
title,
- actions
+ subtitle,
+ actions,
+ showActions
} = $props<{
mode: Mode;
- showActions?: boolean;
customColumns?: Column[];
title?: string;
- actions?: {
- primary?: Action;
- random?: Action;
- };
+ subtitle?: Snippet;
+ actions?: Snippet;
+ showActions?: boolean;
}>();
let spreadsheetContainer: HTMLElement;
let headerElement: HTMLElement | null = null;
let resizeObserver: ResizeObserver;
+ let overlayOffsetHandler: ResizeObserver;
+
+ let overlayLeftOffset = $state('0px');
+ let overlayTopOffset = $state('auto');
let dynamicOverlayHeight = $state('60.5vh');
const baseColProps = { draggable: false, resizable: false };
+ const updateOverlayLeftOffset = () => {
+ if (spreadsheetContainer) {
+ const containerRect = spreadsheetContainer.getBoundingClientRect();
+ overlayLeftOffset = `${containerRect.left}px`;
+ }
+
+ // calculate vertical top position
+ if (!headerElement || !headerElement.isConnected) {
+ headerElement = spreadsheetContainer?.querySelector('[role="rowheader"]');
+ }
+
+ if (headerElement) {
+ const headerRect = headerElement.getBoundingClientRect();
+ overlayTopOffset = `${headerRect.bottom}px`;
+ }
+ };
+
const updateOverlayHeight = () => {
if (!spreadsheetContainer) return;
@@ -82,6 +96,9 @@
if (spreadsheetContainer) {
resizeObserver = new ResizeObserver(debouncedUpdateOverlayHeight);
resizeObserver.observe(spreadsheetContainer);
+
+ overlayOffsetHandler = new ResizeObserver(updateOverlayLeftOffset);
+ overlayOffsetHandler.observe(spreadsheetContainer);
}
});
@@ -89,74 +106,141 @@
if (resizeObserver) {
resizeObserver.disconnect();
}
+
+ if (overlayOffsetHandler) {
+ overlayOffsetHandler.disconnect();
+ }
});
const getCustomColumns = (): Column[] =>
customColumns.map((col: Column) => ({
...col,
- width: 180,
hide: false,
icon: columnOptions.find((colOpt) => colOpt.type === col?.type)?.icon,
...baseColProps
}));
- const getRowColumns = (): Column[] => [
- {
- id: '$id',
- title: '$id',
- type: 'string',
- width: 180,
- icon: IconFingerPrint,
- ...baseColProps
- },
- ...getCustomColumns(),
- {
- id: '$createdAt',
- title: '$createdAt',
- type: 'datetime',
- width: 180,
- icon: IconCalendar,
- ...baseColProps
- },
- {
- id: '$updatedAt',
- title: '$updatedAt',
- type: 'datetime',
- width: 180,
- icon: IconCalendar,
- ...baseColProps
- },
- {
- id: 'actions',
- title: '',
- type: 'string',
- icon: IconPlus,
- width: customColumns.length ? 555 : 832,
- ...baseColProps
- },
- {
- id: 'empty',
- title: '',
- type: 'string',
- ...baseColProps
- }
- ];
+ const getRowColumns = (): Column[] => {
+ const minColumnWidth = 180;
+ const fixedWidths = { id: 180, actions: 40 };
+ const hasCustomColumns = customColumns.length > 0;
- const getIndexesColumns = (): Column[] =>
- [
+ const customColumnsData = getCustomColumns();
+
+ // Calculate column widths based on whether we have custom columns
+ let columnWidths = {
+ id: fixedWidths.id,
+ createdAt: fixedWidths.id,
+ updatedAt: fixedWidths.id,
+ custom: minColumnWidth,
+ actions: hasCustomColumns ? fixedWidths.actions : 1387
+ };
+
+ if (hasCustomColumns) {
+ const equalWidthColumns = [
+ ...customColumnsData,
+ { id: '$createdAt' },
+ { id: '$updatedAt' }
+ ];
+
+ const totalBaseWidth =
+ fixedWidths.id + fixedWidths.actions + equalWidthColumns.length * minColumnWidth;
+
+ const viewportWidth =
+ spreadsheetContainer?.clientWidth ||
+ (typeof window !== 'undefined' ? window.innerWidth : totalBaseWidth);
+
+ const excessSpace = Math.max(0, viewportWidth - totalBaseWidth);
+ const extraPerColumn =
+ equalWidthColumns.length > 0 ? excessSpace / equalWidthColumns.length : 0;
+ const distributedWidth = minColumnWidth + extraPerColumn;
+
+ columnWidths.createdAt = distributedWidth;
+ columnWidths.updatedAt = distributedWidth;
+ columnWidths.custom = distributedWidth;
+ }
+
+ const columns: Column[] = [
+ {
+ id: '$id',
+ title: '$id',
+ type: 'string',
+ width: columnWidths.id,
+ icon: IconFingerPrint,
+ ...baseColProps
+ }
+ ];
+
+ if (hasCustomColumns) {
+ columns.push(
+ ...customColumnsData.map((col) => ({
+ ...col,
+ width: columnWidths.custom
+ }))
+ );
+ }
+
+ columns.push(
+ {
+ id: '$createdAt',
+ title: '$createdAt',
+ type: 'datetime',
+ width: columnWidths.createdAt,
+ icon: IconCalendar,
+ ...baseColProps
+ },
+ {
+ id: '$updatedAt',
+ title: '$updatedAt',
+ type: 'datetime',
+ width: columnWidths.updatedAt,
+ icon: IconCalendar,
+ ...baseColProps
+ },
+ {
+ id: 'actions',
+ title: '',
+ type: 'string',
+ icon: IconPlus,
+ isAction: hasCustomColumns,
+ width: columnWidths.actions,
+ ...baseColProps
+ }
+ );
+
+ if (!hasCustomColumns) {
+ columns.push({
+ id: 'empty',
+ title: '',
+ type: 'string',
+ ...baseColProps
+ });
+ }
+
+ return columns;
+ };
+
+ const getIndexesColumns = (): Column[] => {
+ const columns = [
{ id: 'key', title: 'Key', icon: null, isPrimary: false },
{ id: 'type', title: 'Type', icon: null, isPrimary: false },
- { id: 'columns', title: 'Columns', icon: null, isPrimary: false },
- {
+ { id: 'columns', title: 'Columns', icon: null, isPrimary: false }
+ ] as Column[];
+
+ if (!$isSmallViewport) {
+ columns.push({
id: 'empty',
title: '',
width: 40,
isAction: true,
isPrimary: false
- }
- ] as Column[];
+ } as Column);
+ }
- const spreadsheetColumns = $derived(mode === 'rows' ? getRowColumns() : getIndexesColumns());
+ return columns;
+ };
+
+ const spreadsheetColumns = $derived(mode === 'indexes' ? getIndexesColumns() : getRowColumns());
const emptyCells = $derived(
($isSmallViewport ? 14 : $isTabletViewport ? 17 : 24) + (!$expandTabs ? 2 : 0)
@@ -164,9 +248,11 @@
+ bind:this={spreadsheetContainer}
+ class:custom-columns={customColumns.length > 0}
+ class:no-custom-columns={customColumns.length <= 0}
+ class="databases-spreadsheet spreadsheet-container-outer">
{#each spreadsheetColumns as column (column.id)}
- {@const columnActionsById = column.id === 'actions'}
-
- {
- if (columnActionsById && mode === 'rows') {
- $showCreateColumnSheet.show = true;
- $showCreateColumnSheet.title = 'Create column';
- $showCreateColumnSheet.columns = $tableColumns;
- $showCreateColumnSheet.columnsOrder = $columnsOrder;
- }
- }}>
+ {#if column.isAction}
+
+ {
+ if (mode === 'rows') {
+ $showCreateColumnSheet.show = true;
+ $showCreateColumnSheet.title = 'Create column';
+ $showCreateColumnSheet.columns = $tableColumns;
+ $showCreateColumnSheet.columnsOrder = $columnsOrder;
+ }
+ }}>
+
+
+
+ {:else}
{/if}
-
+ {/if}
{/each}
@@ -236,48 +325,36 @@
{#if !$spreadsheetLoading}
0}
data-collapsed-tabs={!$expandTabs}
+ style:--overlay-left={overlayLeftOffset}
+ style:--overlay-top={overlayTopOffset}
style:--dynamic-overlay-height={dynamicOverlayHeight}>
-
- {title ?? `You have no ${mode} yet`}
+
+
+ {title ?? `You have no ${mode} yet`}
- {#if showActions}
-
- {#if mode !== 'rows-filtered'}
-
-
- {actions?.primary?.text ?? `Create ${mode}`}
-
+ {@render subtitle?.()}
+
- {#if mode === 'rows'}
-
- {actions?.random?.text ?? `Generate sample data`}
-
+ {#if showActions && actions}
+ {@const inline = mode === 'rows-filtered'}
+
+
+ {#if inline}
+ {@render actions?.()}
+ {:else}
+
+ {@render actions?.()}
+
{/if}
- {:else}
-
- {actions?.primary?.text}
-
- {/if}
-
+
+
{/if}
@@ -291,13 +368,59 @@
position: fixed;
overflow: hidden;
+ & :global(.spreadsheet-container) {
+ overflow-x: auto;
+ overflow-y: auto;
+ }
+
+ & :global([data-select='true']) {
+ opacity: 0.85;
+ pointer-events: none;
+ }
+
+ &.custom-columns {
+ width: unset;
+ }
+
+ &.no-custom-columns {
+ @media (max-width: 768px) {
+ & :global(.spreadsheet-wrapper) {
+ opacity: 0;
+ }
+
+ & > .spreadsheet-fade-bottom {
+ top: var(--top-actions-spacing) !important;
+ background: var(--bgcolor-neutral-primary) !important;
+ }
+ }
+ }
+
+ &:not(.custom-columns) :global(.spreadsheet-container) {
+ overflow-x: hidden;
+ overflow-y: hidden;
+ }
+
+ /* alternative selector for header selection */
+ & :global(.sticky-header [data-select='true']) {
+ opacity: 1;
+ pointer-events: none;
+ }
+
&[data-mode='rows'] {
+ --top-actions-spacing: 50%;
+
& :global([role='rowheader'] :nth-last-child(2) [role='presentation']) {
display: none;
}
}
&[data-mode='indexes'] {
+ --top-actions-spacing: 40%;
+
+ & :global([role='cell']:last-child [role='presentation']) {
+ display: none;
+ }
+
& :global([role='rowheader'] [role='cell']:nth-last-child(1)) {
pointer-events: none;
@@ -306,22 +429,14 @@
}
}
}
-
- & :global(.spreadsheet-container) {
- overflow-x: hidden;
- overflow-y: hidden;
- }
-
- & :global([data-select='true']) {
- opacity: 0.85;
- pointer-events: none;
- }
}
.spreadsheet-fade-bottom {
+ right: 0;
bottom: 0;
- width: 100%;
position: fixed;
+ top: var(--overlay-top, auto);
+ left: var(--overlay-left, 0px);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
@@ -330,17 +445,21 @@
);
z-index: 20;
display: flex;
+ align-items: center;
justify-content: center;
transition: none !important;
- height: var(--dynamic-overlay-height, 70.5vh);
-
- @media (max-width: 1024px) {
- height: var(--dynamic-overlay-height, 63.35vh);
+ &.custom-columns {
+ pointer-events: none;
}
+ }
- @media (min-width: 1024px) {
- height: var(--dynamic-overlay-height, 70.35vh);
+ .controlled-width {
+ width: 100%;
+
+ @media (min-width: 1440px) {
+ width: 538px;
+ max-width: 538px;
}
}
@@ -354,36 +473,19 @@
}
.empty-actions {
- left: 50%;
- bottom: 35%;
- position: fixed;
-
- @media (max-width: 768px) and (max-height: 768px) {
- left: unset;
- bottom: 12.5% !important;
- }
-
- @media (max-width: 768px) and (max-height: 1024px) {
- left: unset;
- bottom: 15% !important;
- }
-
- @media (max-width: 1024px) and (max-height: 1024px) {
- left: unset;
- bottom: 15%;
- }
+ margin-bottom: 10%;
+ pointer-events: auto;
@media (max-width: 1024px) {
- left: unset;
- bottom: 30%;
+ // experiment
+ margin-bottom: 15%;
}
+ }
- @media (min-width: 1280px) {
- bottom: 37.5%;
- }
-
- @media (min-width: 1440px) {
- bottom: 40%;
+ @media (max-width: 768px) {
+ // global but controlled properly!
+ :global(main:has(.no-custom-columns) .console-container) {
+ opacity: 0;
}
}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheetCards.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheetCards.svelte
new file mode 100644
index 000000000..f38edeb62
--- /dev/null
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheetCards.svelte
@@ -0,0 +1,48 @@
+
+
+
onClick?.()}>
+
+ {#if icon}
+
+ {/if}
+
+
+
+ {title}
+
+ {#if subtitle}
+
+ {subtitle}
+
+ {/if}
+
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
index 5e3428bd4..ff67ac227 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
@@ -898,6 +898,7 @@
id={row?.$id}
virtualItem={item}
select={rowSelection}
+ hoverEffect
showSelectOnHover
valueWithoutHover={row.$sequence}>
{#each $tableColumns as { id: columnId, isEditable } (columnId)}
@@ -1159,7 +1160,8 @@
gap="xs"
direction="row"
alignItems="center"
- alignContent="center">
+ alignContent="center"
+ class="footer-input-select-wrapper">
Page
const parsedQueries = queryParamToMap(query || '[]');
queries.set(parsedQueries);
+ let activeDeployment: Models.Deployment | null = null;
+ if (data.function.deploymentId) {
+ try {
+ activeDeployment = await sdk
+ .forProject(params.region, params.project)
+ .functions.getDeployment({
+ functionId: params.function,
+ deploymentId: data.function.deploymentId
+ });
+ } catch (error) {
+ // active deployment with the requested ID could not be found
+ activeDeployment = null;
+ }
+ }
+
return {
offset,
limit,
query,
installations: data.installations,
- activeDeployment: data.function.deploymentId
- ? await sdk.forProject(params.region, params.project).functions.getDeployment({
- functionId: params.function,
- deploymentId: data.function.deploymentId
- })
- : null,
+ activeDeployment,
deploymentList: await sdk
.forProject(params.region, params.project)
.functions.listDeployments({
diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte
index 84e6a7809..4c9bb8150 100644
--- a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte
+++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte
@@ -24,8 +24,8 @@
let isSubmitting = writable(false);
let scopes: string[] = [];
- let name = '',
- expire = '';
+ let name = '';
+ let expire: string | null = null;
async function create() {
try {
diff --git a/src/routes/(console)/project-[region]-[project]/overview/components/CursorIconLarge.svelte b/src/routes/(console)/project-[region]-[project]/overview/components/CursorIconLarge.svelte
new file mode 100644
index 000000000..80ef26af0
--- /dev/null
+++ b/src/routes/(console)/project-[region]-[project]/overview/components/CursorIconLarge.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/createAndroid.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/createAndroid.svelte
index 9ad7963e1..3abd55aa9 100644
--- a/src/routes/(console)/project-[region]-[project]/overview/platforms/createAndroid.svelte
+++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/createAndroid.svelte
@@ -17,7 +17,7 @@
import { Card } from '$lib/components';
import { page } from '$app/state';
import { onMount } from 'svelte';
- import { realtime, sdk } from '$lib/stores/sdk';
+ import { getApiEndpoint, realtime, sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { fade } from 'svelte/transition';
@@ -26,6 +26,7 @@
import { PlatformType } from '@appwrite.io/console';
import { project } from '../../store';
import { getCorrectTitle, type PlatformProps } from './store';
+ import LlmBanner from './llmBanner.svelte';
let { isConnectPlatform = false }: PlatformProps = $props();
@@ -35,6 +36,60 @@
let isPlatformCreated = $state(isConnectPlatform);
const projectId = page.params.project;
+ const VERSIONS_ENDPOINT = (() => {
+ const endpoint = getApiEndpoint(page.params.region);
+ const url = new URL('/versions', endpoint);
+ return url.toString();
+ })();
+ let androidSdkVersion = $state('11.3.0');
+
+ function buildAndroidInstructions(version: string) {
+ return `
+Confirm you're working inside the correct Android project before editing anything:
+- Navigate into the directory that contains the real Android app module (look for gradlew, settings.gradle, and the app-level build.gradle(.kts)).
+- If Cursor opens in a parent folder (like your home directory) or you see multiple Android projects, ask which one to modify before making changes.
+- Update the app-level build.gradle.kts by default, but be ready to edit a Groovy build.gradle if the project hasn't migrated to Kotlin DSL yet.
+
+Prefer Version Catalogs when adding the Appwrite SDK:
+1. If ./gradle/libs.versions.toml exists, add or reuse an Appwrite entry:
+\`\`\`toml
+[libraries]
+appwrite = { module = "io.appwrite:sdk-for-android", version = "${version}" }
+\`\`\`
+2. Reference it inside the module's dependencies block:
+\`\`\`kotlin
+dependencies {
+ implementation(libs.appwrite)
+}
+\`\`\`
+Only when the project lacks ./gradle/libs.versions.toml should you hardcode the dependency:
+\`\`\`kotlin
+implementation("io.appwrite:sdk-for-android:${version}")
+\`\`\`
+Legacy Groovy scripts should use:
+\`\`\`groovy
+implementation "io.appwrite:sdk-for-android:${version}"
+\`\`\`
+
+Before introducing any new files, search the project (app/src, libs/, shared modules, etc.) for existing Appwrite client helpers (look for \`Client(\`, \`AppwriteClient\`, or \`.setEndpoint\`). If a client already exists, update its configuration instead of creating a duplicate.
+
+Ensure the Appwrite client is initialized with the application context and current project info:
+\`\`\`kotlin
+val client = Client(applicationContext)
+ .setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}")
+ .setProject("${projectId}")
+
+val account = Account(client)
+\`\`\`
+
+From the app's entry point (e.g., Application class or the first launched Activity), automatically invoke a helper that pings Appwrite so the user can verify connectivity and will be reflected on the Appwrite console:
+\`\`\`kotlin
+client.ping()
+\`\`\`
+`;
+ }
+
+ const alreadyExistsInstructions = $derived(buildAndroidInstructions(androidSdkVersion));
const gitCloneCode =
'\ngit clone https://github.com/appwrite/starter-for-android\ncd starter-for-android\n';
@@ -43,6 +98,22 @@
const val APPWRITE_PROJECT_NAME = "${$project.name}"
const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}"`;
+ async function fetchAndroidSdkVersion() {
+ try {
+ const response = await fetch(VERSIONS_ENDPOINT);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch versions: ${response.status}`);
+ }
+ const data = await response.json();
+ const latestVersion = data?.['client-android'];
+ if (typeof latestVersion === 'string' && latestVersion.trim()) {
+ androidSdkVersion = latestVersion.trim();
+ }
+ } catch (error) {
+ console.error('Unable to fetch latest Android SDK version', error);
+ }
+ }
+
async function createAndroidPlatform() {
try {
isCreatingPlatform = true;
@@ -83,6 +154,7 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
}
onMount(() => {
+ fetchAndroidSdkVersion();
const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => {
if (response.events.includes(`projects.${projectId}.ping`)) {
connectionSuccessful = true;
@@ -171,6 +243,12 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
{#if isPlatformCreated}