|
@@ -156,8 +156,21 @@
|
|
|
<label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
|
|
<label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
|
|
|
{{ t("upload.modelLink") }}
|
|
{{ t("upload.modelLink") }}
|
|
|
</label>
|
|
</label>
|
|
|
- <input v-model="modelLink" type="url" :placeholder="t('upload.modelLinkPlaceholder')"
|
|
|
|
|
- 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 class="relative group">
|
|
|
|
|
+ <input v-model="modelLink" type="url" :placeholder="t('upload.modelLinkPlaceholder')"
|
|
|
|
|
+ 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>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -323,9 +336,9 @@ 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 } from "@/lib/api";
|
|
|
|
|
|
|
+import { submitOrder, getCurrentUser, getMaterials, getPriceEstimate, uploadFilesToServer, importFromUrl } 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; }
|
|
|
|
|
|
|
+interface UploadedFile { id: string; dbId?: number; name: string; size: number; type: string; file?: File; quantity: number; basePrice?: number; isUploading?: boolean; printTime?: string; filamentG?: number; }
|
|
|
|
|
|
|
|
// --- CONFIGURATION ---
|
|
// --- CONFIGURATION ---
|
|
|
// Set to false to hide the hardcore slicing data (time and weight) from the end-user
|
|
// Set to false to hide the hardcore slicing data (time and weight) from the end-user
|
|
@@ -335,6 +348,7 @@ 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("");
|
|
@@ -461,7 +475,43 @@ 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 ?? [])); }
|
|
|
-function handleFileSelect(e: Event) { addFiles(Array.from((e.target as HTMLInputElement).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;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await importFromUrl(modelLink.value);
|
|
|
|
|
+ if (res.status === "success" && res.files) {
|
|
|
|
|
+ for (const f of res.files) {
|
|
|
|
|
+ files.value.push({
|
|
|
|
|
+ id: Math.random().toString(36).substr(2, 9),
|
|
|
|
|
+ dbId: f.id,
|
|
|
|
|
+ name: f.filename,
|
|
|
|
|
+ size: f.size,
|
|
|
|
|
+ type: "model/stl", // Assumption
|
|
|
|
|
+ quantity: 1,
|
|
|
|
|
+ printTime: f.print_time,
|
|
|
|
|
+ filamentG: f.filament_g,
|
|
|
|
|
+ isUploading: false
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ toast.success(t("common.success"));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e: any) {
|
|
|
|
|
+ console.error("Import failed:", e);
|
|
|
|
|
+ toast.error(e.message || "Failed to import models. Check URL or try again later.");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isImporting.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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); }
|
|
|
function formatFileSize(bytes: number) {
|
|
function formatFileSize(bytes: number) {
|
|
|
if (bytes < 1024) return bytes + " B";
|
|
if (bytes < 1024) return bytes + " B";
|