+127
-70
src/components/create.tsx
+127
-70
src/components/create.tsx
···
1
1
import { Client } from "@atcute/client";
2
2
import { remove } from "@mary/exif-rm";
3
3
import { useNavigate, useParams } from "@solidjs/router";
4
-
import { createSignal, Show } from "solid-js";
4
+
import { createSignal, onCleanup, Show } from "solid-js";
5
5
import { Editor, editorView } from "../components/editor.jsx";
6
6
import { agent } from "../components/login.jsx";
7
7
import { setNotif } from "../layout.jsx";
···
15
15
const params = useParams();
16
16
const [openDialog, setOpenDialog] = createSignal(false);
17
17
const [notice, setNotice] = createSignal("");
18
-
const [uploading, setUploading] = createSignal(false);
18
+
const [openUpload, setOpenUpload] = createSignal(false);
19
+
let blobInput!: HTMLInputElement;
19
20
let formRef!: HTMLFormElement;
20
21
21
22
const placeholder = () => {
···
125
126
}
126
127
};
127
128
128
-
const uploadBlob = async () => {
129
-
setNotice("");
130
-
let blob: Blob;
129
+
const FileUpload = (props: { file: File }) => {
130
+
const [uploading, setUploading] = createSignal(false);
131
+
const [error, setError] = createSignal("");
131
132
132
-
const file = (document.getElementById("blob") as HTMLInputElement)?.files?.[0];
133
-
if (!file) return;
133
+
onCleanup(() => (blobInput.value = ""));
134
134
135
-
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
136
-
(document.getElementById("mimetype") as HTMLInputElement).value = "";
137
-
if (mimetype) blob = new Blob([file], { type: mimetype });
138
-
else blob = file;
135
+
const formatFileSize = (bytes: number) => {
136
+
if (bytes === 0) return "0 Bytes";
137
+
const k = 1024;
138
+
const sizes = ["Bytes", "KB", "MB", "GB"];
139
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
140
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
141
+
};
142
+
143
+
const uploadBlob = async () => {
144
+
let blob: Blob;
145
+
146
+
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
147
+
(document.getElementById("mimetype") as HTMLInputElement).value = "";
148
+
if (mimetype) blob = new Blob([props.file], { type: mimetype });
149
+
else blob = props.file;
139
150
140
-
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
141
-
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
142
-
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
143
-
}
151
+
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
152
+
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
153
+
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
154
+
}
144
155
145
-
const rpc = new Client({ handler: agent()! });
146
-
setUploading(true);
147
-
const res = await rpc.post("com.atproto.repo.uploadBlob", {
148
-
input: blob,
149
-
});
150
-
setUploading(false);
151
-
(document.getElementById("blob") as HTMLInputElement).value = "";
152
-
if (!res.ok) {
153
-
setNotice(res.data.error);
154
-
return;
155
-
}
156
-
editorView.dispatch({
157
-
changes: {
158
-
from: editorView.state.selection.main.head,
159
-
insert: JSON.stringify(res.data.blob, null, 2),
160
-
},
161
-
});
156
+
const rpc = new Client({ handler: agent()! });
157
+
setUploading(true);
158
+
const res = await rpc.post("com.atproto.repo.uploadBlob", {
159
+
input: blob,
160
+
});
161
+
setUploading(false);
162
+
if (!res.ok) {
163
+
setError(res.data.error);
164
+
return;
165
+
}
166
+
editorView.dispatch({
167
+
changes: {
168
+
from: editorView.state.selection.main.head,
169
+
insert: JSON.stringify(res.data.blob, null, 2),
170
+
},
171
+
});
172
+
setOpenUpload(false);
173
+
};
174
+
175
+
return (
176
+
<div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-70 left-[50%] max-w-[20rem] min-w-[16rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
177
+
<h2 class="mb-2 font-semibold">Upload blob</h2>
178
+
<div class="flex flex-col gap-2">
179
+
<div class="flex flex-col gap-1">
180
+
<p class="flex gap-1">
181
+
<span class="truncate">{props.file.name}</span>
182
+
<span class="shrink-0 text-neutral-600 dark:text-neutral-400">
183
+
({formatFileSize(props.file.size)})
184
+
</span>
185
+
</p>
186
+
</div>
187
+
<div class="flex items-center gap-x-2">
188
+
<label for="mimetype" class="shrink-0 select-none">
189
+
MIME type
190
+
</label>
191
+
<TextInput id="mimetype" placeholder={props.file.type} />
192
+
</div>
193
+
<div class="flex items-center gap-1">
194
+
<input id="exif-rm" type="checkbox" checked />
195
+
<label for="exif-rm" class="select-none">
196
+
Remove EXIF data
197
+
</label>
198
+
</div>
199
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
200
+
Metadata will be pasted after the cursor
201
+
</p>
202
+
<Show when={error()}>
203
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
204
+
</Show>
205
+
<div class="flex justify-between gap-2">
206
+
<Button onClick={() => setOpenUpload(false)}>Cancel</Button>
207
+
<Show when={uploading()}>
208
+
<div class="flex items-center gap-1">
209
+
<span class="iconify lucide--loader-circle animate-spin"></span>
210
+
<span>Uploading</span>
211
+
</div>
212
+
</Show>
213
+
<Show when={!uploading()}>
214
+
<Button
215
+
onClick={uploadBlob}
216
+
class="dark:shadow-dark-800 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
217
+
>
218
+
Upload
219
+
</Button>
220
+
</Show>
221
+
</div>
222
+
</div>
223
+
</div>
224
+
);
162
225
};
163
226
164
227
return (
···
205
268
/>
206
269
</div>
207
270
</Show>
208
-
<div class="flex items-center gap-x-2">
209
-
<label for="validate" class="min-w-20 select-none">
210
-
Validate
211
-
</label>
212
-
<select
213
-
name="validate"
214
-
id="validate"
215
-
class="dark:bg-dark-100 dark:shadow-dark-800 rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 shadow-xs focus:outline-[1px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200"
216
-
>
217
-
<option value="unset">Unset</option>
218
-
<option value="true">True</option>
219
-
<option value="false">False</option>
220
-
</select>
221
-
</div>
222
-
<div class="flex items-center gap-2">
223
-
<Show when={!uploading()}>
224
-
<div class="dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 flex rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 text-xs font-semibold shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
225
-
<input type="file" id="blob" class="sr-only" onChange={() => uploadBlob()} />
226
-
<label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob">
227
-
<span class="iconify lucide--upload text-sm"></span>
228
-
Upload
229
-
</label>
230
-
</div>
231
-
<p class="text-xs">Metadata will be pasted after the cursor</p>
232
-
</Show>
233
-
<Show when={uploading()}>
234
-
<span class="iconify lucide--loader-circle animate-spin text-xl"></span>
235
-
<p>Uploading...</p>
236
-
</Show>
237
-
</div>
238
-
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
271
+
<div class="flex justify-between">
239
272
<div class="flex items-center gap-x-2">
240
-
<label for="mimetype" class="min-w-20 select-none">
241
-
MIME type
273
+
<label for="validate" class="min-w-20 select-none">
274
+
Validate
242
275
</label>
243
-
<TextInput id="mimetype" placeholder="Optional" class="w-[15rem]" />
276
+
<select
277
+
name="validate"
278
+
id="validate"
279
+
class="dark:bg-dark-100 dark:shadow-dark-800 rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 shadow-xs focus:outline-[1px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200"
280
+
>
281
+
<option value="unset">Unset</option>
282
+
<option value="true">True</option>
283
+
<option value="false">False</option>
284
+
</select>
244
285
</div>
245
-
<div class="flex items-center gap-1">
246
-
<input id="exif-rm" type="checkbox" checked />
247
-
<label for="exif-rm" class="select-none">
248
-
Remove EXIF data
286
+
<div class="dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 flex rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 text-xs shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
287
+
<input
288
+
type="file"
289
+
id="blob"
290
+
class="sr-only"
291
+
ref={blobInput}
292
+
onChange={(e) => {
293
+
if (e.target.files !== null) setOpenUpload(true);
294
+
}}
295
+
/>
296
+
<label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob">
297
+
<span class="iconify lucide--upload text-sm"></span>
298
+
Upload
249
299
</label>
250
300
</div>
301
+
<Modal
302
+
open={openUpload()}
303
+
onClose={() => setOpenUpload(false)}
304
+
closeOnClick={false}
305
+
>
306
+
<FileUpload file={blobInput.files![0]} />
307
+
</Modal>
251
308
</div>
252
309
</div>
253
310
<Editor
+2
-2
src/views/collection.tsx
+2
-2
src/views/collection.tsx
···
267
267
<Button onClick={() => setOpenDelete(false)}>Cancel</Button>
268
268
<Button
269
269
onClick={deleteRecords}
270
-
class={`dark:shadow-dark-800 rounded-lg px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`}
270
+
class={`dark:shadow-dark-800 rounded-lg px-2 py-1.5 text-xs text-white shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`}
271
271
>
272
272
{recreate() ? "Recreate" : "Delete"}
273
273
</Button>
···
301
301
}}
302
302
>
303
303
<span
304
-
class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"} text-sm`}
304
+
class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"}`}
305
305
></span>
306
306
Reverse
307
307
</Button>
+1
-1
src/views/record.tsx
+1
-1
src/views/record.tsx
···
177
177
<Button onClick={() => setOpenDelete(false)}>Cancel</Button>
178
178
<Button
179
179
onClick={deleteRecord}
180
-
class="dark:shadow-dark-800 rounded-lg bg-red-500 px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none hover:bg-red-400 active:bg-red-400"
180
+
class="dark:shadow-dark-800 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400"
181
181
>
182
182
Delete
183
183
</Button>