web_ppt / frontend /src /services /dataSyncService.ts
CatPtain's picture
Upload dataSyncService.ts
669cc4d verified
import api from '@/services'
import { debounce } from 'lodash'
// Using html2canvas instead of html-to-image for better compatibility
// import { toJpeg } from 'html-to-image'
class DataSyncService {
private currentPPTId: string | null = null
private saveTimeout: number | null = null
private isOnline = true
private autoSaveDelay = 300000 // Default 5 minutes, configurable
private isInitialized = false
private debouncedSave: any = null
constructor() {
this.setupNetworkMonitoring()
}
// Delayed initialization, called after Pinia is available
async initialize() {
if (this.isInitialized) return
await this.setupAutoSave()
this.isInitialized = true
}
// Set auto-save delay time (milliseconds)
setAutoSaveDelay(delay: number) {
this.autoSaveDelay = Math.max(500, delay) // Minimum 500ms
if (this.isInitialized) {
this.setupAutoSave() // Reset auto-save
}
}
// Get current auto-save delay time
getAutoSaveDelay(): number {
return this.autoSaveDelay
}
// Set current PPT ID
setCurrentPPTId(pptId: string) {
this.currentPPTId = pptId
}
// Auto-save functionality
private async setupAutoSave() {
if (this.debouncedSave) {
this.debouncedSave.cancel()
}
this.debouncedSave = debounce(async () => {
await this.savePPT()
}, this.autoSaveDelay) // Use configurable delay time
// Listen for slides changes
try {
const { useSlidesStore } = await import('@/store')
const slidesStore = useSlidesStore()
slidesStore.$subscribe(() => {
if (this.isOnline && this.currentPPTId) {
this.debouncedSave()
}
})
}
catch (error) {
// console.warn('Unable to set up auto-save, store not ready:', error)
}
}
// Network status monitoring
private setupNetworkMonitoring() {
window.addEventListener('online', () => {
this.isOnline = true
// console.log('Network connected, resuming auto-save')
})
window.addEventListener('offline', () => {
this.isOnline = false
// console.log('Network disconnected, pausing auto-save')
})
}
// Save PPT to backend
async savePPT(force = false): Promise<boolean> {
// Dynamically import store to avoid dependency issues during initialization
const { useAuthStore, useSlidesStore } = await import('@/store')
try {
const authStore = useAuthStore()
const slidesStore = useSlidesStore()
if (!authStore.isLoggedIn) {
// console.warn('User not logged in, unable to save')
return false
}
// If no current PPT ID and forced save, create new PPT
if (!this.currentPPTId && force) {
try {
const response = await api.createPPT(slidesStore.title || 'Untitled Presentation')
this.currentPPTId = response.pptId
// Update slides store data to match newly created PPT
if (response.ppt) {
slidesStore.setSlides(response.ppt.slides)
slidesStore.setTitle(response.ppt.title)
slidesStore.setTheme(response.ppt.theme)
}
// console.log('Created new PPT and saved successfully')
return true
}
catch (createError) {
// console.error('Failed to create new PPT:', createError)
return false
}
}
if (!this.currentPPTId && !force) {
// console.warn('No current PPT ID')
return false
}
const pptData = {
pptId: this.currentPPTId,
title: slidesStore.title,
slides: slidesStore.slides,
theme: slidesStore.theme,
// Add critical size information
viewportSize: slidesStore.viewportSize,
viewportRatio: slidesStore.viewportRatio
}
await api.savePPT(pptData)
// console.log('PPT saved successfully')
return true
}
catch (error) {
// console.error('Failed to save PPT:', error)
return false
}
}
// Create new PPT
async createNewPPT(title: string): Promise<string | null> {
const { useAuthStore } = await import('@/store')
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
throw new Error('User not logged in')
}
try {
const response = await api.createPPT(title)
this.setCurrentPPTId(response.pptId)
return response.pptId
}
catch (error) {
// console.error('Failed to create PPT:', error)
throw error
}
}
// Load PPT
async loadPPT(pptId: string): Promise<boolean> {
const { useAuthStore, useSlidesStore } = await import('@/store')
const authStore = useAuthStore()
const slidesStore = useSlidesStore()
if (!authStore.isLoggedIn) {
throw new Error('User not logged in')
}
try {
// console.log(`๐Ÿ” Starting to load PPT: ${pptId}`)
const pptData = await api.getPPT(pptId)
if (!pptData) {
throw new Error('PPT data is empty')
}
// console.log('๐Ÿ“‹ PPT data loaded successfully, updating store:', {...})
// Fix: Ensure complete PPT data is loaded and set in correct order
// 1. First set viewport information to ensure correct canvas size
if (pptData.viewportSize && pptData.viewportSize > 0) {
slidesStore.setViewportSize(pptData.viewportSize)
// console.log('โœ… Viewport size set successfully:', pptData.viewportSize)
}
else {
// console.log('โš ๏ธ Using default viewport size: 1000')
slidesStore.setViewportSize(1000)
}
if (pptData.viewportRatio && pptData.viewportRatio > 0) {
slidesStore.setViewportRatio(pptData.viewportRatio)
// console.log('โœ… Viewport ratio set successfully:', pptData.viewportRatio)
}
else {
// console.log('โš ๏ธ Using default viewport ratio: 0.5625')
slidesStore.setViewportRatio(0.5625)
}
// 2. Set theme
if (pptData.theme) {
slidesStore.setTheme(pptData.theme)
// console.log('โœ… Theme set successfully')
}
// 3. Set title
slidesStore.setTitle(pptData.title || 'Untitled Presentation')
// console.log('โœ… Title set successfully:', pptData.title)
// 4. Finally set slides, ensuring it is done after viewport information is set
if (Array.isArray(pptData.slides) && pptData.slides.length > 0) {
// Validate slides data integrity
const validSlides = pptData.slides.filter((slide: any) =>
slide && typeof slide === 'object' && Array.isArray(slide.elements)
)
if (validSlides.length !== pptData.slides.length) {
// console.warn(`โš ๏ธ Found ${pptData.slides.length - validSlides.length} invalid slides, filtered out`)
}
slidesStore.setSlides(validSlides)
// console.log('โœ… Slides set successfully:', validSlides.length, 'pages')
}
else {
// console.warn('โš ๏ธ PPT has no valid slides, creating default slide')
slidesStore.setSlides([{
id: 'default-slide',
elements: [],
background: { type: 'solid', color: '#ffffff' }
}])
}
// 5. Set current PPT ID
const actualPptId = pptData.id || pptData.pptId || pptId
this.setCurrentPPTId(actualPptId)
// console.log('โœ… PPT ID set successfully:', actualPptId)
// console.log('๐ŸŽ‰ PPT loaded successfully!')
return true
}
catch (error: any) {
// console.error('โŒ Failed to load PPT:', { pptId, error: error.message, stack: error.stack })
// Provide more specific error information
if (error.message?.includes('404') || error.message?.includes('not found')) {
throw new Error(`PPT not found (ID: ${pptId})`)
}
if (error.message?.includes('403') || error.message?.includes('unauthorized')) {
throw new Error('No access permission')
}
if (error.message?.includes('Network')) {
throw new Error('Network connection failed, please check network status')
}
throw new Error(`Failed to load: ${error.message}`)
}
}
// Get PPT list
async getPPTList() {
const { useAuthStore } = await import('@/store')
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
throw new Error('User not logged in')
}
return await api.getPPTList()
}
// Delete PPT
async deletePPT(pptId: string): Promise<boolean> {
const { useAuthStore } = await import('@/store')
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
throw new Error('User not logged in')
}
try {
await api.deletePPT(pptId)
// If the deleted PPT is the current PPT, clear the current PPT ID
if (this.currentPPTId === pptId) {
this.currentPPTId = null
}
// console.log('PPT deleted successfully')
return true
}
catch (error) {
// console.error('Failed to delete PPT:', error)
throw error
}
}
// Generate share link
async generateShareLink(slideIndex = 0) {
const { useAuthStore } = await import('@/store')
const authStore = useAuthStore()
if (!authStore.isLoggedIn || !this.currentPPTId) {
throw new Error('User not logged in or no current PPT')
}
try {
const response = await api.generateShareLink(
authStore.currentUser!.id,
this.currentPPTId,
slideIndex
)
return response
}
catch (error) {
// console.error('Failed to generate share link:', error)
throw error
}
}
// Manual save
async manualSave(): Promise<boolean> {
return await this.savePPT(true)
}
// New screenshot solution: use direct image API
async generateScreenshotUrl(slideIndex = 0, options: any = {}) {
const { useAuth = true, format = 'jpeg', quality = 90 } = options;
if (useAuth) {
const { useAuthStore } = await import('@/store')
const authStore = useAuthStore()
if (!authStore.isLoggedIn || !this.currentPPTId) {
throw new Error('User not logged in or no current PPT')
}
const baseUrl = window.location.origin
// ๐Ÿ”ง NEW: Use frontend screenshot data API for direct generation
console.log('๐Ÿš€ Using new frontend screenshot strategy...')
try {
// Get PPT data for frontend generation
const dataResponse = await fetch(`${baseUrl}/api/public/screenshot-data/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`)
if (dataResponse.ok) {
const screenshotData = await dataResponse.json()
// Return data for frontend direct generation
return {
type: 'frontend-data',
data: screenshotData,
isDataUrl: false,
info: {
format,
quality,
strategy: 'frontend-direct-generation',
pptTitle: screenshotData.pptData?.title,
slideIndex: screenshotData.slideIndex,
dimensions: `${screenshotData.exportConfig?.width}x${screenshotData.exportConfig?.height}`
}
}
}
} catch (error) {
console.warn('Frontend data API failed, falling back to export page:', error)
}
// Fallback: Frontend export page (user opens in new tab)
const frontendExportUrl = `${baseUrl}/api/public/image/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}?format=${format}&quality=${quality}`
return {
type: 'frontend-export-page',
url: frontendExportUrl,
isDataUrl: false,
info: {
format,
quality,
strategy: 'frontend-export-page',
usage: 'Open this URL in a new tab to generate and download screenshot'
}
}
} else {
// Public access mode
const baseUrl = window.location.origin
return {
type: 'frontend-export-page-public',
url: `${baseUrl}/api/public/image/public/public/${slideIndex}?format=${format}&quality=${quality}`,
isDataUrl: false
}
}
}
// Generate image and upload to backend for external link access
async generateAndUploadImage(slideIndex = 0, options: any = {}) {
const { format = 'jpeg', quality = 90, elementSelector = '.slide-viewport, .canvas-viewport, .viewport-wrapper' } = options;
try {
const { useAuthStore, useSlidesStore } = await import('@/store')
const authStore = useAuthStore()
const slidesStore = useSlidesStore()
if (!authStore.isLoggedIn || !this.currentPPTId) {
throw new Error('User not logged in or no current PPT')
}
// Find target element for screenshot
let targetElement: HTMLElement | null = null
const selectors = elementSelector.split(',').map((s: string) => s.trim())
for (const selector of selectors) {
targetElement = document.querySelector(selector) as HTMLElement
if (targetElement) {
console.log(`โœ… Found screenshot target element: ${selector}`)
break
}
}
if (!targetElement) {
throw new Error(`Screenshot target element not found. Tried selectors: ${selectors.join(', ')}`)
}
// Dynamically load html2canvas
const html2canvas = await this.loadHtml2Canvas()
console.log('๐Ÿ–ผ๏ธ Starting to generate image for upload...', { format, quality })
// Wait for fonts and images to load
await this.ensureResourcesReady()
// Generate screenshot
const canvas = await html2canvas(targetElement, {
scale: 2, // High resolution
useCORS: true,
allowTaint: true,
backgroundColor: slidesStore.theme?.backgroundColor || '#ffffff',
logging: false,
onclone: function(clonedDoc) {
// Ensure correct fonts in cloned document
const clonedElements = clonedDoc.querySelectorAll('*')
clonedElements.forEach((el: any) => {
if (el.style) {
el.style.fontFamily = 'Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, Helvetica, sans-serif'
}
})
}
})
// Convert to specified format
const imageDataUrl = canvas.toDataURL(`image/${format}`, quality / 100)
console.log('โœ… Image generated, uploading to backend...')
// Upload to backend
const baseUrl = window.location.origin
const uploadResponse = await fetch(`${baseUrl}/api/public/save-image/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
imageData: imageDataUrl,
format,
quality
})
})
if (!uploadResponse.ok) {
throw new Error(`Upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`)
}
const uploadResult = await uploadResponse.json()
console.log('โœ… Image uploaded successfully, external URL:', uploadResult.imageUrl)
return {
success: true,
imageUrl: uploadResult.imageUrl,
imageId: uploadResult.imageId,
expiresAt: uploadResult.expiresAt,
format,
quality,
width: canvas.width,
height: canvas.height
}
} catch (error: any) {
console.error('โŒ Failed to generate and upload image:', error)
throw error
}
}
// Get PPT screenshot data (for frontend direct rendering)
async getScreenshotData(slideIndex = 0) {
const { useAuthStore } = await import('@/store')
const authStore = useAuthStore()
if (!authStore.isLoggedIn || !this.currentPPTId) {
throw new Error('User not logged in or no current PPT')
}
try {
const baseUrl = window.location.origin
const response = await fetch(`${baseUrl}/api/public/screenshot-data/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`)
if (!response.ok) {
throw new Error(`Failed to get screenshot data: ${response.status}`)
}
const data = await response.json()
return data
} catch (error: any) {
console.error('โŒ Failed to get screenshot data:', error)
throw new Error(`Failed to get screenshot data: ${error.message}`)
}
}
// Generate screenshot directly in current page (best solution)
async generateDirectScreenshot(slideIndex = 0, options: any = {}) {
try {
const { useSlidesStore } = await import('@/store')
const slidesStore = useSlidesStore()
// Check if slide data is available
if (!slidesStore.slides || slideIndex >= slidesStore.slides.length) {
throw new Error('Invalid slide index or no slides available')
}
const {
format = 'jpeg',
quality = 90,
width = slidesStore.viewportSize || 1000,
height = Math.ceil((slidesStore.viewportSize || 1000) * (slidesStore.viewportRatio || 0.562)),
elementSelector = '.canvas-main, .slide-container, .editor-main'
} = options
// Find screenshot target element
let targetElement: HTMLElement | null = null
const selectors = elementSelector.split(',').map((s: string) => s.trim())
for (const selector of selectors) {
targetElement = document.querySelector(selector) as HTMLElement
if (targetElement) {
console.log(`โœ… Found screenshot target element: ${selector}`)
break
}
}
if (!targetElement) {
throw new Error(`Screenshot target element not found. Tried selectors: ${selectors.join(', ')}`)
}
// Dynamically load html2canvas
const html2canvas = await this.loadHtml2Canvas()
console.log('๐Ÿ–ผ๏ธ Starting to generate direct screenshot...', { width, height, quality })
// Wait for fonts and images to load
await this.ensureResourcesReady()
// Generate screenshot
const canvas = await html2canvas(targetElement, {
width,
height,
scale: 2, // High resolution
useCORS: true,
allowTaint: true,
backgroundColor: slidesStore.theme?.backgroundColor || '#ffffff',
logging: false,
onclone: function(clonedDoc) {
// Ensure correct fonts in cloned document
const clonedElements = clonedDoc.querySelectorAll('*')
clonedElements.forEach((el: any) => {
if (el.style) {
el.style.fontFamily = 'Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, Helvetica, sans-serif'
}
})
}
})
// Convert to specified format
const imageDataUrl = canvas.toDataURL(`image/${format}`, quality / 100)
console.log('โœ… Direct screenshot generated successfully, size:', imageDataUrl.length)
return {
type: 'direct-frontend',
url: imageDataUrl,
isDataUrl: true,
width: canvas.width,
height: canvas.height,
format,
quality
}
} catch (error: any) {
console.error('โŒ Failed to generate direct screenshot:', error)
// Fallback to frontend export page solution
console.log('๐Ÿ”„ Falling back to frontend export page solution...')
return await this.generateScreenshotUrl(slideIndex, options)
}
}
// Dynamically load html2canvas library
private async loadHtml2Canvas(): Promise<any> {
// Check if already loaded
if ((window as any).html2canvas) {
return (window as any).html2canvas
}
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js'
script.onload = () => {
resolve((window as any).html2canvas)
}
script.onerror = (error) => {
reject(new Error('Failed to load html2canvas library'))
}
document.head.appendChild(script)
})
}
// Wait for resources to be ready
private async ensureResourcesReady(): Promise<void> {
// Wait for font loading
if (document.fonts && document.fonts.ready) {
await document.fonts.ready
}
// Wait for image loading
const images = document.querySelectorAll('img')
const imagePromises = Array.from(images).map(img => {
if (img.complete) return Promise.resolve()
return new Promise((resolve) => {
img.onload = () => resolve(void 0)
img.onerror = () => resolve(void 0) // Continue even if failed
setTimeout(() => resolve(void 0), 2000) // 2 second timeout
})
})
await Promise.all(imagePromises)
// Additional wait to ensure rendering is complete
await new Promise(resolve => setTimeout(resolve, 300))
}
// Simplified frontend screenshot generation (for compatibility)
async generateFrontendScreenshot(elementId = 'slideContainer', options: any = {}) {
console.warn('โš ๏ธ generateFrontendScreenshot is deprecated, use generateDirectScreenshot instead')
return await this.generateDirectScreenshot(0, { ...options, elementSelector: `#${elementId}` })
}
// Ensure element is fully rendered (for compatibility)
ensureElementReady(element: HTMLElement) {
console.warn('โš ๏ธ ensureElementReady is deprecated, now using ensureResourcesReady')
return this.ensureResourcesReady()
}
// Generate frontend fallback screenshot (for compatibility)
generateFallbackScreenshot(width = 1000, height = 562) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) return ''
// Draw background
const gradient = ctx.createLinearGradient(0, 0, width, height)
gradient.addColorStop(0, '#f8f9fa')
gradient.addColorStop(1, '#e9ecef')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, width, height)
// Draw border
ctx.strokeStyle = '#dee2e6'
ctx.lineWidth = 3
ctx.setLineDash([15, 10])
ctx.strokeRect(20, 20, width - 40, height - 40)
// Optimized font rendering
ctx.fillStyle = '#495057'
ctx.font = 'bold 32px "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Noto Sans SC", "WenQuanYi Micro Hei", "SimHei", "SimSun", Arial, sans-serif'
ctx.textAlign = 'center'
ctx.fillText('PPT Preview', width / 2, height * 0.35)
ctx.fillStyle = '#6c757d'
ctx.font = '18px "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Noto Sans SC", "WenQuanYi Micro Hei", "SimHei", "SimSun", Arial, sans-serif'
ctx.fillText('Frontend Screenshot Service', width / 2, height * 0.5)
ctx.fillStyle = '#adb5bd'
ctx.font = '16px "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Noto Sans SC", "WenQuanYi Micro Hei", "SimHei", "SimSun", Arial, sans-serif'
ctx.fillText(`Size: ${width} ร— ${height}`, width / 2, height * 0.6)
return canvas.toDataURL('image/jpeg', 0.9)
}
}
// Create singleton instance
export const dataSyncService = new DataSyncService()
export default dataSyncService