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