Skip to content

Commit f1d826c

Browse files
authored
Merge pull request #11 from jrafaaael/feat/upload-recording
feat: upload recording
2 parents a608400 + 6659caf commit f1d826c

File tree

9 files changed

+175
-9
lines changed

9 files changed

+175
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ These are some features that we want to develop in the short, medium and long te
3636
| ------------------------- | ------ | ---------------- | ------ | -------------------- | ------ |
3737
| Store videos || Selfie recording || Auto zoom ||
3838
| Upload background || Subtitles || 3D renderer ||
39-
| Upload videos (recording) | | Upload audio || Auto trim low-volume ||
39+
| Upload videos (recording) | | Upload audio || Auto trim low-volume ||
4040
| Crop || Clips || Mobile ||
4141
| Trim || Layouts || | |
4242
| Vertical export || Mockups || | |

src/routes/(recorder)/+page.svelte

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,49 @@
11
<script lang="ts">
2+
import { goto } from '$app/navigation';
3+
import { saveFile } from './utils/save-file';
4+
import Dropzone from './components/dropzone.svelte';
25
import RecordingList from './components/recording-list.svelte';
36
import FloatingRecordingBar from './components/floating-recording-bar.svelte';
7+
8+
let isDroppingNewRecording = false;
9+
10+
async function handleDrop({ detail: { files } }: CustomEvent<{ files: FileList | undefined }>) {
11+
const file = files?.[0];
12+
13+
if (!file) return;
14+
if (!file.type.includes('video')) return;
15+
16+
const filename = file.name;
17+
const folderName = file.name.split('.').slice(0, -1).join('.');
18+
19+
await saveFile(file, `${folderName}/${filename}`);
20+
await goto(folderName);
21+
}
422
</script>
523

6-
<main
7-
class="w-full h-full p-8 pb-44 grid grid-cols-[repeat(auto-fill,minmax(360px,1fr))] auto-rows-min gap-8 md:p-16 md:pb-44 lg:px-32 lg:gap-y-16"
8-
>
9-
<RecordingList />
10-
<FloatingRecordingBar />
24+
<main>
25+
<Dropzone
26+
class="w-full min-h-dvh p-8 pb-44 grid grid-cols-[repeat(auto-fill,minmax(360px,1fr))] auto-rows-min gap-8 cursor-auto outline-none md:p-16 md:pb-44 lg:px-32 lg:gap-y-16 {isDroppingNewRecording
27+
? '*:pointer-events-none'
28+
: ''}"
29+
openPickerOnClick={false}
30+
on:dragenter={() => (isDroppingNewRecording = true)}
31+
on:dragover={() => (isDroppingNewRecording = true)}
32+
on:dragleave={() => (isDroppingNewRecording = false)}
33+
on:drop={(e) => {
34+
isDroppingNewRecording = false;
35+
handleDrop(e);
36+
}}
37+
>
38+
<article
39+
class="w-full h-full bg-white/10 flex justify-center items-center flex-col gap-2 fixed inset-0 z-10 backdrop-blur {isDroppingNewRecording
40+
? 'block'
41+
: 'hidden'}"
42+
>
43+
<h2 class="text-2xl font-bold">Drop it!</h2>
44+
<p>Drop here any video to upload it and star editing!</p>
45+
</article>
46+
<RecordingList />
47+
<FloatingRecordingBar />
48+
</Dropzone>
1149
</main>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script lang="ts">
2+
import { createEventDispatcher } from 'svelte';
3+
4+
export let openPickerOnClick = true;
5+
export let accept: string | null = null;
6+
let inputRef: HTMLInputElement;
7+
let dispatch = createEventDispatcher();
8+
9+
function handleDrop(
10+
e: DragEvent & {
11+
currentTarget: EventTarget & HTMLDivElement;
12+
}
13+
) {
14+
const dt = e.dataTransfer;
15+
const files = dt?.files;
16+
17+
dispatch('drop', { files });
18+
}
19+
</script>
20+
21+
<!-- svelte-ignore a11y-click-events-have-key-events -->
22+
<div
23+
role="button"
24+
tabindex="-1"
25+
class={$$restProps.class}
26+
on:click={() => openPickerOnClick && inputRef.showPicker()}
27+
on:drag|preventDefault|stopPropagation
28+
on:dragstart|preventDefault|stopPropagation
29+
on:dragend|preventDefault|stopPropagation
30+
on:dragenter|preventDefault|stopPropagation
31+
on:dragover|preventDefault|stopPropagation
32+
on:dragleave|preventDefault|stopPropagation
33+
on:drop|preventDefault|stopPropagation={handleDrop}
34+
>
35+
<slot />
36+
<input
37+
class="sr-only"
38+
type="file"
39+
name="dropzone"
40+
id="dropzone"
41+
tabindex="-1"
42+
{accept}
43+
bind:this={inputRef}
44+
/>
45+
</div>

