Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,14 @@
]"
>
<router-view v-slot="{ Component }">
<KeepAlive :max="3">
<component
:is="Component"
:share-id="activeShareId"
:key-url-safe="activeShareKey"
@send-to-tool="switchTool"
@exit-receive="switchTool('merge')"
@open-enterprise="isEnterpriseOpen = true"
/>
</KeepAlive>
<component
:is="Component"
:share-id="activeShareId"
:key-url-safe="activeShareKey"
@send-to-tool="switchTool"
@exit-receive="switchTool('merge')"
@open-enterprise="isEnterpriseOpen = true"
/>
</router-view>
</main>

Expand Down Expand Up @@ -99,6 +97,9 @@
:is-open="isPrivacyOpen"
@close="isPrivacyOpen = false"
/>

<!-- Global Loading Mask for async heavy operations -->
<GlobalLoadingMask />
</div>
</template>

Expand All @@ -109,6 +110,7 @@ import Navbar from './components/Navbar.vue';
import Footer from './components/Footer.vue';
import MobileBottomNav from './components/MobileBottomNav.vue';
import MobileToolsDrawer from './components/MobileToolsDrawer.vue';
import GlobalLoadingMask from './components/GlobalLoadingMask.vue';
const FeedbackModal = defineAsyncComponent(() => import('./components/FeedbackModal.vue'));
const PrivacyModal = defineAsyncComponent(() => import('./components/PrivacyModal.vue'));
const GlobalSettingsModal = defineAsyncComponent(() => import('./components/GlobalSettingsModal.vue'));
Expand Down
38 changes: 38 additions & 0 deletions src/components/GlobalLoadingMask.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<Transition name="fade">
<div
v-if="globalLoading"
class="fixed inset-0 z-[9999] bg-slate-900/40 backdrop-blur-sm flex flex-col items-center justify-center transition-all"
>
<!-- Sleek, minimal glassmorphism pill -->
<div class="bg-white/95 backdrop-blur-md px-6 py-4 rounded-2xl shadow-2xl flex items-center space-x-4 border border-white/40 transform scale-100 animate-in zoom-in-95 duration-200">
<!-- Spinner -->
<Loader2 class="w-6 h-6 text-blue-600 animate-spin" />

<!-- Message -->
<p class="text-slate-700 font-medium text-sm tracking-wide">
{{ loadingMessage || t('loading_default', 'Processing...') }}
</p>
</div>
</div>
</Transition>
</template>

<script setup>
import { useGlobalLoading } from '../utils/useGlobalLoading';
import { Loader2 } from 'lucide-vue-next';
import { t } from '../i18n';

const { globalLoading, loadingMessage } = useGlobalLoading();
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
192 changes: 192 additions & 0 deletions src/components/PagePreviewModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<template>
<Transition name="fade">
<div
v-if="isOpen"
class="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-slate-950/85 backdrop-blur-md select-none overflow-hidden"
tabindex="-1"
@keydown.esc="close"
>
<!-- Top Floating Control Bar -->
<div class="absolute top-4 inset-x-0 z-20 flex items-center justify-between px-6 pointer-events-none">
<!-- Title / Page hint (left) -->
<div class="pointer-events-auto flex items-center space-x-2 bg-slate-900/80 backdrop-blur-md px-3.5 py-1.5 rounded-full border border-slate-700/60 text-xs font-medium text-slate-300 shadow-xl">
<span>{{ title || t('preview_title', 'Page Preview') }}</span>
</div>

<!-- Zoom & Viewport Toolbar (center) -->
<div class="pointer-events-auto flex items-center space-x-1.5 bg-slate-900/80 backdrop-blur-md px-3 py-1.5 rounded-full border border-slate-700/60 shadow-xl text-slate-200">
<button
@click="zoomOut"
:disabled="zoomLevel <= 0.5"
class="p-1.5 hover:bg-slate-800 text-slate-300 hover:text-white rounded-full transition disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
:title="t('zoom_out', 'Zoom Out')"
>
<ZoomOut class="w-4 h-4" />
</button>

