diff --git a/package.json b/package.json index a2e1778fe3..cde91d1c53 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,11 @@ "@ai-sdk/svelte": "^1.1.24", "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779", "@faker-js/faker": "^9.9.0", + "@neodrag/svelte": "^2.3.3", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", "@stripe/stripe-js": "^3.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a968c01e9..2d3be5ae05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,17 +18,20 @@ importers: specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 + '@neodrag/svelte': + specifier: ^2.3.3 + version: 2.3.3(svelte@5.25.3) '@popperjs/core': specifier: ^2.11.8 version: 2.11.8 @@ -281,8 +284,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -296,8 +299,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -690,6 +693,11 @@ packages: peerDependencies: svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.118 + '@neodrag/svelte@2.3.3': + resolution: {integrity: sha512-avXzhrilsBsnMFljhVAQ7h+6hbSIrvRCJ61GCiGbGISkC1QOhjDCNvPZo2+7KVwiYrnUBx4NRH0kTIqrcxv9Lg==} + peerDependencies: + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3829,7 +3837,7 @@ snapshots: dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3842,7 +3850,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 @@ -4209,6 +4217,10 @@ snapshots: nanoid: 5.1.5 svelte: 5.25.3 + '@neodrag/svelte@2.3.3(svelte@5.25.3)': + dependencies: + svelte: 5.25.3 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 diff --git a/src/lib/helpers/files.ts b/src/lib/helpers/files.ts index b72e289f66..79a71152a3 100644 --- a/src/lib/helpers/files.ts +++ b/src/lib/helpers/files.ts @@ -97,6 +97,32 @@ export enum InvalidFileType { EXTENSION = 'invalid_extension' } +/** + * Check if a file is an image based on its MIME type + */ +export function isImageFile(mimeType: string | null | undefined): boolean { + if (!mimeType) return false; + return mimeType.startsWith('image/'); +} + +/** + * Check if a file is larger than the specified size threshold (in bytes) + */ +export function isLargeFile(fileSize: number, thresholdBytes: number = 1024 * 1024): boolean { + return fileSize > thresholdBytes; +} + +/** + * Check if a file is a large image + */ +export function isLargeImage( + mimeType: string | null | undefined, + fileSize: number, + thresholdBytes: number = 1024 * 1024 +): boolean { + return isImageFile(mimeType) && isLargeFile(fileSize, thresholdBytes); +} + export const defaultIgnore = ` ### Node ### # Logs diff --git a/src/lib/helpers/imageTransformations.ts b/src/lib/helpers/imageTransformations.ts new file mode 100644 index 0000000000..b42d969b8c --- /dev/null +++ b/src/lib/helpers/imageTransformations.ts @@ -0,0 +1,123 @@ +import { ImageFormat, type Models } from '@appwrite.io/console'; + +export type TransformationState = { + width?: number; + height?: number; + gravity?: string; // focal point: 'top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right' + borderWidth?: number; + borderColor?: string; // hex without # + borderOpacity?: number; // 0-100 + borderRadius?: number; + background?: string; // hex without # + quality?: number; // 1-100 + output?: ImageFormat; + rotation?: number; // 0-360 +}; + +export function generateTransformationParams( + state: TransformationState +): Record { + const params: Record = {}; + + if (state.width) params.width = state.width; + if (state.height) params.height = state.height; + if (state.gravity && state.gravity !== 'center') { + params.gravity = state.gravity; + } + if (state.borderWidth && state.borderWidth > 0) { + params.borderWidth = state.borderWidth; + if (state.borderColor) { + params.borderColor = state.borderColor.replace('#', ''); + } + } + if (state.borderRadius && state.borderRadius > 0) { + params.borderRadius = state.borderRadius; + } + if (state.background) { + params.background = state.background.replace('#', ''); + } + if (state.quality && state.quality < 100) { + params.quality = state.quality; + } + if (state.output) { + params.output = state.output; + } + if (state.rotation && state.rotation !== 0) { + params.rotation = state.rotation; + } + + return params; +} + +export function generateSDKCode( + state: TransformationState, + bucketId: string, + fileId: string, + sdk: 'js' | 'python' | 'flutter' | 'swift' | 'kotlin' +): string { + const params = generateTransformationParams(state); + const paramStrings: string[] = []; + + // Build parameter object/string + Object.entries(params).forEach(([key, value]) => { + if (sdk === 'js' || sdk === 'python') { + paramStrings.push(` ${key}: ${typeof value === 'string' ? `'${value}'` : value}`); + } else if (sdk === 'flutter' || sdk === 'swift' || sdk === 'kotlin') { + paramStrings.push(` ${key}: ${typeof value === 'string' ? `"${value}"` : value}`); + } + }); + + const paramsBlock = paramStrings.length > 0 ? `,\n${paramStrings.join(',\n')}` : ''; + + switch (sdk) { + case 'js': + return `storage.getFilePreview({ + bucketId: '${bucketId}', + fileId: '${fileId}'${paramsBlock} +});`; + + case 'python': + return `storage.get_file_preview( + bucket_id='${bucketId}', + file_id='${fileId}'${paramsBlock.replace(/(\w+):/g, '$1=')} +);`; + + case 'flutter': + return `Storage.getFilePreview( + bucketId: '${bucketId}', + fileId: '${fileId}'${paramsBlock} +);`; + + case 'swift': + return `storage.getFilePreview( + bucketId: "${bucketId}", + fileId: "${fileId}"${paramsBlock} +)`; + + case 'kotlin': + return `storage.getFilePreview( + bucketId = "${bucketId}", + fileId = "${fileId}"${paramsBlock.replace(/(\w+):/g, '$1 =')} +)`; + + default: + return ''; + } +} + +export function getFormatLabel(format: ImageFormat): string { + switch (format) { + case ImageFormat.Jpg: + return 'JPG'; + case ImageFormat.Png: + return 'PNG'; + case ImageFormat.Gif: + return 'GIF'; + case ImageFormat.Webp: + return 'WEBP'; + case ImageFormat.Avif: + return 'AVIF'; + default: + return 'JPG'; + } +} diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte new file mode 100644 index 0000000000..a983c4a78b --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte @@ -0,0 +1,795 @@ + + + + {#if loading} + + Loading editor... + + {:else if bucketFiles.length === 0} + +
+ Image Editor + No images found in this bucket. +
+
+ {:else if !selectedFile} + + + Select an image to edit + + + {:else} + + + +
+ +
+ + +
+ +
+ +
+ + Focal point: {getFocalPointLabel()} + +
+ + + + + + +
+ {#each canvasObjects as obj (obj.id)} + {#if obj.type === 'image'} + + {:else if obj.type === 'shape'} + + {/if} + {/each} +
+ + + {#if focalPointOverlay || transformationState.gravity} + {#each canvasObjects as obj (obj.id)} + {#if obj.type === 'image' && obj.id === 'main-image'} + {@const pos = getFocalPointPosition()} +
+
+
+
+ {/if} + {/each} + {/if} + + + {#each canvasObjects as obj (obj.id)} + {#if obj.type === 'image' && obj.id === 'main-image'} +
+ + {Math.round(obj.width)} × {Math.round(obj.height)} + +
+ {/if} + {/each} +
+ +
+ + +
+
+ +
+ +
+ + +
+ {(transformationState.rotation || 0).toFixed(1)}° +
+
+
+ + +
+ handlePresetSelected(e.detail)} /> + + {#if activeTab === 'code' && selectedFile} +
+ +
+ {/if} +
+
+
+ {/if} +
+ + diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.ts b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.ts new file mode 100644 index 0000000000..7f9a7283d5 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.ts @@ -0,0 +1,3 @@ +export const load = async () => { + return {}; +}; diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/codePanel.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/codePanel.svelte new file mode 100644 index 0000000000..f47e08ac36 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/codePanel.svelte @@ -0,0 +1,72 @@ + + + + + Code + + + +
+ +
+
+ + diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/imageGrid.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/imageGrid.svelte new file mode 100644 index 0000000000..a88f1f47b6 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/imageGrid.svelte @@ -0,0 +1,148 @@ + + +{#if imageFiles.length === 0} +
+

No images found in this bucket

+
+{:else} + + {#each imageFiles as file (file.$id)} + {@const previewUrl = getPreview( + file.$id, + appliedPresets[file.$id] ? transformationState : undefined + )} + {@const isSelected = selectedFile?.$id === file.$id} +
selectFile(file)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + selectFile(file); + } + }}> +
+ + {#if appliedPresets[file.$id]} + + {/if} +
+
{file.name}
+
+ {/each} +
+{/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/presetManager.ts b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/presetManager.ts new file mode 100644 index 0000000000..516f738bb0 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/presetManager.ts @@ -0,0 +1,56 @@ +import type { TransformationState } from '$lib/helpers/imageTransformations'; + +export type Preset = { + id: string; + name: string; + transformations: TransformationState; + createdAt: number; +}; + +const STORAGE_PREFIX = 'image-presets-'; + +export function getPresets(bucketId: string): Preset[] { + if (typeof window === 'undefined') return []; + try { + const stored = localStorage.getItem(`${STORAGE_PREFIX}${bucketId}`); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } +} + +export function savePreset(bucketId: string, preset: Preset): void { + if (typeof window === 'undefined') return; + try { + const presets = getPresets(bucketId); + const existingIndex = presets.findIndex((p) => p.id === preset.id); + if (existingIndex >= 0) { + presets[existingIndex] = preset; + } else { + presets.push(preset); + } + localStorage.setItem(`${STORAGE_PREFIX}${bucketId}`, JSON.stringify(presets)); + } catch (error) { + console.error('Failed to save preset:', error); + } +} + +export function deletePreset(bucketId: string, presetId: string): void { + if (typeof window === 'undefined') return; + try { + const presets = getPresets(bucketId); + const filtered = presets.filter((p) => p.id !== presetId); + localStorage.setItem(`${STORAGE_PREFIX}${bucketId}`, JSON.stringify(filtered)); + } catch (error) { + console.error('Failed to delete preset:', error); + } +} + +export function createPreset(name: string, transformations: TransformationState): Preset { + return { + id: `preset-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + name, + transformations: { ...transformations }, + createdAt: Date.now() + }; +} diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/transformationPanel.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/transformationPanel.svelte new file mode 100644 index 0000000000..e4f5b52df2 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/components/transformationPanel.svelte @@ -0,0 +1,707 @@ + + +
+ +
+
+ ({ + value: file.$id, + label: file.name + }))} + on:change={handleFileSwitch} /> +
+
+ ({ + value: preset.id, + label: preset.name + })) + ]} + on:change={handlePresetSwitch} /> +
+
+ + +
+
+ + +
+
+ +
+
+ + {#if activeTab === 'design'} + +
+ +
+
+ Dimensions +
+ +
+ W + +
+ +
+ H + +
+ + +
+
+ +
+ Crop + { + const target = e.target as HTMLInputElement; + transformationState.crop = target.value; + }} /> +
+
+
+
+ + +
+ +
+
+ Color +
+ { + transformationState.borderColor = ( + e.target as HTMLInputElement + ).value.replace('#', ''); + }} /> + { + const value = (e.target as HTMLInputElement).value.replace( + '#', + '' + ); + if (/^[0-9A-Fa-f]{0,6}$/.test(value)) { + transformationState.borderColor = value; + } + }} /> +
+ + +
+
+
+
+ Width +
+ { + transformationState.borderWidth = + parseInt(e.currentTarget.value) || 0; + }} /> +
+ + +
+
+
+
+ Border radius +
+ + { + transformationState.borderRadius = + parseInt(e.currentTarget.value) || 0; + }} /> + +
+
+
+
+
+ + +
+ +
+
+ Background Color + { + transformationState.background = ( + e.target as HTMLInputElement + ).value.replace('#', ''); + }} /> +
+
+
+
+ + +
+ +
+ +
+
+
+ {/if} +
+ + diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/+page.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/+page.svelte index a940dc8f6f..fa28b68bd6 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/+page.svelte @@ -1,4 +1,5 @@ + + + + +
+ + {previewUrl.split('://')[0]}:// + +
+ + +
+ + (activeTab = 'design')}> + Design + + (activeTab = 'code')}> + Code + + +
+ +
+
+ + +
+ +
+ {#if $file} + +
+ + Focal point: {focalPointOptions.find((opt) => opt.value === focalPoint) + ?.label || 'Bottom-Left'} + +
+ +
+
+ + +
+
+
+
+
+
+
+ + {width} × {height} + + + +
+ + +
+
+ {/if} +
+ + +
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ +
+
+ Dimensions +
+
+ W + + handleWidthChange(Number(e.currentTarget.value))} /> +
+ + +
+
+
+ H + + handleHeightChange( + Number(e.currentTarget.value) + )} /> +
+ + +
+
+ +
+
+ +
+ Crop + +
+
+
+
+ + +
+ +
+
+
+ + {#if borderWidth > 0} + + {/if} +
+
+
+
+
+ + +
+ +
+
+ +
+
+
+
+ + +
+ +
+
+ +
+
+
+
+
+
+
+
+ + diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/editor/+page.ts b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/editor/+page.ts new file mode 100644 index 0000000000..375d099ab8 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/editor/+page.ts @@ -0,0 +1,9 @@ +import { Dependencies } from '$lib/constants'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ depends }) => { + depends(Dependencies.FILE); + depends(Dependencies.BUCKET); + + return {}; +}; diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/header.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/header.svelte index 5bdf521833..23c46a9914 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/header.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/header.svelte @@ -17,6 +17,12 @@ event: 'files', hasChildren: true }, + { + href: `${path}/editor`, + title: 'Editor', + event: 'editor', + hasChildren: true + }, { href: `${path}/usage`, title: 'Usage', diff --git a/src/routes/(console)/project-[region]-[project]/storage/grid.svelte b/src/routes/(console)/project-[region]-[project]/storage/grid.svelte index dbdc549a3f..5b20e9721d 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/grid.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/grid.svelte @@ -3,14 +3,30 @@ import { page } from '$app/state'; import { CardContainer, GridItem1, Id } from '$lib/components'; import { canWriteBuckets } from '$lib/stores/roles'; - import { Badge, Tooltip } from '@appwrite.io/pink-svelte'; + import { Badge, Tooltip, Layout, Popover, Typography } from '@appwrite.io/pink-svelte'; import type { PageData } from './$types'; + import { goto } from '$app/navigation'; + import Link from '$lib/elements/link.svelte'; export let data: PageData; export let showCreate = false; const region = page.params.region; const project = page.params.project; + + let isMouseOverTooltip = false; + function hidePopover(hideTooltip: () => void, timeout = true) { + if (!timeout) { + isMouseOverTooltip = false; + return hideTooltip(); + } + + setTimeout(() => { + if (!isMouseOverTooltip) { + hideTooltip(); + } + }, 150); + } (showCreate = true)}> {#each data.buckets.buckets as bucket} - - {bucket.name} + {@const showOptimizable = bucket.transformations} + {@const bucketId = bucket.$id} + + + + {bucket.name} + {#if showOptimizable} + + { + setTimeout(show, 150); + }} + on:mouseleave={() => hidePopover(hide)}> + + +
(isMouseOverTooltip = true)} + on:mouseleave={() => hidePopover(hide, false)}> + {#if showing} + + This bucket contains large images. Use{' '} + { + e.preventDefault(); + hide(); + goto( + `${base}/project-${region}-${project}/storage/bucket-${bucketId}/settings#transformations` + ); + }}>image transformations{' '}to serve optimized versions in your app. + + {/if} +
+
+ {/if} +
+
{#if !bucket.enabled} -
- -
+ {/if}
diff --git a/src/routes/(console)/project-[region]-[project]/storage/store.ts b/src/routes/(console)/project-[region]-[project]/storage/store.ts index 92de92584f..a5a0a35120 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/store.ts +++ b/src/routes/(console)/project-[region]-[project]/storage/store.ts @@ -3,7 +3,8 @@ import { writable } from 'svelte/store'; export const columns = writable([ { id: '$id', title: 'Bucket ID', type: 'string', width: 200 }, - { id: 'name', title: 'Name', type: 'string', width: { min: 120 } }, + { id: 'name', title: 'Name', type: 'string', width: { min: 200 } }, + { id: 'storageUsage', title: 'Storage usage', type: 'integer', width: 220 }, { id: '$createdAt', title: 'Created', type: 'datetime', width: { min: 120 } }, { id: '$updatedAt', title: 'Updated', type: 'datetime', width: { min: 120 } } ]); diff --git a/src/routes/(console)/project-[region]-[project]/storage/table.svelte b/src/routes/(console)/project-[region]-[project]/storage/table.svelte index fd7ef03960..a9595331cb 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/table.svelte @@ -5,9 +5,29 @@ import DualTimeView from '$lib/components/dualTimeView.svelte'; import type { PageData } from './$types'; import { columns } from './store'; - import { Table } from '@appwrite.io/pink-svelte'; + import { Table, Badge, Layout, Popover, Typography } from '@appwrite.io/pink-svelte'; + import { goto } from '$app/navigation'; + import Link from '$lib/elements/link.svelte'; + import { calculateSize } from '$lib/helpers/sizeConvertion'; export let data: PageData; + + const region = page.params.region; + const project = page.params.project; + + let isMouseOverTooltip = false; + function hidePopover(hideTooltip: () => void, timeout = true) { + if (!timeout) { + isMouseOverTooltip = false; + return hideTooltip(); + } + + setTimeout(() => { + if (!isMouseOverTooltip) { + hideTooltip(); + } + }, 150); + } @@ -30,6 +50,48 @@ {/key} {:else if column.id === 'name'} {bucket.name} + {:else if column.id === 'storageUsage'} + + {calculateSize(0)} + {#if bucket.transformations} + + { + setTimeout(show, 150); + }} + on:mouseleave={() => hidePopover(hide)} + on:click|stopPropagation> + + +
(isMouseOverTooltip = true)} + on:mouseleave={() => hidePopover(hide, false)}> + {#if showing} + + This bucket contains large images. Use{' '} + { + e.preventDefault(); + hide(); + goto( + `${base}/project-${region}-${project}/storage/bucket-${bucket.$id}/settings#transformations` + ); + }}>image transformations{' '}to serve optimized versions in your app. + + {/if} +
+
+ {/if} +
{:else if column.type === 'datetime'} {:else}