src/routes/(recorder)/components/floating-recording-bar.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { isRecording } from '../stores/is-recording';
99
import { COUNTDOWN_OPTIONS } from '../utils/constants';
1010
import { recordScreen } from '../utils/record-screen';
11+
import UploadRecording from './upload-recording.svelte';
1112
import VideoSlash from './icons/video-slash.svelte';
1213
import Video from './icons/video.svelte';
1314
import Clock from './icons/clock.svelte';
@@ -69,6 +70,8 @@
6970
: 'bg-red-600'}"
7071
>
7172
{#if !countdownInterval}
73+
<UploadRecording />
74+
<div class="w-[2px] h-[40%] bg-white/20" />
7275
<button class="h-full px-5 flex items-center gap-2" on:click={handleClick}>
7376
<span class="w-4 aspect-square text-neutral-50/50">
7477
{#if !$isRecording}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<svg
2+
xmlns="http://www.w3.org/2000/svg"
3+
fill="none"
4+
viewBox="0 0 24 24"
5+
stroke-width="2"
6+
stroke="currentColor"
7+
>
8+
<path
9+
stroke-linecap="round"
10+
stroke-linejoin="round"
11+
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"
12+
/>
13+
</svg>

src/routes/(recorder)/components/recording-list.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727
</script>
2828

2929
{#each recordings as record}
30+
{@const folderName = isNaN(Number(record)) ? record : new Date(+record).toLocaleString()}
3031
<article class="flex flex-col gap-2">
3132
<a href="/{record}">
3233
<span class="w-full aspect-video bg-white/5 border border-white/5 rounded-md inline-block" />
3334
</a>
3435
<div class="flex justify-between items-center">
3536
<h3 class="text-sm font-medium">
3637
<a class="py-2" href="/{record}">
37-
{new Date(+record).toLocaleString(undefined)}
38+
{folderName}
3839
</a>
3940
</h3>
4041
<button
@@ -60,7 +61,7 @@
6061
</span>
6162
<div class="flex flex-col gap-1">
6263
<h3 class="font-medium">No recordings yet!</h3>
63-
<p class="text-sm">Record your first video and it'll show up here!</p>
64+
<p class="text-sm">Record, upload or drag 'n drop your first video and it'll show up here!</p>
6465
</div>
6566
</article>
6667
{/each}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script lang="ts">
2+
import { goto } from '$app/navigation';
3+
import { saveFile } from '../utils/save-file';
4+
import Upload from './icons/upload.svelte';
5+
6+
let inputRef: HTMLInputElement;
7+
8+
async function handleUpload(
9+
e: Event & {
10+
currentTarget: EventTarget & HTMLInputElement;
11+
}
12+
) {
13+
const files = e.currentTarget.files;
14+
const file = files?.[0];
15+
16+
if (!file) return;
17+
if (!file.type.includes('video')) return;
18+
19+
const filename = file.name;
20+
const folderName = file.name.split('.').slice(0, -1).join('.');
21+
22+
await saveFile(file, `${folderName}/${filename}`);
23+
await goto(folderName);
24+
}
25+
</script>
26+
27+
<button
28+
class="h-full px-5 flex items-center gap-2"
29+
on:click={() => {
30+
inputRef.showPicker();
31+
}}
32+
>
33+
<span class="w-4 aspect-square text-neutral-50/50">
34+
<Upload />
35+
</span>
36+
<span>Upload video</span>
37+
</button>
38+
<input
39+
class="sr-only"
40+
type="file"
41+
name="recording"
42+
id="recording"
43+
accept="video/*"
44+
bind:this={inputRef}
45+
on:change={handleUpload}
46+
/>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export async function saveFile(file: File, path: string) {
2+
if (path.startsWith('/')) throw new Error("path shouldn't start with /");
3+
4+
const [filename, ...folders] = path.split('/').toReversed();
5+
const root = await navigator.storage.getDirectory();
6+
let fileParentHandle: FileSystemDirectoryHandle;
7+
8+
for (const folder of folders.toReversed()) {
9+
const dir = fileParentHandle ?? root;
10+
11+
fileParentHandle = await dir.getDirectoryHandle(folder, { create: true });
12+
}
13+
14+
const fileHandle = await fileParentHandle.getFileHandle(filename, { create: true });
15+
const writable = await fileHandle.createWritable();
16+
await writable.write(file);
17+
await writable.close();
18+
}

src/routes/[id]/components/header.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
1313
export let getFrameAsImage: () => string;
1414
export let getMP4: () => Promise<string>;
15-
const folderName = new Date(+$page.params.id).toLocaleString(undefined);
15+
const folderName = isNaN(Number(+$page.params.id))
16+
? $page.params.id
17+
: new Date(+$page.params.id).toLocaleString();
1618
let extension = writable(EXPORT_OPTIONS.at(0));
1719
let isExporting = false;
1820

0 commit comments

Comments
 (0)