|
@@ -158,39 +158,8 @@
|
|
|
</label>
|
|
</label>
|
|
|
<div class="relative group">
|
|
<div class="relative group">
|
|
|
<input v-model="modelLink" type="url" :placeholder="t('upload.modelLinkPlaceholder')"
|
|
<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')"
|
|
|
|
|
- type="button"
|
|
|
|
|
- @click="handleImportUrl"
|
|
|
|
|
- :disabled="isImporting"
|
|
|
|
|
- class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-primary text-white hover:bg-primary/90 transition-all shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
- :title="t('common.import')"
|
|
|
|
|
- >
|
|
|
|
|
- <Loader2 v-if="isImporting" class="w-4 h-4 animate-spin" />
|
|
|
|
|
- <LinkIcon v-else class="w-4 h-4" />
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
|
|
|
</div>
|
|
</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>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -370,7 +339,7 @@ import { Upload, FileBox, X, Check, Link as LinkIcon, MapPin, User, Phone, Mail,
|
|
|
import Button from "./ui/button.vue";
|
|
import Button from "./ui/button.vue";
|
|
|
import { defineAsyncComponent } from "vue";
|
|
import { defineAsyncComponent } from "vue";
|
|
|
const StlViewer = defineAsyncComponent(() => import("@/components/StlViewer.vue"));
|
|
const StlViewer = defineAsyncComponent(() => import("@/components/StlViewer.vue"));
|
|
|
-import { submitOrder, getCurrentUser, getMaterials, getPriceEstimate, uploadFilesToServer, importFromUrl } from "@/lib/api";
|
|
|
|
|
|
|
+import { submitOrder, getCurrentUser, getMaterials, getPriceEstimate, uploadFilesToServer } from "@/lib/api";
|
|
|
|
|
|
|
|
interface UploadedFile { id: string; dbId?: number; name: string; size: number; type: string; file?: File; quantity: number; basePrice?: number; isUploading?: boolean; printTime?: string; filamentG?: number; dimensions?: string; }
|
|
interface UploadedFile { id: string; dbId?: number; name: string; size: number; type: string; file?: File; quantity: number; basePrice?: number; isUploading?: boolean; printTime?: string; filamentG?: number; dimensions?: string; }
|
|
|
|
|
|
|
@@ -382,7 +351,6 @@ const { t, locale } = useI18n();
|
|
|
const files = ref<UploadedFile[]>([]);
|
|
const files = ref<UploadedFile[]>([]);
|
|
|
const isDragging = ref(false);
|
|
const isDragging = ref(false);
|
|
|
const isSubmitting = ref(false);
|
|
const isSubmitting = ref(false);
|
|
|
-const isImporting = ref(false);
|
|
|
|
|
const modelLink = ref("");
|
|
const modelLink = ref("");
|
|
|
const address = ref("");
|
|
const address = ref("");
|
|
|
const firstName = ref("");
|
|
const firstName = ref("");
|
|
@@ -512,60 +480,6 @@ async function addFiles(rawFiles: File[]) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
function handleDrop(e: DragEvent) { isDragging.value = false; addFiles(Array.from(e.dataTransfer?.files ?? [])); }
|
|
function handleDrop(e: DragEvent) { isDragging.value = false; addFiles(Array.from(e.dataTransfer?.files ?? [])); }
|
|
|
-async function handleImportUrl() {
|
|
|
|
|
- if (!modelLink.value || isImporting.value) return;
|
|
|
|
|
-
|
|
|
|
|
- const token = localStorage.getItem("token");
|
|
|
|
|
- if (!token) {
|
|
|
|
|
- toast.error(t("auth.loginRequired"));
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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",
|
|
|
|
|
- quantity: 1,
|
|
|
|
|
- printTime: f.print_time,
|
|
|
|
|
- filamentG: f.filament_g,
|
|
|
|
|
- dimensions: f.dimensions,
|
|
|
|
|
- isUploading: false
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- toast.success(t("common.success"), { id: loadingToast });
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e: any) {
|
|
|
|
|
- console.error("Import failed:", e);
|
|
|
|
|
- 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.value.length) {
|
|
|
|
|
- handleImportUrl();
|
|
|
|
|
- }
|
|
|
|
|
- }, 100);
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
async function handleFileSelect(e: Event) { addFiles(Array.from((e.target as HTMLInputElement).files ?? [])); }
|
|
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 removeFile(id: string) { files.value = files.value.filter(f => f.id !== id); }
|