Files
pureRef/preload.js
小煜 ffb387bd20 feat: 实现撤销重做功能并优化剪贴板处理
添加撤销/重做历史记录功能,支持Ctrl+Z/Y/Shift+Z快捷键
优化剪贴板处理逻辑,支持GIF动画和多文件复制
更新帮助文档和图标资源,完善打包配置
2025-12-17 22:13:10 +08:00

1358 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const {
ipcRenderer,
contextBridge,
ipcMain
} = require('electron')
const addEvent = function (el, type, fn) {
if (el.addEventListener)
el.addEventListener(type, fn, false);
else
el.attachEvent('on' + type, fn);
};
const extend = function (obj, ext) {
for (var key in ext)
if (ext.hasOwnProperty(key))
obj[key] = ext[key];
return obj;
};
const interact = require('./interact.min.js')
let state = {
mode: 'init', // 'init', 'standard', 'edit-video'
editVideo: {
},
currentScale: 1,
translate: {
translateX: 0,
translateY: 0
},
elements: [
],
workspaceRect: {
x1: 0,
y1: 0,
x2: 0,
y2: 0
},
isLocked: false
}
// 撤销/重做历史记录
let history = {
undoStack: [],
redoStack: [],
maxSize: 50
}
// 保存当前状态到历史记录
function saveStateToHistory() {
// 创建状态快照(深拷贝)
const snapshot = {
elements: state.elements.map(el => ({
path: el.path,
type: el.type,
x: parseFloat(el.element.dataset.x) || 0,
y: parseFloat(el.element.dataset.y) || 0,
width: el.width,
height: el.height,
loopPairs: el.loopPairs,
activeLoopPair: el.activeLoopPair
})),
currentScale: state.currentScale,
translate: { ...state.translate }
}
history.undoStack.push(snapshot)
// 限制历史记录大小
if (history.undoStack.length > history.maxSize) {
history.undoStack.shift()
}
// 清空重做栈
history.redoStack = []
console.log('State saved to history, undo stack size:', history.undoStack.length)
}
// 撤销
function undo() {
if (history.undoStack.length === 0) {
console.log('Nothing to undo')
return
}
// 保存当前状态到重做栈
const currentSnapshot = {
elements: state.elements.map(el => ({
path: el.path,
type: el.type,
x: parseFloat(el.element.dataset.x) || 0,
y: parseFloat(el.element.dataset.y) || 0,
width: el.width,
height: el.height,
loopPairs: el.loopPairs,
activeLoopPair: el.activeLoopPair
})),
currentScale: state.currentScale,
translate: { ...state.translate }
}
history.redoStack.push(currentSnapshot)
// 恢复上一个状态
const snapshot = history.undoStack.pop()
restoreStateFromSnapshot(snapshot)
console.log('Undo performed, undo stack:', history.undoStack.length, 'redo stack:', history.redoStack.length)
}
// 重做
function redo() {
if (history.redoStack.length === 0) {
console.log('Nothing to redo')
return
}
// 保存当前状态到撤销栈
const currentSnapshot = {
elements: state.elements.map(el => ({
path: el.path,
type: el.type,
x: parseFloat(el.element.dataset.x) || 0,
y: parseFloat(el.element.dataset.y) || 0,
width: el.width,
height: el.height,
loopPairs: el.loopPairs,
activeLoopPair: el.activeLoopPair
})),
currentScale: state.currentScale,
translate: { ...state.translate }
}
history.undoStack.push(currentSnapshot)
// 恢复重做状态
const snapshot = history.redoStack.pop()
restoreStateFromSnapshot(snapshot)
console.log('Redo performed, undo stack:', history.undoStack.length, 'redo stack:', history.redoStack.length)
}
// 从快照恢复状态
function restoreStateFromSnapshot(snapshot) {
// 清除当前所有元素
state.elements.forEach(el => el.element.remove())
state.elements = []
// 恢复缩放和平移
updateScaleAndTranslate(snapshot.currentScale, snapshot.translate)
// 恢复所有元素
snapshot.elements.forEach(elementData => {
addMediaWithPath(elementData.path, elementData.type, elementData)
})
}
let videoExample = {
loopPairs: [[0, 119], [44, 56]], // can derive A, B, C & color coding from index
activeLoopPair: 0
}
function closeEditVideo() {
//state.editVideo.videoElement.removeEventListener('timeupdate', onPlayerProgress)
state.mode = 'standard'
updateScaleAndTranslate(state.editVideo.backupState.currentScale, state.editVideo.backupState.translate)
state.editVideo.video.element.className = state.editVideo.backupState.elementClasses
document.querySelector('#root').classList.remove('disableBorder')
document.querySelector('#editVideoTools').classList.add('hide')
state.editVideo = {}
}
function editVideo(video) {
console.log('video', video)
state.mode = 'edit-video'
document.querySelector('#root').style.cursor = ""
state.editVideo.video = video
state.editVideo.backupState = {
currentScale: state.currentScale,
translate: Object.assign({}, state.translate),
elementClasses: video.element.className
}
updateScaleAndTranslate(1, {
translateX: 0,
translateY: 0
})
video.element.className = "editVideo"
document.querySelector('#root').classList.add('disableBorder')
document.querySelector('#editVideoTools').classList.remove('hide')
document.getElementById('eventTrigger').dataset.changesliders = JSON.stringify(video.loopPairs[video.activeLoopPair])
if (video.type == 'youtube') {
state.editVideo.videoElement = document.querySelector('.editVideo iframe').contentDocument.querySelector('video')
} else {
state.editVideo.videoElement = document.querySelector('.editVideo')
}
//state.editVideo.videoElement.addEventListener('timeupdate', onPlayerProgress)
//
console.log(video)
}
function onPlayerProgress(e) {
//am i editvideo?
// handle edit stuff
let sliderPositions = document.querySelectorAll('.noUi-handle')
let leftSliderPercent = Math.min(sliderPositions[0]['ariaValueText'], sliderPositions[1]['ariaValueText'])
let rightSliderPercent = Math.max(sliderPositions[0]['ariaValueText'], sliderPositions[1]['ariaValueText'])
//state.editVideo.video
let sliderElement = document.querySelector('#slider')
let currentTimePercent = (this.currentTime / this.duration * 100)
if (this != state.editVideo.videoElement) {
let leftPercent = parseFloat(this.dataset.loopLeft)
let rightPercent = parseFloat(this.dataset.loopRight)
//console.log('im not edit video',leftPercent, rightPercent)
if (leftPercent > currentTimePercent) {
this.currentTime = percentToCurrentTime(leftPercent, this.duration)
currentTimePercent = (this.currentTime / this.duration * 100)
}
if (rightPercent < currentTimePercent) {
this.currentTime = percentToCurrentTime(leftPercent, this.duration)
currentTimePercent = (this.currentTime / this.duration * 100)
}
return;
}
//console.log('im edit video')
state.editVideo.video.loopPairs[state.editVideo.video.activeLoopPair][0] = leftSliderPercent
state.editVideo.video.loopPairs[state.editVideo.video.activeLoopPair][1] = rightSliderPercent
this.dataset.loopLeft = leftSliderPercent
this.dataset.loopRight = rightSliderPercent
//console.log(state.editVideo.video.loopPairs, leftSliderPercent, rightSliderPercent)
if (sliderElement.classList.contains('noUi-state-drag')) {
dragPercent = parseFloat(sliderElement.querySelector('.noUi-active')['ariaValueNow'])
this.currentTime = percentToCurrentTime(dragPercent, this.duration)
} else {
if (leftSliderPercent > currentTimePercent) {
this.currentTime = percentToCurrentTime(leftSliderPercent, this.duration)
currentTimePercent = (this.currentTime / this.duration * 100)
}
if (rightSliderPercent < currentTimePercent) {
//.noUi-state-drag
if (sliderElement.classList.contains('noUi-state-drag')) {
this.currentTime = percentToCurrentTime(rightSliderPercent, this.duration)
} else
this.currentTime = percentToCurrentTime(leftSliderPercent, this.duration)
currentTimePercent = (this.currentTime / this.duration * 100)
}
}
let progressBarWidth = sliderElement.getBoundingClientRect().width // 10
let progressBarTimePosition = progressBarWidth * (currentTimePercent / 100) // 4
// transform: translateX(41.4966%);
document.getElementById('progressbar').style.transform = `translateX(${progressBarTimePosition}px)`
//sliderElement.style.background = `linear-gradient(90deg, rgba(78,47,102,1) 0%, rgba(91,52,122,1) ${progressBarTimePosition}px, rgba(113,80,136,1) ${progressBarTimePosition}px, rgba(121,76,157,1) 100%)`
}
function percentToCurrentTime(percent, duration) {
if (percent >= 100) return duration
if (percent <= 0) return 0
let ct = (percent * duration) / 100
console.log("percentToCurrentTime", ct)
return ct
}
function newScene() {
document.getElementById('itemHolder').innerHTML = ''
updateScaleAndTranslate(1, {
translateX: 0,
translateY: 0
});
initWorkspace()
refreshWorkspace()
state.elements = []
document.querySelector('#welcome').classList.remove("hide")
}
function loadState(loadedState, filePath) {
// file stuff
if (state.mode == 'init')
init();
newScene()
updateScaleAndTranslate(loadedState.currentScale, loadedState.translate)
setTimeout(function () {
for (var i in loadedState.elements) {
addMediaWithPath(loadedState.elements[i].path, loadedState.elements[i].type, loadedState.elements[i])
}
ipcRenderer.send('loaded-state', filePath)
}, 1000);
}
document.addEventListener('keydown', evt => {
if (state.isLocked) return
mouseObj.keys[evt.key] = true
if (evt.key === 'Escape') {
// 取消选择框
if (mouseObj.selecting) {
hideSelectionBox()
}
} else if (evt.key === 'h' && evt.ctrlKey) {
evt.preventDefault()
ipcRenderer.send('show-help')
console.log('Ctrl+H was pressed')
} else if (evt.key === 'c' && evt.ctrlKey) {
evt.preventDefault()
copySelected()
console.log('Ctrl+C was pressed')
} else if (evt.key === 'z' && evt.ctrlKey && !evt.shiftKey) {
evt.preventDefault()
undo()
console.log('Ctrl+Z was pressed (Undo)')
} else if (evt.key === 'y' && evt.ctrlKey) {
evt.preventDefault()
redo()
console.log('Ctrl+Y was pressed (Redo)')
} else if (evt.key === 'z' && evt.ctrlKey && evt.shiftKey) {
evt.preventDefault()
redo()
console.log('Ctrl+Shift+Z was pressed (Redo)')
} else if (evt.key === 'Delete') {
console.log('delete selected')
deleteSelected()
} else if (evt.key === 'v' && evt.ctrlKey) {
evt.preventDefault()
ipcRenderer.send('handle-paste')
console.log('Ctrl+V was pressed');
} else if (evt.key === 'l' && evt.ctrlKey) {
evt.preventDefault()
ipcRenderer.send('trigger-load')
console.log('Ctrl+L was pressed');
} else if (evt.key === 's' && evt.ctrlKey) {
evt.preventDefault()
ipcRenderer.send('trigger-save')
console.log('Ctrl+S was pressed');
} else if (evt.key === 'n' && evt.ctrlKey) {
evt.preventDefault()
newScene()
console.log('Ctrl+N was pressed');
} else if (evt.key === 'w' && evt.ctrlKey) {
evt.preventDefault()
ipcRenderer.send('trigger-close')
console.log('Ctrl+W was pressed');
} else if (evt.key === 'f' && evt.ctrlKey) {
evt.preventDefault()
ipcRenderer.send('trigger-maximize')
console.log('Ctrl+F was pressed');
} else if (evt.key === 'm' && evt.ctrlKey) {
evt.preventDefault()
ipcRenderer.send('trigger-minimize')
console.log('Ctrl+M was pressed');
} else if (evt.key === ' ' && evt.ctrlKey) {
mouseObj.ctrlSpace = true;
console.log('Ctrl+space was pressed');
} else if (evt.key === ' ') {
mouseObj.space = true;
}
});
document.addEventListener('keyup', evt => {
mouseObj.keys[evt.key] = false
console.log('keyup', evt.key);
if (evt.key === ' ' || evt.key == 'Control') {
mouseObj.ctrlSpace = false;
console.log('Ctrl+space was released');
if (evt.key === ' ') {
mouseObj.space = false;
}
}
})
function getSelected() {
if (document.querySelector('.editVideo')) return { type: 'edit-video' }
let lastIndex = state.elements.length - 1
if (!state.elements[lastIndex] || !state.elements[lastIndex].element.classList.contains('selectedItem')) return null
return state.elements[lastIndex]
}
document.addEventListener('contextmenu', (e) => {
e.preventDefault()
console.log(state)
if (mouseObj.dragging) {
mouseObj.dragging = false;
return;
}
})
let mouseObj = {
initPos: null,
initClientPos: null,
dragging: false,
ctrlSpace: false,
space: false,
keys: new Object(),
//time dragging & distance dragged
selecting: false,
selectionStart: null
}
document.addEventListener('mousedown', (e) => {
if (state.isLocked) return
mouseObj.initPos = { x: e.clientX, y: e.clientY }
mouseObj.initClientPos = { x: e.screenX, y: e.screenY }
mouseObj.initTranslate = { x: (parseFloat(document.body.dataset.translateX) || 0), y: (parseFloat(document.body.dataset.translateY) || 0) }
ipcRenderer.send('record-window-size', window.innerHeight, window.innerHeight)
mouseObj.dragging = false
// 左键点击空白区域开始选择(不按任何修饰键)
if (e.button === 0 && !mouseObj.keys[' '] && !mouseObj.keys['Control'] &&
(e.target.id === 'root' || e.target.id === 'workspaceBox' || e.target.id === 'itemHolder')) {
mouseObj.selecting = true
mouseObj.selectionStart = { x: e.clientX, y: e.clientY }
}
})
document.addEventListener('mousemove', (e) => {
if (state.isLocked) return
// 处理选择框拖动
if (mouseObj.selecting && e.buttons === 1) {
updateSelectionBox(e.clientX, e.clientY)
e.preventDefault()
return
}
// ctrl + space + mouse drag = zoom
if (e.buttons == 1 && mouseObj.keys[' '] && mouseObj.keys['Control']) {
//console.log(`handleZoom(${e.movementX})`);
handleZoom(e.movementX, mouseObj.initPos.x, mouseObj.initPos.y)
e.preventDefault()
} else if (e.buttons == 1 && mouseObj.space) {
e.preventDefault()
handleMove(e.movementX, e.movementY)
} else if (e.buttons == 4) { // middle mouse drag = move
e.preventDefault()
handleMove(e.movementX, e.movementY)
} else if (e.buttons == 2) {
e.preventDefault()
ipcRenderer.send('move-electron-window', e.screenX, e.screenY, mouseObj.initPos)
mouseObj.dragging = true;
}
})
function handleMove(dX, dY) {
currentScale = parseFloat(document.body.dataset.currentScale) || 1
let translateX = (parseFloat(document.body.dataset.translateX) || 0);
let translateY = (parseFloat(document.body.dataset.translateY) || 0);//(parseFloat(document.body.dataset.translateY) || 0);
translateX += dX
translateY += dY //- mouseObj.initTranslate.y
updateScaleAndTranslate(currentScale, { translateX, translateY })
}
function handleZoom(_delta, clientX, clientY) {
if (document.querySelector('.editVideo')) return;
currentScale = parseFloat(document.body.dataset.currentScale) || 1
let delta = _delta / 60
const nextScale = Math.max(currentScale + delta * (currentScale / 2), 0.01)
const ratio = 1 - nextScale / currentScale
let translateX = (parseFloat(document.body.dataset.translateX) || 0);
let translateY = (parseFloat(document.body.dataset.translateY) || 0);
translateX += (clientX - translateX) * ratio
translateY += (clientY - translateY) * ratio
currentScale = nextScale
updateScaleAndTranslate(currentScale, { translateX, translateY })
//zoom(nextScale, e)
}
document.addEventListener('mouseup', (e) => {
if (state.isLocked) return
// 处理选择框结束
if (mouseObj.selecting) {
finishSelection()
hideSelectionBox()
if (e.button === 0) {
return
}
}
console.log(e.button, mouseObj.dragging)
if (e.button == 2) {
if (mouseObj.dragging) {
distance = Math.sqrt(
Math.pow(e.screenX - mouseObj.initClientPos.x, 2)
+
Math.pow(e.screenY - mouseObj.initClientPos.y, 2));
console.log(distance)
if (distance < 4) {
ipcRenderer.send('show-context-menu', getSelected()?.type || "void")
}
mouseObj.dragging = false;
} else {
ipcRenderer.send('show-context-menu', getSelected()?.type || "void")
}
}
})
ipcRenderer.on('close-edit-video', (event, newState) => {
closeEditVideo()
})
ipcRenderer.on('edit-video', (event, newState) => {
editVideo(getSelected())
})
ipcRenderer.on('new-scene', (event) => {
newScene()
})
ipcRenderer.on('load-scene', (event, newState, filePath) => {
loadState(newState, filePath)
})
ipcRenderer.on('lock-window', (event) => {
state.isLocked = true
document.body.classList.add('locked')
clearAllSelected() // 取消所有选中的对象
})
ipcRenderer.on('unlock-window', (event) => {
state.isLocked = false
document.body.classList.remove('locked')
})
ipcRenderer.on('save-scene', (event, filePath) => {
var stateCopy = JSON.parse(JSON.stringify(state));
for (var i = 0; i < stateCopy.elements.length; i++) {
//stateCopy.elements.push()
delete stateCopy.elements[i].element;
}
console.log(stateCopy)
ipcRenderer.send('save-scene', filePath, stateCopy)
})
ipcRenderer.on('clipboard', (event, msg) => {
let payload = JSON.parse(msg);
console.log(payload)
if (/youtube.com\/.*v=([^\?]*)/.test(payload[payload.type])) {
addMediaWithPath(payload[payload.type], "youtube")
} else
addMediaWithPath(payload[payload.type], payload.type)
})
function getCenterOfWindowScaled() {
const width = window.innerWidth;
const height = window.innerHeight;
const widthScaled = (width / 2) / state.currentScale;
const heightScaled = (height / 2) / state.currentScale;
return {
centerX: (-state.translate.translateX / state.currentScale) + widthScaled,
centerY: (-state.translate.translateY / state.currentScale) + heightScaled
};
}
function addMediaWithPath(path, type = 'img', loadedState) {
isNewElement = loadedState == null
let centerWin = getCenterOfWindowScaled();
loadedState = loadedState || { x: centerWin.centerX, y: centerWin.centerY, width: null, height: null }
if (state.mode == 'init') init();
if (!document.querySelector('#welcome').classList.contains("hide"))
document.querySelector('#welcome').classList.add("hide");
let itemHolder = document.getElementById('itemHolder')
let mediaElement = undefined
if (type == 'img' || type == 'dataURL' || type == 'filePath') {
mediaElement = document.createElement('img')
if (isNewElement)
mediaElement.style.opacity = 0;
mediaElement.addEventListener('load', function loaded() {
let { x, y, width, height } = this.getClientRects()[0]
if (isNewElement) {
//loadedState.x = centerWin.centerX - width / 2
//loadedState.y = centerWin.centerY - height / 2
mediaElement.style.opacity = 1;
setTransformForElement(mediaElement.dataset.zIndex, -width / 2, -height / 2, width, height)
}
resizeWorkspaceToFitObj(loadedState.x, loadedState.y, width, height)
})
mediaElement.src = path;
} else if (type == 'video') {
mediaElement = document.createElement('video')
mediaElement.autoplay = true;
mediaElement.loop = true;
mediaElement.muted = true;
let srcElement = document.createElement('source')
srcElement.src = path;
mediaElement.appendChild(srcElement)
} else if (type == 'youtube') {
//mediaElement = document.createElement('iframe')
if (/youtube.com\/.*v=([^\?]*)/.test(path)) {
var code = extractYoutubeId(path)
if (code == null) return;
mediaElement = document.createElement('div')
mediaElement.classList.add('youtubePlayer')
mediaElement.classList.add('playerNeedsSetup')
mediaElement.dataset.idcode = code
mediaElement.style.width = (loadedState.width || 640) + "px";
mediaElement.style.height = (loadedState.height || 360) + "px";
//mediaElement.style.background = 'red'
var iframeDiv = document.createElement('div')
iframeDiv.classList.add('iframeDiv')
mediaElement.appendChild(iframeDiv)
document.getElementById('eventTrigger').dataset.youtubetrigger = Date.now()
/*
mediaElement.src =
`https://www.youtube.com/embed/${code}?` +//&autoplay=1` +
`&controls=0&disablekb=1&enablejsapi=1&fs=0&loop=1` +
`&origin=${window.location.href}`
mediaElement.frameBorder = "0"
mediaElement.allowFullscreen = false;
*/
}
} else if (type == "text") {
mediaElement = document.createElement('div')
mediaElement.classList.add('textElement')
mediaElement.innerText = path
console.log("isNewElement", isNewElement)
if (isNewElement) {
/*
mediaElement.style.fontSize = '1ch' // 1ch is the width of the 0 character
mediaElement.style.opacity = 0
document.body.appendChild(mediaElement)
let { x, y, width: widthOnDom, height: heightOnDom } = mediaElement.getClientRects()[0]
document.body.removeChild(mediaElement)
const windowWidth = window.innerWidth
const fontScaleFactor = windowWidth / widthOnDom
newWidth = windowWidth / state.currentScale
newHeight = (heightOnDom * (windowWidth / widthOnDom)) / state.currentScale
mediaElement.style.opacity = 1
mediaElement.style.fontSize = fontScaleFactor + "ch"
mediaElement.style.width = newWidth + "px";
mediaElement.style.height = newHeight + "px";
*/
const { width: newWidth, height: newHeight } = adjustFontSize2(mediaElement, path)
loadedState.x = centerWin.centerX - (newWidth / state.currentScale / 2)
loadedState.y = centerWin.centerY - (newHeight / state.currentScale / 2)
loadedState.width = newWidth
loadedState.height = newHeight
} else {
//debugger;
const { width: newWidth, height: newHeight } = adjustFontSize2(mediaElement, path, loadedState.width)
oldRatio = loadedState.width / loadedState.height
console.log("oldRatio", oldRatio, "newRatio", newWidth / newHeight)
mediaElement.style.width = loadedState.width + "px";
mediaElement.style.height = newHeight + "px";
loadedState.width = newWidth
loadedState.height = newHeight
}
mediaElement.width = loadedState.width
mediaElement.height = loadedState.height
} else {
alert('unsupported media type')
return;
}
mediaElement.classList.add('draggable')
zIndex = state.elements.length;
mediaElement.style.zIndex = zIndex
mediaElement.dataset.zIndex = zIndex
mediaElement.dataset.x = loadedState.x
mediaElement.dataset.y = loadedState.y
if (loadedState.width != null && loadedState.height != null) {
mediaElement.width = loadedState.width
mediaElement.height = loadedState.height
}
let mediaObj = {
path: path,
type: type,
element: mediaElement,
width: loadedState.width,
height: loadedState.height
}
if (type == 'youtube' || type == 'video') {
mediaObj.loopPairs = loadedState.loopPairs || [[0, 100]] // 0% & 100% positions for loop
mediaObj.activeLoopPair = loadedState.activeLoopPair || 0
}
state.elements.push(mediaObj)
if (type == 'video') {
mediaObj.element.dataset.loopLeft = mediaObj.loopPairs[mediaObj.activeLoopPair][0]
mediaObj.element.dataset.loopRight = mediaObj.loopPairs[mediaObj.activeLoopPair][1]
mediaObj.element.addEventListener('timeupdate', onPlayerProgress)
}
itemHolder.appendChild(mediaElement)
//debugger;
if (type == 'text') {
//setTransformForElement(zIndex, 0, 0, loadedState.width, loadedState.height)
console.log("loadedState", loadedState, mediaElement)
setTransformForElement(zIndex)
//adjustFontSize2(mediaElement, path, loadedState.width)
} else
setTransformForElement(zIndex)
// 保存历史记录(仅在添加新元素时,不在恢复状态时)
if (isNewElement) {
saveStateToHistory()
}
}
function adjustFontSize2(mediaElement, text, maxWidth = window.innerWidth) {
temp = document.createElement('div')
temp.style.fontSize = '1ch' // 1ch is the width of the 0 character
temp.style.position = 'absolute'
temp.style.opacity = 0
temp.style.color = "white"
temp.style.whiteSpace = "nowrap"
temp.innerText = text
document.getElementById('hiddenTextTester').appendChild(temp)
let { x, y, width: widthOnDom, height: heightOnDom } = temp.getClientRects()[0]
newRatio = widthOnDom / heightOnDom
console.log("widthOnDom", widthOnDom, "HeightOnDom", heightOnDom, temp.clientWidth, temp.clientHeight, newRatio)
document.getElementById('hiddenTextTester').removeChild(temp)
const windowWidth = maxWidth
const fontScaleFactor = (maxWidth / widthOnDom) //* state.currentScale
//const fontScaleFactorScaled = fontScaleFactor / state.currentScale
newWidth = maxWidth
//newWidthScaled = newWidth / state.currentScale
newHeight = (heightOnDom * fontScaleFactor)
//newHeightScaled = newHeight / state.currentScale
console.log("Adjustfont2", fontScaleFactor, state.currentScale)
mediaElement.style.opacity = 1
mediaElement.style.fontSize = fontScaleFactor * state.currentScale + "ch"
mediaElement.style.width = newWidth + "px";
mediaElement.style.height = newHeight + "px";
return { width: newWidth, height: newHeight }
}
document.addEventListener('drop', (event) => {
if (state.isLocked) return
event.preventDefault();
event.stopPropagation();
// Todo check if file is valid
for (const f of event.dataTransfer.files) {
// Using the path attribute to get absolute file path
console.log('File Path of dragged files: ', f.path, state)
if (f.path.endsWith('.mp4')) {
addMediaWithPath(f.path, 'video')
} else {
// 发送文件路径到主进程,让主进程转换为 base64
ipcRenderer.send('convert-file-to-dataurl', f.path)
}
}
});
// 接收主进程转换后的 dataURL
ipcRenderer.on('file-converted-to-dataurl', (event, dataURL) => {
console.log('Received dataURL from main process')
addMediaWithPath(dataURL, 'dataURL')
});
function init() {
document.documentElement.addEventListener('mousedown', (event) => {
var target = event.target
//console.log('click', target)
if (target.id == 'root' || target.id == 'workspaceBox') {
console.log('background click')
clearAllSelected()
}
})
state.mode = 'standard'
}
document.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
function extractYoutubeId(path) {
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
var capture = path.match(regExp)//path.match(/v=([^\?]*)/);
return capture[7]
}
function clampWorkspaceTranslate(ratio, newTranslate) {
let { translateX, translateY } = newTranslate
let windowElement = document.getElementById('root')
let windowWidth = windowElement.clientWidth
let windowHeight = windowElement.clientHeight
if (-(translateX) > state.workspaceRect.x2 * ratio) { //workspace off screen to left
console.log('workspace off screen to left')
translateX = -(state.workspaceRect.x2 * ratio)
}
else if ((translateX) + (state.workspaceRect.x1 * ratio) > windowWidth) { // workspace off screen to right
console.log('workspace off screen to right')
translateX = windowWidth - (state.workspaceRect.x1 * ratio)
}
if (-(translateY) > state.workspaceRect.y2 * ratio) { // workspace off screen to top
console.log('workspace off screen to top')
translateY = -(state.workspaceRect.y2 * ratio)
}
else if ((translateY) + (state.workspaceRect.y1 * ratio) > windowHeight) { // workspace off screen to bottom
console.log('workspace off screen to bottom')
translateY = windowHeight - (state.workspaceRect.y1 * ratio)
}
return { translateX: translateX, translateY: translateY }
}
function updateScaleAndTranslate(newScale, newTranslate) {
if (newScale > 2) {
state.currentScale = 2
return;
}
newTranslate = clampWorkspaceTranslate(newScale, newTranslate)
state.currentScale = newScale
state.translate = newTranslate
document.body.style.transform = `translate(${state.translate.translateX}px, ${state.translate.translateY}px) scale(${state.currentScale})`
document.body.dataset.currentScale = state.currentScale
document.body.dataset.translateX = state.translate.translateX
document.body.dataset.translateY = state.translate.translateY
const ROOTCSS = document.querySelector(':root');
ROOTCSS.style.setProperty('--scale', newScale);
//window.currentScale = state.currentScale;
}
let objPlayground = { hi: 'yo' };
contextBridge.exposeInMainWorld('myAPI', {
updateScaleAndTranslate: updateScaleAndTranslate,
updateYoutubeOriginalSize: (idcode, w, h) => {
for (var i in state.elements) {
if (state.elements[i].type == 'youtube' && extractYoutubeId(state.elements[i].path) == idcode) {
state.elements[i].width = state.elements[i].width || w;
state.elements[i].height = state.elements[i].height || h;
state.elements[i].element.style.width = state.elements[i].width
state.elements[i].element.style.height = state.elements[i].height
let videoElement = state.elements[i].element.querySelector('iframe').contentDocument.querySelector('video');
videoElement.dataset.loopLeft = state.elements[i].loopPairs[state.elements[i].activeLoopPair][0]
videoElement.dataset.loopRight = state.elements[i].loopPairs[state.elements[i].activeLoopPair][1]
videoElement.addEventListener('timeupdate', onPlayerProgress)
console.log(state.elements[i])
}
}
},
objPlayground: objPlayground
})
interact('.draggable')
.draggable({
listeners: {
move: dragMoveListener,
end: function(event) {
// 拖动结束时保存历史记录
saveStateToHistory()
}
},
inertia: false,
}).on('tap', function (event) {
var target = event.target
handleSelected(target)
//
event.preventDefault()
})
function isMouseInBlockingState() {
if (mouseObj.keys[' '] && mouseObj.keys['Control']) {
return true
} else if (mouseObj.keys[' ']) {
return true
}
return false
}
interact('.selectedItem').resizable({
// resize from all edges and corners
//allowFrom: '.selectedItem',
edges: { left: true, right: true, bottom: true, top: true },
ratio: 1,
enabled: true,
margin: 4,
listeners: [{
move(event) {
if (isMouseInBlockingState()) return;
var target = event.target
//handleSelected(target, true)
console.log('resize')
if (target.classList.contains('textElement')) {
adjustFontSize2(target, target.innerText, event.rect.width)
}
setTransformForElement(target.dataset.zIndex, event.deltaRect.left, event.deltaRect.top, event.rect.width, event.rect.height)
forceRedraw()
},
end(event) {
// 调整大小结束时保存历史记录
saveStateToHistory()
}
}],
modifiers: [
interact.modifiers.aspectRatio({
// make sure the width is always double the height
ratio: 'preserve',
// also restrict the size by nesting another modifier
}),
// minimum size
interact.modifiers.restrictSize({
min: { width: 10 }
})
],
inertia: true
}).draggable({
listeners: {
move: dragMoveListener,
end: function(event) {
// 拖动结束时保存历史记录
saveStateToHistory()
}
},
inertia: false
}).on('tap', function (event) {
var target = event.target
handleSelected(target)
//
event.preventDefault()
})
function copySelected() {
if (state.elements.length == 0) return
// 获取所有选中的元素
const selectedElements = state.elements.filter(el =>
el.element.classList.contains('selectedItem')
)
if (selectedElements.length === 0) return
console.log('Copying', selectedElements.length, 'selected element(s)')
// 如果只选中一个元素
if (selectedElements.length === 1) {
const selectedElement = selectedElements[0]
if (selectedElement.type === 'text') {
// 文本元素:复制文本内容
ipcRenderer.send('copy-text', selectedElement.element.innerText)
console.log('Copying text:', selectedElement.element.innerText)
} else if (selectedElement.type === 'img' || selectedElement.type === 'dataURL' || selectedElement.type === 'filePath') {
// 图片元素:使用 Canvas 转换为高质量 PNG dataURL
copyImageToClipboard(selectedElement)
} else if (selectedElement.type === 'video' || selectedElement.type === 'youtube') {
// 视频元素:复制路径或链接
ipcRenderer.send('copy-text', selectedElement.path)
console.log('Copying video path:', selectedElement.path)
}
} else {
// 多个元素:将所有图片合并到一个画布上
copyMultipleImages(selectedElements)
}
}
function copyImageToClipboard(selectedElement) {
try {
const img = selectedElement.element
const imgSrc = img.src || selectedElement.path
console.log('Copying image - type:', selectedElement.type)
console.log('Image src:', imgSrc.substring(0, 100))
// 检查是否是 GIF通过 dataURL 或文件扩展名)
const isGif = imgSrc.startsWith('data:image/gif') ||
imgSrc.toLowerCase().endsWith('.gif') ||
(selectedElement.path && selectedElement.path.toLowerCase().endsWith('.gif'))
if (isGif && imgSrc.startsWith('data:')) {
// 如果是 GIF 的 dataURL直接使用原始数据
console.log('Copying GIF with original data (preserving animation)')
ipcRenderer.send('copy-image', imgSrc)
} else if (selectedElement.type === 'dataURL' && imgSrc.startsWith('data:')) {
// 如果是其他格式的 dataURL也直接使用
console.log('Copying image with original dataURL')
ipcRenderer.send('copy-image', imgSrc)
} else {
// 对于非 dataURL 的图片,使用 Canvas 转换
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth || img.width
canvas.height = img.naturalHeight || img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
// 转换为 PNG 格式的 dataURL
const pngDataURL = canvas.toDataURL('image/png')
console.log('Copying image converted to PNG, size:', canvas.width, 'x', canvas.height)
ipcRenderer.send('copy-image', pngDataURL)
}
} catch (err) {
console.error('Error copying image:', err)
// 降级方案:直接使用原始路径
const imgSrc = selectedElement.element.src || selectedElement.path
ipcRenderer.send('copy-image', imgSrc)
}
}
async function copyMultipleImages(selectedElements) {
try {
// 过滤出图片元素
const imageElements = selectedElements.filter(el =>
el.type === 'img' || el.type === 'dataURL' || el.type === 'filePath'
)
if (imageElements.length === 0) {
console.log('No images to copy')
return
}
console.log('Copying', imageElements.length, 'images')
// 收集所有图片的原始数据(保持 GIF 动画)
const imageDataURLs = []
for (const el of imageElements) {
try {
const img = el.element
const imgSrc = img.src || el.path
// 检查是否是 GIF
const isGif = imgSrc.startsWith('data:image/gif') ||
imgSrc.toLowerCase().endsWith('.gif') ||
(el.path && el.path.toLowerCase().endsWith('.gif'))
if (isGif && imgSrc.startsWith('data:')) {
// GIF dataURL - 直接使用原始数据
console.log(' Preserving GIF animation for element')
imageDataURLs.push(imgSrc)
} else if (el.type === 'dataURL' && imgSrc.startsWith('data:')) {
// 其他 dataURL - 直接使用
console.log(' Using original dataURL for element')
imageDataURLs.push(imgSrc)
} else {
// 其他格式 - 转换为 PNG
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth || img.width
canvas.height = img.naturalHeight || img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
const pngDataURL = canvas.toDataURL('image/png')
console.log(' Converted to PNG for element')
imageDataURLs.push(pngDataURL)
}
} catch (err) {
console.error('Error processing image:', err)
}
}
if (imageDataURLs.length === 0) {
console.log('No valid images to copy')
return
}
// 发送到主进程处理
console.log('Sending', imageDataURLs.length, 'images to main process')
ipcRenderer.send('copy-multiple-images', imageDataURLs)
} catch (err) {
console.error('Error copying multiple images:', err)
ipcRenderer.send('copy-text', '复制多张图片失败: ' + err.message)
}
}
function deleteSelected() {
if (state.elements.length == 0) return
// 收集所有选中的元素
const selectedElements = state.elements.filter(el =>
el.element.classList.contains('selectedItem')
)
if (selectedElements.length === 0) return
console.log('Deleting', selectedElements.length, 'selected element(s)')
// 保存历史记录
saveStateToHistory()
// 删除所有选中的元素
selectedElements.forEach(elementObj => {
// 从 DOM 中移除
elementObj.element.remove()
// 从 state.elements 数组中移除
const index = state.elements.indexOf(elementObj)
if (index > -1) {
state.elements.splice(index, 1)
}
})
// 重新设置所有剩余元素的 zIndex
state.elements.forEach((elementObj, index) => {
elementObj.element.style.zIndex = index
elementObj.element.dataset.zIndex = index
})
clearAllSelected()
}
function clearAllSelected() {
document.querySelectorAll('.selectedItem').forEach((elm) => {
elm.classList.remove('selectedItem')
elm.classList.add('draggable')
})
}
function handleSelected(target, dragging = false) {
if (isMouseInBlockingState()) return;
clearAllSelected()
target.classList.remove('draggable')
target.classList.add('selectedItem')
if (state.elements.length > 1) {
let targetIndex = parseInt(target.dataset.zIndex);
state.elements.push(state.elements.splice(targetIndex, 1)[0]);
for (var i = targetIndex; i < state.elements.length; i++) { // i can start at targetIndex
state.elements[i].element.style.zIndex = i;
state.elements[i].element.dataset.zIndex = i;
}
}
}
function dragMoveListener(event) {
if (isMouseInBlockingState()) return;
var target = event.target
handleSelected(target, true)
setTransformForElement(target.dataset.zIndex, event.dx, event.dy)
forceRedraw()
}
function resizeWorkspaceToFitObj(x, y, width, height) {
if (width == 0 || height == 0) return
state.workspaceRect.x1 = Math.min(x, state.workspaceRect.x1)
state.workspaceRect.y1 = Math.min(y, state.workspaceRect.y1)
state.workspaceRect.x2 = Math.max(x + width, state.workspaceRect.x2)
state.workspaceRect.y2 = Math.max(y + height, state.workspaceRect.y2)
refreshWorkspace()
}
function initWorkspace() {
state.workspaceRect = {
x1: 0,
y1: 0,
x2: 0,
y2: 0
}
}
function refreshWorkspace() {
const element = document.getElementById('workspaceBox');
element.style.left = state.workspaceRect.x1 + "px";
element.style.top = state.workspaceRect.y1 + "px";
element.style.width = (state.workspaceRect.x2 - state.workspaceRect.x1) + "px"
element.style.height = (state.workspaceRect.y2 - state.workspaceRect.y1) + "px"
}
function setTransformForElement(elementIndex, dx = 0, dy = 0, width = null, height = null) {
let elementObj = state.elements[elementIndex]
let x = (parseFloat(elementObj.element.dataset.x) || 0) + (dx / state.currentScale)
let y = (parseFloat(elementObj.element.dataset.y) || 0) + (dy / state.currentScale)
elementObj.element.setAttribute('data-x', x)
elementObj.element.setAttribute('data-y', y)
elementObj.x = x
elementObj.y = y
if (width != null && height != null) {
elementObj.width = width / state.currentScale
elementObj.height = height / state.currentScale
elementObj.element.style.width = width / state.currentScale + 'px'
elementObj.element.style.height = height / state.currentScale + 'px'
}
resizeWorkspaceToFitObj(x, y, elementObj.width || elementObj.element.width, elementObj.height || elementObj.element.height)
elementObj.element.style.transform = 'translate(' + x + 'px, ' + y + 'px)'
}
function forceRedraw() {
if (document.body.parentElement.style.backgroundColor == '') {
document.body.parentElement.style.backgroundColor = '#04040400'
} else {
document.body.parentElement.style.backgroundColor = ''
}
}
// 选择框相关函数
function hideSelectionBox() {
const selectionBox = document.getElementById('selectionBox')
if (selectionBox) {
selectionBox.style.display = 'none'
selectionBox.style.width = '0px'
selectionBox.style.height = '0px'
}
mouseObj.selecting = false
mouseObj.selectionStart = null
mouseObj.selectionWorldRect = null
}
function updateSelectionBox(currentX, currentY) {
const selectionBox = document.getElementById('selectionBox')
if (!selectionBox || !mouseObj.selectionStart) return
// 将屏幕坐标转换为世界坐标body 内的坐标系统)
const startWorldX = (mouseObj.selectionStart.x - state.translate.translateX) / state.currentScale
const startWorldY = (mouseObj.selectionStart.y - state.translate.translateY) / state.currentScale
const currentWorldX = (currentX - state.translate.translateX) / state.currentScale
const currentWorldY = (currentY - state.translate.translateY) / state.currentScale
const worldLeft = Math.min(startWorldX, currentWorldX)
const worldTop = Math.min(startWorldY, currentWorldY)
const worldWidth = Math.abs(currentWorldX - startWorldX)
const worldHeight = Math.abs(currentWorldY - startWorldY)
// 直接使用世界坐标,因为选择框在 body 内,会自动应用 body 的 transform
selectionBox.style.left = worldLeft + 'px'
selectionBox.style.top = worldTop + 'px'
selectionBox.style.width = worldWidth + 'px'
selectionBox.style.height = worldHeight + 'px'
selectionBox.style.display = 'block'
// 保存世界坐标用于后续检测
mouseObj.selectionWorldRect = {
left: worldLeft,
top: worldTop,
right: worldLeft + worldWidth,
bottom: worldTop + worldHeight
}
}
function finishSelection() {
const selectionBox = document.getElementById('selectionBox')
if (!selectionBox || !mouseObj.selectionWorldRect) return
// 清除之前的选择
clearAllSelected()
// 检查每个元素是否与选择框相交(使用世界坐标)
let selectedElements = []
state.elements.forEach((elementObj, index) => {
// 获取元素的世界坐标
const elemX = parseFloat(elementObj.element.dataset.x) || 0
const elemY = parseFloat(elementObj.element.dataset.y) || 0
const elemWidth = elementObj.width || elementObj.element.width || 0
const elemHeight = elementObj.height || elementObj.element.height || 0
const elementWorldRect = {
left: elemX,
top: elemY,
right: elemX + elemWidth,
bottom: elemY + elemHeight
}
// 检查矩形是否相交
if (isRectIntersecting(mouseObj.selectionWorldRect, elementWorldRect)) {
selectedElements.push(index)
}
})
// 选中所有相交的元素
if (selectedElements.length > 0) {
// 将最后一个选中的元素移到最上层
const lastIndex = selectedElements[selectedElements.length - 1]
handleSelected(state.elements[lastIndex].element)
// 标记其他选中的元素
selectedElements.slice(0, -1).forEach(index => {
state.elements[index].element.classList.add('selectedItem')
state.elements[index].element.classList.remove('draggable')
})
}
}
function isRectIntersecting(rect1, rect2) {
return !(rect1.right < rect2.left ||
rect1.left > rect2.right ||
rect1.bottom < rect2.top ||
rect1.top > rect2.bottom)
}
window.addEventListener('load', (event) => {
setTimeout(function () {
console.log('page is fully loaded');
ipcRenderer.send('ready')
}, 2000);
});
// 窗口获得焦点时重置键盘状态
window.addEventListener('focus', () => {
console.log('Window focused - resetting keyboard state')
mouseObj.keys = new Object()
mouseObj.ctrlSpace = false
mouseObj.space = false
// 如果有未完成的选择,清理它
if (mouseObj.selecting) {
hideSelectionBox()
}
});
// 窗口失去焦点时重置键盘状态
window.addEventListener('blur', () => {
console.log('Window blurred - resetting keyboard state')
mouseObj.keys = new Object()
mouseObj.ctrlSpace = false
mouseObj.space = false
// 如果有未完成的选择,清理它
if (mouseObj.selecting) {
hideSelectionBox()
}
});