<button
@click="resetZoom"
class="px-2 py-0.5 text-xs font-mono font-medium hover:bg-slate-800 text-slate-300 hover:text-white rounded-md transition cursor-pointer min-w-[50px] text-center"
:title="t('zoom_reset', 'Reset (Fit)')"
>
{{ Math.round(zoomLevel * 100) }}%
</button>

<button
@click="zoomIn"
:disabled="zoomLevel >= 3.0"
class="p-1.5 hover:bg-slate-800 text-slate-300 hover:text-white rounded-full transition disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
:title="t('zoom_in', 'Zoom In')"
>
<ZoomIn class="w-4 h-4" />
</button>

<div class="h-4 w-px bg-slate-700/80 mx-1"></div>

<button
@click="resetZoom"
class="p-1.5 hover:bg-slate-800 text-slate-300 hover:text-white rounded-full transition cursor-pointer"
:title="t('fit_screen', 'Fit Screen')"
>
<Maximize2 class="w-4 h-4" />
</button>
</div>

<!-- Close button (right) -->
<div class="pointer-events-auto">
<button
@click="close"
class="text-slate-300 hover:text-white bg-slate-900/80 hover:bg-rose-600/80 p-2 rounded-full border border-slate-700/60 transition-all shadow-xl cursor-pointer"
:title="t('close', 'Close (Esc)')"
>
<XIcon class="w-5 h-5" />
</button>
</div>
</div>

<!-- Main Canvas / Image Viewport Container -->
<div
ref="viewportRef"
class="w-full h-full flex items-center justify-center overflow-auto p-4 sm:p-8 pt-16 sm:pt-20 cursor-grab active:cursor-grabbing"
@click.self="close"
@wheel.prevent="handleWheel"
>
<!-- Loading state overlay -->
<div
v-if="isLoading"
class="flex flex-col items-center justify-center bg-slate-900/90 border border-slate-700/60 rounded-2xl px-6 py-5 shadow-2xl backdrop-blur-md"
>
<Loader2 class="w-8 h-8 text-blue-500 animate-spin mb-3" />
<span class="text-slate-200 text-xs font-medium tracking-wide">
{{ t('preview_rendering', 'Rendering high-resolution preview...') }}
</span>
</div>

<!-- Rendered High-Res Image -->
<div
v-else-if="imgSrc"
class="transition-transform duration-150 ease-out flex items-center justify-center shrink-0"
:style="{
transform: `scale(${zoomLevel}) rotate(${rotation || 0}deg)`,
transformOrigin: 'center center'
}"
>
<img
:src="imgSrc"
class="max-h-[85vh] max-w-[90vw] object-contain shadow-2xl bg-white rounded-lg border border-slate-800/40 pointer-events-auto"
alt="PDF Page High-Res Preview"
/>
</div>
</div>
</div>
</Transition>
</template>

<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { X as XIcon, Loader2, ZoomIn, ZoomOut, Maximize2 } from 'lucide-vue-next';
import { t } from '../i18n';

const props = defineProps({
isOpen: Boolean,
isLoading: Boolean,
imgSrc: String,
rotation: {
type: Number,
default: 0
},
title: {
type: String,
default: ''
}
});

const emit = defineEmits(['close']);

const zoomLevel = ref(1.0);
const viewportRef = ref(null);

watch(() => props.isOpen, (newVal) => {
if (newVal) {
zoomLevel.value = 1.0;
}
});

function zoomIn() {
zoomLevel.value = Math.min(3.0, Number((zoomLevel.value + 0.25).toFixed(2)));
}

function zoomOut() {
zoomLevel.value = Math.max(0.5, Number((zoomLevel.value - 0.25).toFixed(2)));
}

function resetZoom() {
zoomLevel.value = 1.0;
}

function handleWheel(e) {
if (e.ctrlKey || e.metaKey) {
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
}
}

