|
|
@@ -158,6 +158,7 @@
|
|
|
</label>
|
|
|
<div class="relative group">
|
|
|
<input v-model="modelLink" type="url" :placeholder="t('upload.modelLinkPlaceholder')"
|
|
|
+ @paste="onLinkPaste"
|
|
|
class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 pr-12 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
|
|
|
<button
|
|
|
v-if="modelLink.includes('thingiverse.com') || modelLink.includes('printables.com')"
|
|
|
@@ -171,6 +172,25 @@
|
|
|
<LinkIcon v-else class="w-4 h-4" />
|
|
|
</button>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- Animated Import Prompt -->
|
|
|
+ <Transition enter-active-class="transition duration-300 ease-out" enter-from-class="transform -translate-y-2 opacity-0" enter-to-class="transform translate-y-0 opacity-100">
|
|
|
+ <div v-if="(modelLink.includes('thingiverse.com') || modelLink.includes('printables.com')) && !files.length && !isImporting"
|
|
|
+ class="mt-3 p-4 bg-primary/10 border border-primary/20 rounded-2xl flex items-center justify-between gap-4 animate-pulse-subtle">
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
+ <div class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
|
|
+ <FileBox class="w-5 h-5" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <p class="text-xs font-bold text-primary uppercase tracking-tight">{{ t("common.import") }}?</p>
|
|
|
+ <p class="text-[10px] text-primary/60 font-medium">{{ t("upload.importHint") || "We can automatically download files from this link!" }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <Button @click="handleImportUrl" variant="hero" size="sm" class="h-9 px-4 rounded-xl shadow-lg">
|
|
|
+ {{ t("common.import") }}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </Transition>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -502,16 +522,21 @@ async function handleImportUrl() {
|
|
|
}
|
|
|
|
|
|
isImporting.value = true;
|
|
|
+ const loadingToast = toast.loading(t("common.loading") + "...");
|
|
|
+
|
|
|
try {
|
|
|
const res = await importFromUrl(modelLink.value);
|
|
|
if (res.status === "success" && res.files) {
|
|
|
for (const f of res.files) {
|
|
|
+ // Prevent duplicates
|
|
|
+ if (files.value.some(existing => existing.dbId === f.id)) continue;
|
|
|
+
|
|
|
files.value.push({
|
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
|
dbId: f.id,
|
|
|
name: f.filename,
|
|
|
size: f.size,
|
|
|
- type: "model/stl", // Assumption
|
|
|
+ type: "model/stl",
|
|
|
quantity: 1,
|
|
|
printTime: f.print_time,
|
|
|
filamentG: f.filament_g,
|
|
|
@@ -519,16 +544,29 @@ async function handleImportUrl() {
|
|
|
isUploading: false
|
|
|
});
|
|
|
}
|
|
|
- toast.success(t("common.success"));
|
|
|
+ toast.success(t("common.success"), { id: loadingToast });
|
|
|
}
|
|
|
} catch (e: any) {
|
|
|
console.error("Import failed:", e);
|
|
|
- toast.error(e.message || "Failed to import models. Check URL or try again later.");
|
|
|
+ let msg = e.message || "Failed to import models";
|
|
|
+ if (modelLink.value.includes("printables.com")) {
|
|
|
+ msg = "Printables link failed. This site is often protected. Please download ZIP manually and upload here.";
|
|
|
+ }
|
|
|
+ toast.error(msg, { id: loadingToast });
|
|
|
} finally {
|
|
|
isImporting.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function onLinkPaste() {
|
|
|
+ // Wait for next tick to let v-model update
|
|
|
+ setTimeout(() => {
|
|
|
+ if ((modelLink.value.includes('thingiverse.com') || modelLink.value.includes('printables.com')) && !files.length) {
|
|
|
+ handleImportUrl();
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
+}
|
|
|
+
|
|
|
async function handleFileSelect(e: Event) { addFiles(Array.from((e.target as HTMLInputElement).files ?? [])); }
|
|
|
function removeFile(id: string) { files.value = files.value.filter(f => f.id !== id); }
|
|
|
function formatFileSize(bytes: number) {
|