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/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..a11bb8d228 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte @@ -0,0 +1,856 @@ + + + + {#if loading} + + Loading editor... + + {:else if !selectedFile} + + + Image Editor + No files available to edit. + + + {:else} + + + + + + + + + + (activeTab = 'design')}> + Design + + (activeTab = 'code')}> + Code + + + + + 100% + 90% + 80% + 70% + 50% + + + + + + + + + + + + Focal point: {focalPointOptions.find((opt) => opt.value === focalPoint) + ?.label || 'Bottom-Left'} + + + + + + + + + + + + + + + + {width} × {height} + + + + + + 0° + + + + + + + + + + + {#each bucketFiles as f} + {f.name} + {/each} + + + + + None + + + + + + + + (activeTab = 'design')}> + Design + + (activeTab = 'code')}> + Code + + + + + 100% + 90% + 80% + 75% + + + + + + + + + + + Dimensions + + + + W + + handleWidthChange( + parseInt(e.currentTarget.value) || 0 + )} /> + + changeWidth(1)}>▲ + changeWidth(-1)}>▼ + + + + + H + + handleHeightChange( + parseInt(e.currentTarget.value) || 0 + )} /> + + changeHeight(1)}>▲ + changeHeight(-1)}>▼ + + + + + (aspectRatioLocked = !aspectRatioLocked)}> + + + 🔒 + + + + + + Crop + + None + {#each focalPointOptions as option} + {option.label} + {/each} + + + + + + + + + + + + Width + + + {#if borderWidth > 0} + + Color + + + {/if} + + + + + + + + + + Background Color + + + + + + + + + + + + Format + + {#each formatOptions as option} + {#if option.value} + {option.label} + {/if} + {/each} + + + + + + + + + {/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]/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 + + + + + 100% + 90% + 80% + 70% + 50% + + + + + + + + + {#if $file} + + + + Focal point: {focalPointOptions.find((opt) => opt.value === focalPoint) + ?.label || 'Bottom-Left'} + + + + + + + + + + + + + + + + {width} × {height} + + + + + + 0° + + + {/if} + + + + + + + + + {#if bucketFiles.length > 0} + {#each bucketFiles as f} + {f.name} + {/each} + {:else} + {$file?.name} + {/if} + + + + + None + + + + + + + + (activeTab = 'design')}> + Design + + (activeTab = 'code')}> + Code + + + + + 100% + 90% + 80% + 70% + 50% + + + + + + + + + + Dimensions + + + W + + handleWidthChange(Number(e.currentTarget.value))} /> + + handleWidthChange(width + 1)} + >▲ + handleWidthChange(width - 1)} + >▼ + + + + H + + handleHeightChange( + Number(e.currentTarget.value) + )} /> + + handleHeightChange(height + 1)} + >▲ + handleHeightChange(height - 1)} + >▼ + + + (aspectRatioLocked = !aspectRatioLocked)}> + {#if aspectRatioLocked} + + + + {:else} + + + + {/if} + + + + + + Crop + + None + {#each gravityOptions as option} + {option.label} + {/each} + + + + + + + + + + + + + + {#if borderWidth > 0} + + {/if} + + + + + + + + + + + + + + + + + + + + + + + + {#each formatOptions as option} + {option.label} + {/each} + + + + + + + + + + + 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}