diff --git a/app/components/AttachmentItem.vue b/app/components/AttachmentItem.vue
index fffc42a..7b507ba 100644
--- a/app/components/AttachmentItem.vue
+++ b/app/components/AttachmentItem.vue
@@ -63,6 +63,23 @@ const imageLoaded = ref(false)
const router = useRouter()
function openExternally() {
+ // Capture image position for transition
+ const img = event?.target as HTMLImageElement
+ if (img && itemType.value === 'image') {
+ const rect = img.getBoundingClientRect()
+ const transitionData = {
+ src: remoteSource.value,
+ x: rect.left,
+ y: rect.top,
+ width: rect.width,
+ height: rect.height,
+ aspectRatio: aspectRatio.value
+ }
+
+ // Store transition data
+ sessionStorage.setItem('imageTransition', JSON.stringify(transitionData))
+ }
+
router.push('/files/' + props.item.id)
}
diff --git a/app/pages/files/[id].vue b/app/pages/files/[id].vue
index 9b56e35..00d0f40 100644
--- a/app/pages/files/[id].vue
+++ b/app/pages/files/[id].vue
@@ -133,6 +133,19 @@
+
+
+
+
![Transitioning image]()
+
@@ -144,10 +157,6 @@ import { downloadAndDecryptFile } from "./secure"
import { formatBytes } from "./format"
import type { SnCloudFile } from "~/types/api/post"
-useHead({
- title: computed(() => fileInfo.value?.name ? `${fileInfo.value.name} - File Preview` : 'File Preview')
-})
-
const route = useRoute()
const error = ref(null)
@@ -161,9 +170,18 @@ const infoDialog = ref(false)
const secretDialog = ref(false)
const dialogPassword = ref("")
+// View transition state
+const isTransitioning = ref(false)
+const transitionImage = ref("")
+const transitionStyle = ref>({})
+
const api = useSolarNetwork()
const fileInfo = ref(null)
+
+useHead({
+ title: computed(() => fileInfo.value?.name ? `${fileInfo.value.name} - File Preview` : 'File Preview')
+})
async function fetchFileInfo() {
try {
let url = "/drive/files/" + fileId + "/info"
@@ -176,7 +194,61 @@ async function fetchFileInfo() {
error.value = (err as Error).message
}
}
-onMounted(() => fetchFileInfo())
+
+function checkForTransition() {
+ const transitionData = sessionStorage.getItem('imageTransition')
+ if (transitionData) {
+ try {
+ const data = JSON.parse(transitionData)
+ isTransitioning.value = true
+ transitionImage.value = data.src
+
+ // Calculate final position (centered in viewport)
+ const viewportWidth = window.innerWidth
+ const viewportHeight = window.innerHeight
+ const finalWidth = Math.min(viewportWidth * 0.9, data.width * (viewportHeight / data.height))
+ const finalHeight = finalWidth / data.aspectRatio
+ const finalX = (viewportWidth - finalWidth) / 2
+ const finalY = (viewportHeight - finalHeight) / 2
+
+ // Set initial position (from original image location)
+ transitionStyle.value = {
+ position: 'fixed',
+ top: `${data.y}px`,
+ left: `${data.x}px`,
+ width: `${data.width}px`,
+ height: `${data.height}px`,
+ zIndex: 9999,
+ transition: 'all 0.3s ease-out'
+ }
+
+ // Animate to final position
+ requestAnimationFrame(() => {
+ transitionStyle.value = {
+ ...transitionStyle.value,
+ top: `${finalY}px`,
+ left: `${finalX}px`,
+ width: `${finalWidth}px`,
+ height: `${finalHeight}px`
+ }
+
+ // Hide transition after animation
+ setTimeout(() => {
+ isTransitioning.value = false
+ sessionStorage.removeItem('imageTransition')
+ }, 300)
+ })
+ } catch (error) {
+ console.warn('Failed to parse transition data:', error)
+ sessionStorage.removeItem('imageTransition')
+ }
+ }
+}
+
+onMounted(() => {
+ fetchFileInfo()
+ checkForTransition()
+})
const apiBase = useSolarNetworkUrl()
@@ -349,4 +421,17 @@ definePageMeta({
.top-toolbar :deep(.v-app-bar__content) {
padding: 0;
}
+
+/* View transition styles */
+.transition-overlay {
+ pointer-events: none;
+ z-index: 9999;
+}
+
+.transition-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+}