✨ Improved file uploader; uploads from the library can be selected
This commit is contained in:
@@ -222,6 +222,23 @@ async def list_images(
|
|||||||
return return_items
|
return return_items
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/list/last")
|
||||||
|
async def get_latest_images(count: int = 50, user: User = Depends(get_current_user)) -> list[PrivateStorageItem]:
|
||||||
|
if count > 50:
|
||||||
|
count = 50
|
||||||
|
items = (
|
||||||
|
await StorageItem.objects.filter(user=user)
|
||||||
|
.limit(count)
|
||||||
|
.select_related([StorageItem.quizzes, StorageItem.quiztivities])
|
||||||
|
.order_by(StorageItem.uploaded_at.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return_items: list[PrivateStorageItem] = []
|
||||||
|
for item in items:
|
||||||
|
return_items.append(PrivateStorageItem.from_db_model(item))
|
||||||
|
return return_items
|
||||||
|
|
||||||
|
|
||||||
class ReturnGetStorageLimit(BaseModel):
|
class ReturnGetStorageLimit(BaseModel):
|
||||||
limit: int
|
limit: int
|
||||||
limit_reached: bool
|
limit_reached: bool
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full max-h-full pb-20 px-20 h-full">
|
<div class="w-full max-h-full pb-20 px-20 h-full">
|
||||||
<div class="rounded-lg bg-white w-full h-full border-gray-500 drop-shadow-2xl dark:bg-gray-700">
|
<div class="rounded-lg bg-white w-full h-full border-gray-500 dark:bg-gray-700 shadow-2xl">
|
||||||
<div class="h-12 bg-gray-300 rounded-t-lg dark:bg-gray-500">
|
<div class="h-12 bg-gray-300 rounded-t-lg dark:bg-gray-500">
|
||||||
<div class="flex align-middle p-4 gap-3">
|
<div class="flex align-middle p-4 gap-3">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import Dashboard from '@uppy/dashboard';
|
import Dashboard from '@uppy/dashboard';
|
||||||
import Compressor from '@uppy/compressor';
|
import Compressor from '@uppy/compressor';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import BrownButton from '$lib/components/buttons/brown.svelte';
|
||||||
|
|
||||||
// CSS imports
|
// CSS imports
|
||||||
import '@uppy/core/dist/style.css';
|
import '@uppy/core/dist/style.css';
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
import type { EditorData } from '../quiz_types';
|
import type { EditorData } from '../quiz_types';
|
||||||
import { getLocalization } from '$lib/i18n';
|
import { getLocalization } from '$lib/i18n';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import Library from '$lib/editor/uploader/Library.svelte';
|
||||||
|
|
||||||
const { t } = getLocalization();
|
const { t } = getLocalization();
|
||||||
|
|
||||||
@@ -34,6 +36,18 @@
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
let video_popup: undefined | WindowProxy = undefined;
|
let video_popup: undefined | WindowProxy = undefined;
|
||||||
|
|
||||||
|
let selected_type: AvailableUploadTypes | null = null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
enum AvailableUploadTypes {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Image,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Video,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
Library
|
||||||
|
}
|
||||||
|
|
||||||
const uppy = new Uppy()
|
const uppy = new Uppy()
|
||||||
.use(DropTarget, {
|
.use(DropTarget, {
|
||||||
target: document.body
|
target: document.body
|
||||||
@@ -73,6 +87,7 @@
|
|||||||
console.log(selected_question, data);
|
console.log(selected_question, data);
|
||||||
|
|
||||||
modalOpen = false;
|
modalOpen = false;
|
||||||
|
selected_type = null;
|
||||||
});
|
});
|
||||||
console.log(edit_id);
|
console.log(edit_id);
|
||||||
|
|
||||||
@@ -83,6 +98,7 @@
|
|||||||
}
|
}
|
||||||
localStorage.removeItem('video_upload_id');
|
localStorage.removeItem('video_upload_id');
|
||||||
data.questions[selected_question].image = e.newValue;
|
data.questions[selected_question].image = e.newValue;
|
||||||
|
selected_type = null;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,42 +113,84 @@
|
|||||||
video_popup = undefined;
|
video_popup = undefined;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handle_on_click = (e: Event) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
modalOpen = false;
|
||||||
|
selected_type = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
modalOpen = false;
|
||||||
|
selected_type = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if modalOpen}
|
{#if modalOpen}
|
||||||
<div
|
<div
|
||||||
class="w-full h-full absolute top-0 left-0 bg-opacity-60 z-20 flex justify-center"
|
class="w-screen h-screen fixed top-0 left-0 bg-opacity-50 bg-black z-20 flex justify-center"
|
||||||
transition:fade|local
|
on:click={handle_on_click}
|
||||||
|
transition:fade|local={{ duration: 100 }}
|
||||||
>
|
>
|
||||||
<div>
|
{#if selected_type === null}
|
||||||
<div>
|
<div class="m-auto w-1/3 h-auto bg-white dark:bg-gray-700 p-4 rounded">
|
||||||
<button
|
<h1 class="text-3xl text-center mb-4">Select the Upload Type</h1>
|
||||||
type="button"
|
<div class="flex flex-row gap-4">
|
||||||
class="rounded-t-lg bg-black text-white px-1"
|
<div class="w-full">
|
||||||
|
<BrownButton
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
modalOpen = false;
|
selected_type = AvailableUploadTypes.Image;
|
||||||
|
}}>Image</BrownButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<BrownButton
|
||||||
|
disabled={!video_upload}
|
||||||
|
on:click={() => {
|
||||||
|
selected_type = AvailableUploadTypes.Video;
|
||||||
}}
|
}}
|
||||||
>Close
|
>Video
|
||||||
</button>
|
</BrownButton>
|
||||||
{#if video_upload}
|
|
||||||
<button
|
|
||||||
class="rounded-t-lg bg-black text-white px-1"
|
|
||||||
on:click={upload_video}
|
|
||||||
type="button"
|
|
||||||
>Upload video
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if video_popup}
|
<div class="w-full">
|
||||||
<div class="flex w-full h-1/3 bg-white dark:bg-gray-700 p-4">
|
<BrownButton
|
||||||
<h2 class="text-4xl m-auto">Popup open</h2>
|
on:click={() => {
|
||||||
|
selected_type = AvailableUploadTypes.Library;
|
||||||
|
}}
|
||||||
|
>Library
|
||||||
|
</BrownButton>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if selected_type === AvailableUploadTypes.Image}
|
||||||
|
<div class="m-auto w-1/3 h-5/6" transition:fade|local={{ duration: 100 }}>
|
||||||
<div>
|
<div>
|
||||||
<SvelteDashboard {uppy} width="100%" {props} />
|
<SvelteDashboard {uppy} width="100%" {props} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if selected_type === AvailableUploadTypes.Video}
|
||||||
|
<div
|
||||||
|
class="m-auto w-1/3 h-auto bg-white dark:bg-gray-700 p-4 rounded"
|
||||||
|
transition:fade|local={{ duration: 100 }}
|
||||||
|
>
|
||||||
|
<h1 class="text-3xl text-center mb-4">Upload a Video</h1>
|
||||||
|
{#if video_popup}
|
||||||
|
<p class="text-center">
|
||||||
|
The popup is open; have a look at it for further information
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<BrownButton on:click={upload_video} type="button">Upload video</BrownButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{:else if selected_type === AvailableUploadTypes.Library}
|
||||||
|
<div>
|
||||||
|
<Library bind:data {selected_question} bind:modalOpen />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex justify-center w-full pt-10" transition:fade|local>
|
<div class="flex justify-center w-full pt-10" transition:fade|local>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<!--
|
||||||
|
- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PrivateImageData } from '$lib/quiz_types';
|
||||||
|
import Spinner from '$lib/Spinner.svelte';
|
||||||
|
import type { EditorData } from '$lib/quiz_types';
|
||||||
|
import BrownButton from '$lib/components/buttons/brown.svelte';
|
||||||
|
|
||||||
|
export let data: EditorData;
|
||||||
|
export let selected_question: number;
|
||||||
|
export let modalOpen: boolean;
|
||||||
|
|
||||||
|
const fetch_images = async (): Promise<PrivateImageData> => {
|
||||||
|
const response = await fetch('/api/v1/storage/list/last?count=50');
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
let image_fetch = fetch_images();
|
||||||
|
|
||||||
|
const set_image = (id: string) => {
|
||||||
|
if (selected_question === undefined) {
|
||||||
|
data.cover_image = id;
|
||||||
|
} else if (selected_question === -1) {
|
||||||
|
data.background_image = id;
|
||||||
|
} else {
|
||||||
|
data.questions[selected_question].image = id;
|
||||||
|
}
|
||||||
|
modalOpen = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await image_fetch}
|
||||||
|
<Spinner />
|
||||||
|
{:then images}
|
||||||
|
<div class="flex w-screen p-8 h-screen">
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-1/3 m-auto overflow-scroll h-full rounded p-4 gap-4 bg-white dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
{#each images as image}
|
||||||
|
<div class="rounded border-2 border-[#B07156] p-2 flex-col flex gap-2">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="/api/v1/storage/download/{image.id}"
|
||||||
|
loading="lazy"
|
||||||
|
alt={image.alt_text}
|
||||||
|
class="object-contain w-full h-full max-h-full rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-center">{image.filename ?? 'No name available'}</p>
|
||||||
|
<BrownButton
|
||||||
|
on:click={() => {
|
||||||
|
set_image(image.id);
|
||||||
|
}}>Select</BrownButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
@@ -88,3 +88,19 @@ export interface EditorData {
|
|||||||
background_color?: string;
|
background_color?: string;
|
||||||
background_image?: string;
|
background_image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PrivateImageData {
|
||||||
|
id: string;
|
||||||
|
uploaded_at: string;
|
||||||
|
mime_type: string;
|
||||||
|
hash?: string;
|
||||||
|
size?: number;
|
||||||
|
deleted_at?: string;
|
||||||
|
alt_text?: string;
|
||||||
|
filename?: string;
|
||||||
|
thumbhash?: string;
|
||||||
|
server?: string;
|
||||||
|
imported: boolean;
|
||||||
|
quizzes: { id: string }[];
|
||||||
|
quiztivities: { id: string }[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,26 +4,11 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
import type { PrivateImageData } from '$lib/quiz_types';
|
||||||
export interface ImageData {
|
|
||||||
id: string;
|
|
||||||
uploaded_at: string;
|
|
||||||
mime_type: string;
|
|
||||||
hash?: string;
|
|
||||||
size?: number;
|
|
||||||
deleted_at?: string;
|
|
||||||
alt_text?: string;
|
|
||||||
filename?: string;
|
|
||||||
thumbhash?: string;
|
|
||||||
server?: string;
|
|
||||||
imported: boolean;
|
|
||||||
quizzes: { id: string }[];
|
|
||||||
quiztivities: { id: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const load = (async ({ fetch }) => {
|
export const load = (async ({ fetch }) => {
|
||||||
const res = await fetch('/api/v1/storage/list');
|
const res = await fetch('/api/v1/storage/list');
|
||||||
const json: ImageData[] = await res.json();
|
const json: PrivateImageData[] = await res.json();
|
||||||
return {
|
return {
|
||||||
images: json
|
images: json
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user