function handleKeydown(e) {
if (!props.isOpen) return;
if (e.key === 'Escape') {
close();
} else if (e.key === '+' || e.key === '=') {
zoomIn();
} else if (e.key === '-') {
zoomOut();
} else if (e.key === '0') {
resetZoom();
}
}

onMounted(() => {
window.addEventListener('keydown', handleKeydown);
});

onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown);
});

function close() {
emit('close');
}
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
19 changes: 17 additions & 2 deletions src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1488,5 +1488,20 @@
"next_action_cta_image_to_pdf": "Zu PDF konvertieren ➔",
"next_action_badge_general": "Nahtlose Weiterleitung",
"next_action_desc_general": "Datei im Browserspeicher gehalten. Nahtlose Weiterleitung ohne erneuten Upload.",
"next_action_desc_merge_paginated": "Zusammengefügte Dokumente haben oft lückenhafte Seitenzahlen. Paginieren Sie das Gesamtdokument fortlaufend neu."
}
"next_action_desc_merge_paginated": "Zusammengefügte Dokumente haben oft lückenhafte Seitenzahlen. Paginieren Sie das Gesamtdokument fortlaufend neu.",
"loading_file": "Loading file...",
"loading_default": "Processing...",
"preview_title": "Seiten-HD-Vorschau",
"zoom_in": "Vergrößern",
"zoom_out": "Verkleinern",
"zoom_reset": "Zurücksetzen",
"fit_screen": "An Bildschirm anpassen",
"preview_rendering": "Hochauflösende Vorschau wird gerendert...",
"close": "Schließen (Esc)",
"compress_target_already_fits": "Originalgröße entspricht bereits dem Ziel. Führe verlustfreie Optimierung durch...",
"compress_target_bisection": "Bisektionssuche läuft [Iteration {current}/{total}]...",
"compress_target_resampling": "Seiten werden mit optimaler Auflösung neu gerendert ({current}/{total})...",
"compress_target_finetune": "Feinabstimmung der Skalierungsparameter...",
"compress_target_boost": "Schärfeparameter werden optimiert...",
"compress_target_done": "Zielgrößen-Komprimierung abgeschlossen"
}
19 changes: 17 additions & 2 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1488,5 +1488,20 @@
"next_action_cta_image_to_pdf": "Convert to PDF ➔",
"next_action_badge_general": "Seamless Relay",
"next_action_desc_general": "File held in browser memory. Seamlessly relay to your next task with zero re-uploading.",
"next_action_desc_merge_paginated": "Merged documents often have fragmented page numbering. Re-paginate the combined document sequentially."
}
"next_action_desc_merge_paginated": "Merged documents often have fragmented page numbering. Re-paginate the combined document sequentially.",
"loading_file": "Loading file...",
"loading_default": "Processing...",
"preview_title": "Page HD Preview",
"zoom_in": "Zoom In",
"zoom_out": "Zoom Out",
"zoom_reset": "Reset Zoom",
"fit_screen": "Fit Screen",
"preview_rendering": "Rendering high-resolution preview...",
"close": "Close (Esc)",
"compress_target_already_fits": "Original size already meets target limit. Running lossless optimization...",
"compress_target_bisection": "Bisection search in progress [iteration {current}/{total}]...",
"compress_target_resampling": "Resampling pages at optimal resolution ({current}/{total})...",
"compress_target_finetune": "Fine-tuning scale parameters to strictly meet target limit...",
"compress_target_boost": "Boosting clarity parameters to maximize visual sharpness...",
"compress_target_done": "Target size compression complete"
}
19 changes: 17 additions & 2 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1488,5 +1488,20 @@
"next_action_cta_image_to_pdf": "Convertir a PDF ➔",
"next_action_badge_general": "Relevo continuo",
"next_action_desc_general": "Archivo retenido en memoria. Continúe con la siguiente tarea sin volver a subir.",
"next_action_desc_merge_paginated": "Los documentos combinados a menudo tienen numeración fragmentada. Vuelva a numerar el documento completo de forma secuencial."
}
"next_action_desc_merge_paginated": "Los documentos combinados a menudo tienen numeración fragmentada. Vuelva a numerar el documento completo de forma secuencial.",
"loading_file": "Loading file...",
"loading_default": "Processing...",
"preview_title": "Vista previa HD",
"zoom_in": "Acercar",
"zoom_out": "Alejar",
"zoom_reset": "Restablecer zoom",
"fit_screen": "Ajustar a la pantalla",
"preview_rendering": "Generando vista previa en alta resolución...",
"close": "Cerrar (Esc)",
"compress_target_already_fits": "El tamaño original ya cumple con el objetivo. Ejecutando optimización sin pérdidas...",
"compress_target_bisection": "Búsqueda por bisección en progreso [iteración {current}/{total}]...",
"compress_target_resampling": "Remuestreando páginas con resolución óptima ({current}/{total})...",
"compress_target_finetune": "Ajustando parámetros para cumplir con el límite objetivo...",
"compress_target_boost": "Optimizando parámetros para maximizar la nitidez...",
"compress_target_done": "Compresión a tamaño objetivo completada"
}
19 changes: 17 additions & 2 deletions src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1488,5 +1488,20 @@
"next_action_cta_image_to_pdf": "Convertir en PDF ➔",
"next_action_badge_general": "Relais continu",
"next_action_desc_general": "Fichier conservé en mémoire. Poursuivez votre tâche sans aucun nouveau téléversement.",
"next_action_desc_merge_paginated": "Les documents fusionnés ont souvent une numérotation fragmentée. Re-numérotez le document de manière séquentielle."
}
"next_action_desc_merge_paginated": "Les documents fusionnés ont souvent une numérotation fragmentée. Re-numérotez le document de manière séquentielle.",
"loading_file": "Loading file...",
"loading_default": "Processing...",
"preview_title": "Aperçu HD de la page",
"zoom_in": "Zoom avant",
"zoom_out": "Zoom arrière",
"zoom_reset": "Réinitialiser le zoom",
"fit_screen": "Ajuster à l'écran",
"preview_rendering": "Génération de l'aperçu haute résolution...",
"close": "Fermer (Esc)",
"compress_target_already_fits": "La taille d'origine respecte déjà la cible. Optimisation sans perte en cours...",
"compress_target_bisection": "Recherche par dichotomie en cours [itération {current}/{total}]...",
"compress_target_resampling": "Rééchantillonnage des pages à la résolution optimale ({current}/{total})...",
"compress_target_finetune": "Ajustement fin des paramètres de mise à l'échelle...",
"compress_target_boost": "Amélioration de la netteté visuelle...",
"compress_target_done": "Compression à la taille cible terminée"
}
19 changes: 17 additions & 2 deletions src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1488,5 +1488,20 @@
"next_action_cta_image_to_pdf": "一键拼成 PDF ➔",
"next_action_badge_general": "无缝接力",
"next_action_desc_general": "文件已保留在浏览器内存中,零等待直接流转至下一步处理环节。",
"next_action_desc_merge_paginated": "多个原文件拼接后各章节页码已割裂,建议为新文档一键重新编排连续页码。"
}
"next_action_desc_merge_paginated": "多个原文件拼接后各章节页码已割裂,建议为新文档一键重新编排连续页码。",
"loading_file": "正在加载文件...",
"loading_default": "处理中...",
"preview_title": "页面高清预览",
"zoom_in": "放大",
"zoom_out": "缩小",
"zoom_reset": "适应屏幕",
"fit_screen": "适应屏幕",
"preview_rendering": "正在生成高清预览...",
"close": "关闭 (Esc)",
"compress_target_already_fits": "原文件已符合目标大小,正在执行无损优化...",
"compress_target_bisection": "正在智能推演最佳压缩率 (轮次 {current}/{total})...",
"compress_target_resampling": "正在以最优分辨率重采样页面 ({current}/{total})...",
"compress_target_finetune": "正在微调尺寸参数以严格符合目标大小...",
"compress_target_boost": "正在智能提升清晰度参数以最大化画质...",
"compress_target_done": "目标大小压缩完成!"
}
Loading
Loading