feat: 初始化RefVroid项目,添加核心功能与资源文件
- 添加主窗口、帮助窗口和系统托盘功能 - 实现图片、视频和文本的拖放与粘贴功能 - 添加场景保存与加载功能 - 包含基本UI样式和快捷键帮助文档 - 添加.gitattributes和.npmrc配置文件 - 包含LICENSE许可文件和图标资源
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
82
.gitignore
vendored
Normal file
82
.gitignore
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
.vscode/launch.json
|
||||
|
||||
# electron forge output
|
||||
out/
|
||||
|
||||
*.purgif
|
||||
5
.npmrc
Normal file
5
.npmrc
Normal file
@@ -0,0 +1,5 @@
|
||||
node-linker=hoisted
|
||||
# Electron 中国镜像源配置
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
ELECTRON_CUSTOM_DIR={{ version }}
|
||||
ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 小煜
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
148
help.html
Normal file
148
help.html
Normal file
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>快捷键帮助 - AnimRef</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #259dc7;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 20px 0 12px 0;
|
||||
color: #88c0d0;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #444;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.help-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.help-item kbd {
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
color: #e0e0e0;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.help-item span {
|
||||
color: #b0b0b0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.help-footer {
|
||||
margin-top: 25px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background: #555;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb:hover {
|
||||
background: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>快捷键帮助</h2>
|
||||
|
||||
<div class="help-section">
|
||||
<h3>文件操作</h3>
|
||||
<div class="help-item"><kbd>Ctrl+C</kbd><span>复制选中元素</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+V</kbd><span>粘贴图片/文本</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+L</kbd><span>加载场景</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+S</kbd><span>保存场景</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+N</kbd><span>新建场景</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+W</kbd><span>关闭窗口</span></div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h3>窗口操作</h3>
|
||||
<div class="help-item"><kbd>Ctrl+F</kbd><span>最大化/恢复窗口</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+M</kbd><span>最小化窗口</span></div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h3>视图操作</h3>
|
||||
<div class="help-item"><kbd>空格+拖动</kbd><span>平移画布</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+空格+拖动</kbd><span>缩放画布</span></div>
|
||||
<div class="help-item"><kbd>鼠标滚轮</kbd><span>缩放画布</span></div>
|
||||
<div class="help-item"><kbd>中键拖动</kbd><span>平移画布</span></div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h3>元素操作</h3>
|
||||
<div class="help-item"><kbd>左键拖动</kbd><span>移动元素</span></div>
|
||||
<div class="help-item"><kbd>左键框选</kbd><span>选择多个元素</span></div>
|
||||
<div class="help-item"><kbd>拖动边缘</kbd><span>调整大小</span></div>
|
||||
<div class="help-item"><kbd>Delete</kbd><span>删除选中元素</span></div>
|
||||
<div class="help-item"><kbd>Esc</kbd><span>取消选择框</span></div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h3>其他</h3>
|
||||
<div class="help-item"><kbd>右键</kbd><span>显示菜单</span></div>
|
||||
<div class="help-item"><kbd>Ctrl+H</kbd><span>显示/隐藏帮助</span></div>
|
||||
</div>
|
||||
|
||||
<p class="help-footer">按 Esc 或关闭窗口退出帮助</p>
|
||||
|
||||
<script>
|
||||
// 按 Esc 关闭窗口
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
window.close()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
65
images/play.svg
Normal file
65
images/play.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 71.412065 50"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="YouTube_dark_icon (2017).svg"
|
||||
width="71.412064"
|
||||
height="50"><metadata
|
||||
id="metadata33"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs31" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="715"
|
||||
id="namedview29"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="5.4355702"
|
||||
inkscape:cx="30.091613"
|
||||
inkscape:cy="15.29613"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><style
|
||||
type="text/css"
|
||||
id="style3">
|
||||
.st0{fill:#FF0000;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#282828;}
|
||||
</style><g
|
||||
id="g5"
|
||||
transform="scale(0.58823529,0.58823529)"><path
|
||||
class="st0"
|
||||
d="M 118.9,13.3 C 117.5,8.1 113.4,4 108.2,2.6 98.7,0 60.7,0 60.7,0 60.7,0 22.7,0 13.2,2.5 8.1,3.9 3.9,8.1 2.5,13.3 0,22.8 0,42.5 0,42.5 0,42.5 0,62.3 2.5,71.7 3.9,76.9 8,81 13.2,82.4 22.8,85 60.7,85 60.7,85 c 0,0 38,0 47.5,-2.5 5.2,-1.4 9.3,-5.5 10.7,-10.7 2.5,-9.5 2.5,-29.2 2.5,-29.2 0,0 0.1,-19.8 -2.5,-29.3 z"
|
||||
id="path7"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#282828;fill-opacity:1" /><polygon
|
||||
class="st1"
|
||||
points="80.2,42.5 48.6,24.3 48.6,60.7 "
|
||||
id="polygon9"
|
||||
style="fill:#ffffff" /></g></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
images/tray-icon.png
Normal file
BIN
images/tray-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 B |
271
index.css
Normal file
271
index.css
Normal file
@@ -0,0 +1,271 @@
|
||||
:root {
|
||||
--scale: 1;
|
||||
}
|
||||
html {
|
||||
border: #0c0c0c solid 1px;
|
||||
transform-origin: 0 0;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
html,
|
||||
body {
|
||||
background: #202020;
|
||||
background-color: transparent;
|
||||
height: calc(100% - 2px);
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
body {
|
||||
transition: -webkit-transform 0.1s ease 0s;
|
||||
transform-origin: 0px 0px;
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
#welcome {
|
||||
font-family: sans-serif;
|
||||
color: #7a7a7a;
|
||||
text-align: center;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 107px;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
left: calc(50vw - 360px / 2);
|
||||
top: calc(50vh - 140px / 2);
|
||||
}
|
||||
h1,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
#welcome h1 {
|
||||
line-height: 1;
|
||||
font-weight: normal;
|
||||
font-size: x-large;
|
||||
color: #888888;
|
||||
}
|
||||
#welcome p {
|
||||
margin-top: 12px;
|
||||
}
|
||||
#workspaceBox {
|
||||
position: absolute;
|
||||
background-color: #2a2a2a;
|
||||
z-index: -1;
|
||||
}
|
||||
#itemHolder {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.selectedItem {
|
||||
outline: calc(0.15rem / var(--scale)) solid rgb(37 157 199);
|
||||
}
|
||||
.selectedItem.resizeItem {
|
||||
}
|
||||
#itemHolder > * {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.textElement {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
text-shadow: rgb(255, 255, 255) -1px -1px 0px, rgb(255, 255, 255) 1px -1px 0px,
|
||||
rgb(255, 255, 255) -1px 1px 0px, rgb(255, 255, 255) 1px 1px 0px;
|
||||
text-align: center;
|
||||
display:block;
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
.hiddenTextTester{
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
font-size: 1ch;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.iframeDiv {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
}
|
||||
@keyframes rock {
|
||||
0% {
|
||||
transform: rotate(-9deg);
|
||||
filter: blur(0.5px);
|
||||
}
|
||||
50% {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(9deg);
|
||||
filter: blur(0.5px);
|
||||
}
|
||||
}
|
||||
@keyframes gradientLoad {
|
||||
from {
|
||||
background-size: 150% 100%;
|
||||
filter: drop-shadow(2px 4px 6px black);
|
||||
}
|
||||
|
||||
to {
|
||||
background-size: 200% 200%;
|
||||
filter: drop-shadow(2px 4px 2px black);
|
||||
}
|
||||
}
|
||||
.playerNeedsSetup::before {
|
||||
background-image: url("images/play.svg");
|
||||
background-repeat: no-repeat;
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 71px;
|
||||
height: 50px;
|
||||
transform: rotate(0deg);
|
||||
|
||||
top: calc(50% - 50px / 2);
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: calc(50% - 71px / 2);
|
||||
opacity: 0.5;
|
||||
animation-name: rock;
|
||||
animation-duration: 0.5s;
|
||||
animation-direction: alternate;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.playerNeedsSetup {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
rgba(0, 0, 0, 0.1) 0%,
|
||||
rgba(0, 0, 0, 0.6) 100%
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
filter: drop-shadow(2px 4px 6px black);
|
||||
animation-name: gradientLoad;
|
||||
animation-duration: 1s;
|
||||
animation-direction: alternate-reverse;
|
||||
animation-iteration-count: infinite;
|
||||
background-position: center;
|
||||
}
|
||||
.editVideo {
|
||||
width: 100vw !important;
|
||||
height: calc(100vh - 20px) !important;
|
||||
transform: unset !important;
|
||||
background-color: black;
|
||||
}
|
||||
.disableBorder {
|
||||
border: unset !important;
|
||||
}
|
||||
.timeline {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 20px;
|
||||
background: #403f3f;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(100vh - 20px);
|
||||
}
|
||||
#progressbar {
|
||||
background-color: #ececec;
|
||||
box-shadow: inset 0 0.5em 0.5em rgb(0 0 0 / 5%);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: all 400ms ease-out;
|
||||
}
|
||||
#slider {
|
||||
background-color: rgba(78, 47, 102, 1);
|
||||
}
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
#slider:hover .noUi-connect,
|
||||
#slider:hover .noUi-handle {
|
||||
filter: brightness(1.5);
|
||||
transition: filter 300ms ease;
|
||||
}
|
||||
#slider .noUi-handle {
|
||||
border-left: 1em solid transparent;
|
||||
border-right: 1em solid transparent;
|
||||
border-bottom: 1em solid #a74040;
|
||||
outline: none;
|
||||
border-radius: unset;
|
||||
background: unset;
|
||||
cursor: default;
|
||||
box-shadow: unset;
|
||||
border-top: unset;
|
||||
opacity: 0.95;
|
||||
}
|
||||
#slider .noUi-handle:before,
|
||||
#slider .noUi-handle:after {
|
||||
content: unset;
|
||||
}
|
||||
#slider .noUi-connect {
|
||||
background: #b128284a; /*linear-gradient(90deg, rgba(78,47,102,1) 0%, rgba(91,52,122,1) 47%, rgba(113,80,136,1) 47%, rgba(121,76,157,1) 100%);*/
|
||||
opacity: 0.9;
|
||||
box-shadow: inset 3px 2px 15px 0px #000000;
|
||||
transition: all 400ms ease-out;
|
||||
}
|
||||
#slider .noUi-target {
|
||||
background: #bfbfbf;
|
||||
border-radius: 7px;
|
||||
border: unset;
|
||||
box-shadow: inset 5px 6px 7px #d3d1d6, 8px 2px 8px -5px #4b285d;
|
||||
}
|
||||
|
||||
|
||||
/* 选择框样式 */
|
||||
#selectionBox {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(37, 157, 199, 0.8);
|
||||
background-color: rgba(37, 157, 199, 0.2);
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* 锁定状态样式 */
|
||||
body.locked {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.locked #lockIndicator {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#lockIndicator {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
position: fixed !important;
|
||||
bottom: 20px !important;
|
||||
right: 20px !important;
|
||||
font-size: 48px;
|
||||
opacity: 0.8;
|
||||
pointer-events: none !important;
|
||||
z-index: 2147483647 !important;
|
||||
text-shadow:
|
||||
0 0 10px rgba(0, 0, 0, 1),
|
||||
0 0 20px rgba(0, 0, 0, 0.8),
|
||||
2px 2px 4px rgba(0, 0, 0, 0.9);
|
||||
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.9));
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
38
index.html
Normal file
38
index.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html id="root">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>RefVroid Support</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="script-src 'self' 'unsafe-inline' https://www.youtube.com;"
|
||||
/>
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link href="node_modules/nouislider/dist/nouislider.css" rel="stylesheet" />
|
||||
<script src="node_modules/nouislider/dist/nouislider.js"></script>
|
||||
<script src="https://www.youtube.com/iframe_api" defer></script>
|
||||
<script src="renderer.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="hiddenTextTester"></div>
|
||||
<div id="editVideoTools" class="hide">
|
||||
<div class="timeline">
|
||||
<div id="slider"><div id="progressbar"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="eventTrigger"></div>
|
||||
<div id="welcome">
|
||||
<h1>
|
||||
这里什么都没有!<br />
|
||||
拖放一些图片开始使用。
|
||||
</h1>
|
||||
<p>(按 Ctrl+H 获取更多帮助)</p>
|
||||
</div>
|
||||
<div id="itemHolder"></div>
|
||||
<div id="workspaceBox"></div>
|
||||
<div id="selectionBox"></div>
|
||||
<div id="lockIndicator">🔒</div>
|
||||
</body>
|
||||
</html>
|
||||
3
interact.min.js
vendored
Normal file
3
interact.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
664
main.js
Normal file
664
main.js
Normal file
@@ -0,0 +1,664 @@
|
||||
const {
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ipcMain,
|
||||
app,
|
||||
clipboard,
|
||||
screen,
|
||||
globalShortcut,
|
||||
dialog,
|
||||
Tray,
|
||||
nativeImage
|
||||
} = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs');
|
||||
const Store = require('electron-store');
|
||||
|
||||
const store = new Store();
|
||||
//store.clear()
|
||||
|
||||
let width = 400;
|
||||
let height = 300;
|
||||
let isLocked = false;
|
||||
let tray = null;
|
||||
let helpWindow = null;
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
backgroundColor: "#202020",
|
||||
width: width,
|
||||
height: height,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: true,
|
||||
}
|
||||
})
|
||||
win.setAlwaysOnTop(true);
|
||||
|
||||
win.loadFile('index.html')
|
||||
return win;
|
||||
}
|
||||
|
||||
function createHelpWindow(parentWindow) {
|
||||
// 如果帮助窗口已存在,则聚焦它
|
||||
if (helpWindow && !helpWindow.isDestroyed()) {
|
||||
helpWindow.focus()
|
||||
return
|
||||
}
|
||||
|
||||
helpWindow = new BrowserWindow({
|
||||
width: 700,
|
||||
height: 600,
|
||||
backgroundColor: "#2a2a2a",
|
||||
title: "快捷键帮助 - RefVroid",
|
||||
autoHideMenuBar: true,
|
||||
parent: parentWindow,
|
||||
modal: true,
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
})
|
||||
|
||||
helpWindow.loadFile('help.html')
|
||||
|
||||
// 窗口关闭时清理引用
|
||||
helpWindow.on('closed', () => {
|
||||
helpWindow = null
|
||||
})
|
||||
}
|
||||
app.commandLine.appendSwitch('disable-site-isolation-trials');
|
||||
const contextMenu = new Menu()
|
||||
const recentSubmenu = new Menu()
|
||||
const windowSubmenu = new Menu()
|
||||
const opacitySubmenu = new Menu()
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const mainWin = createWindow()
|
||||
|
||||
// 创建系统托盘图标
|
||||
let trayIcon
|
||||
const iconPath = path.join(__dirname, 'images', 'tray-icon.png')
|
||||
|
||||
if (fs.existsSync(iconPath)) {
|
||||
trayIcon = nativeImage.createFromPath(iconPath)
|
||||
} else {
|
||||
// 使用 base64 图标
|
||||
const iconData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAA5SURBVDiN7dMxDQAgEMCwA/+fGQZgABZgARZgARZgARZgARZgARZgARZgARZgARZgARZgARbwsgMvpQYH8VLsJwAAAABJRU5ErkJggg=='
|
||||
trayIcon = nativeImage.createFromDataURL(iconData)
|
||||
}
|
||||
|
||||
if (trayIcon.isEmpty()) {
|
||||
trayIcon = nativeImage.createFromBuffer(
|
||||
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAA5SURBVDiN7dMxDQAgEMCwA/+fGQZgABZgARZgARZgARZgARZgARZgARZgARZgARZgARZgARbwsgMvpQYH8VLsJwAAAABJRU5ErkJggg==', 'base64')
|
||||
)
|
||||
}
|
||||
|
||||
tray = new Tray(trayIcon)
|
||||
|
||||
// 创建更新托盘菜单的函数
|
||||
function updateTrayMenu() {
|
||||
const menuTemplate = [
|
||||
{
|
||||
label: 'RefVroid',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
}
|
||||
]
|
||||
|
||||
// 只在锁定时显示解锁选项
|
||||
if (isLocked) {
|
||||
menuTemplate.push({
|
||||
label: '解锁窗口',
|
||||
click: () => {
|
||||
isLocked = false
|
||||
mainWin.setIgnoreMouseEvents(false)
|
||||
mainWin.webContents.send('unlock-window')
|
||||
updateTrayMenu()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
menuTemplate.push(
|
||||
{
|
||||
label: '显示窗口',
|
||||
click: () => {
|
||||
mainWin.show()
|
||||
mainWin.focus()
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: '退出',
|
||||
click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const trayMenu = Menu.buildFromTemplate(menuTemplate)
|
||||
tray.setContextMenu(trayMenu)
|
||||
}
|
||||
|
||||
updateTrayMenu()
|
||||
tray.setToolTip('RefVroid - 动画参考工具')
|
||||
tray.on('click', () => {
|
||||
mainWin.isVisible() ? mainWin.hide() : mainWin.show()
|
||||
})
|
||||
mainWin.on('ready-to-show', () => {
|
||||
console.log('Window ready to be presented');
|
||||
mainWin.show();
|
||||
});
|
||||
mainWin.webContents.on('did-finish-load', () => {
|
||||
console.log('Page fully loaded');
|
||||
//setTimeout(loadMostRecent, 1000)
|
||||
loadMostRecent()
|
||||
});
|
||||
|
||||
contextMenu.append(new MenuItem({
|
||||
label: '粘贴',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+V' : 'Ctrl+V',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
handlePaste();
|
||||
}
|
||||
}));
|
||||
contextMenu.append(new MenuItem({ type: 'separator' }))
|
||||
|
||||
contextMenu.append(new MenuItem({
|
||||
label: '总是置顶', type: 'checkbox', checked: true,
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
mainWin.setAlwaysOnTop(menuItem.checked);
|
||||
}
|
||||
}));
|
||||
|
||||
// 只注册开发者工具的全局快捷键
|
||||
globalShortcut.register('Control+Shift+I', () => {
|
||||
mainWin.webContents.openDevTools()
|
||||
});
|
||||
|
||||
function handlePaste() {
|
||||
console.log('handlePaste')
|
||||
|
||||
let payload = {}
|
||||
///strimg = JSON.stringify(img)
|
||||
var formats = clipboard.availableFormats();
|
||||
var rawFilePath = clipboard.read('FileNameW');
|
||||
|
||||
if (rawFilePath) {
|
||||
var filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '');
|
||||
payload.type = 'filePath'
|
||||
payload.filePath = filePath
|
||||
console.log(filePath)
|
||||
|
||||
} else if (formats.indexOf('image/png') > -1) {
|
||||
img = clipboard.readImage();
|
||||
payload.type = 'dataURL'
|
||||
payload.dataURL = img.toDataURL().replace('png', 'gif')
|
||||
} else if (formats.indexOf('text/plain') > -1) {
|
||||
//potential link
|
||||
var potentialUrl = clipboard.readText()
|
||||
console.log("potentialUrl", potentialUrl)
|
||||
if (validateUrl(potentialUrl)) {
|
||||
payload.type = 'filePath'
|
||||
payload.filePath = potentialUrl
|
||||
} else {
|
||||
// paste as text element
|
||||
payload.type = 'text'
|
||||
payload.text = clipboard.readText()
|
||||
}
|
||||
}
|
||||
|
||||
console.log(formats)
|
||||
//console.log(payload)
|
||||
mainWin.webContents.send('clipboard', JSON.stringify(payload)) // send to web page
|
||||
}
|
||||
function addToRecent(filePath) {
|
||||
var recent = JSON.parse(store.get('recent') || "[]")
|
||||
console.log("addToRecent", store.get('recent'), filePath)
|
||||
let index = recent.indexOf(filePath)
|
||||
let exists = index != -1
|
||||
if (exists) {
|
||||
recent.splice(index, 1)
|
||||
}
|
||||
recent.push(filePath)
|
||||
store.set('recent', JSON.stringify(recent));
|
||||
console.log("addedToRecent", store.get('recent'), filePath)
|
||||
if (!exists) {
|
||||
recentSubmenu.append(new MenuItem({
|
||||
label: filePath,
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
readAndLoadFilePath(menuItem.label)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
function populateRecent() {
|
||||
var recent = JSON.parse(store.get('recent') || "[]")
|
||||
console.log("populateRecent", store.get('recent'))
|
||||
for (recentfile of recent) {
|
||||
recentSubmenu.append(new MenuItem({
|
||||
label: recentfile,
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
readAndLoadFilePath(menuItem.label)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
function loadMostRecent() {
|
||||
var recent = JSON.parse(store.get('recent') || "[]")
|
||||
if (recent.length > 0)
|
||||
readAndLoadFilePath(recent[recent.length - 1])
|
||||
}
|
||||
|
||||
contextMenu.append(new MenuItem({ type: 'separator' }))
|
||||
|
||||
function readAndLoadFilePath(filePath) {
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) throw err;
|
||||
let newState = JSON.parse(data);
|
||||
mainWin.webContents.send('load-scene', newState, filePath)
|
||||
});
|
||||
}
|
||||
|
||||
contextMenu.append(new MenuItem({
|
||||
label: "最近使用", type: 'submenu',
|
||||
submenu: recentSubmenu
|
||||
}));
|
||||
populateRecent()
|
||||
|
||||
// 添加窗口菜单
|
||||
contextMenu.append(new MenuItem({
|
||||
label: "窗口", type: 'submenu',
|
||||
submenu: windowSubmenu
|
||||
}));
|
||||
|
||||
// 添加透明度菜单
|
||||
for (let opacity = 100; opacity >= 10; opacity -= 10) {
|
||||
opacitySubmenu.append(new MenuItem({
|
||||
label: `${opacity}%`,
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
mainWin.setOpacity(opacity / 100)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
contextMenu.append(new MenuItem({
|
||||
label: "透明度", type: 'submenu',
|
||||
submenu: opacitySubmenu
|
||||
}));
|
||||
|
||||
// 添加锁定窗口菜单
|
||||
contextMenu.append(new MenuItem({
|
||||
id: "lock-window",
|
||||
label: '锁定窗口',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
isLocked = true
|
||||
mainWin.setIgnoreMouseEvents(true, { forward: true })
|
||||
mainWin.webContents.send('lock-window')
|
||||
updateTrayMenu()
|
||||
}
|
||||
}));
|
||||
contextMenu.append(new MenuItem({
|
||||
label: '加载',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+L' : 'Ctrl+L',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'RefVroid 场景', extensions: ['purgif'] }
|
||||
]
|
||||
}).then(result => {
|
||||
if (!result.canceled) {
|
||||
readAndLoadFilePath(result.filePaths[0])
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
}));
|
||||
contextMenu.append(new MenuItem({
|
||||
label: '保存',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+S' : 'Ctrl+S',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
dialog.showSaveDialog({
|
||||
defaultPath: 'scene.purgif',
|
||||
filters: [
|
||||
{ name: 'RefVroid 场景', extensions: ['purgif'] }
|
||||
]
|
||||
}).then(result => {
|
||||
if (!result.canceled) {
|
||||
mainWin.webContents.send('save-scene', result.filePath)
|
||||
addToRecent(result.filePath)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
}));
|
||||
contextMenu.append(new MenuItem({
|
||||
label: '新建场景',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+N' : 'Ctrl+N',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
mainWin.webContents.send('new-scene');
|
||||
}
|
||||
}));
|
||||
contextMenu.append(new MenuItem({
|
||||
label: '关闭',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+W' : 'Ctrl+W',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
browserWindow.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Menu.setApplicationMenu(contextMenu)
|
||||
|
||||
if (process.argv.indexOf("debug") > -1)
|
||||
mainWin.webContents.openDevTools()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
app.on('browser-window-created', (event, win) => {
|
||||
win.webContents.on('context-menu', (e, params) => {
|
||||
//menu.popup(win, params.x, params.y)
|
||||
})
|
||||
})
|
||||
ipcMain.on('ready', (event, menuType) => {
|
||||
//loadMostRecent()
|
||||
windowIsReady = true;
|
||||
});
|
||||
ipcMain.on('show-context-menu', (event, menuType) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
|
||||
// 根据窗口状态动态更新窗口菜单
|
||||
const isMaximized = win.isMaximized()
|
||||
windowSubmenu.clear()
|
||||
windowSubmenu.append(new MenuItem({
|
||||
label: isMaximized ? "恢复默认" : "最大化",
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+F' : 'Ctrl+F',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
if(!browserWindow.isMaximized()) {
|
||||
browserWindow.maximize();
|
||||
} else {
|
||||
browserWindow.unmaximize();
|
||||
}
|
||||
}
|
||||
}))
|
||||
windowSubmenu.append(new MenuItem({
|
||||
label: "最小化",
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+M' : 'Ctrl+M',
|
||||
click: (menuItem, browserWindow, event) => {
|
||||
browserWindow.minimize();
|
||||
}
|
||||
}))
|
||||
|
||||
contextMenu.popup(win)
|
||||
})
|
||||
ipcMain.on('save-scene', (event, filePath, stateCopy) => {
|
||||
console.log('save', filePath, stateCopy)
|
||||
let data = JSON.stringify(stateCopy);
|
||||
fs.writeFileSync(filePath, data);
|
||||
//const win = BrowserWindow.fromWebContents(event.sender)
|
||||
//menu.popup(win)
|
||||
})
|
||||
let dragState = {
|
||||
dragging: false
|
||||
}
|
||||
ipcMain.on('handle-paste', (event, w, h) => {
|
||||
handlePaste()
|
||||
})
|
||||
ipcMain.on('trigger-load', (event) => {
|
||||
dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'RefVroid 场景', extensions: ['purgif'] }
|
||||
]
|
||||
}).then(result => {
|
||||
if (!result.canceled) {
|
||||
readAndLoadFilePath(result.filePaths[0])
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
})
|
||||
ipcMain.on('trigger-save', (event) => {
|
||||
dialog.showSaveDialog({
|
||||
defaultPath: 'scene.purgif',
|
||||
filters: [
|
||||
{ name: 'RefVroid 场景', extensions: ['purgif'] }
|
||||
]
|
||||
}).then(result => {
|
||||
if (!result.canceled) {
|
||||
mainWin.webContents.send('save-scene', result.filePath)
|
||||
addToRecent(result.filePath)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
})
|
||||
ipcMain.on('trigger-close', (event) => {
|
||||
mainWin.close()
|
||||
})
|
||||
ipcMain.on('trigger-maximize', (event) => {
|
||||
if(!mainWin.isMaximized()) {
|
||||
mainWin.maximize()
|
||||
} else {
|
||||
mainWin.unmaximize()
|
||||
}
|
||||
})
|
||||
ipcMain.on('trigger-minimize', (event) => {
|
||||
mainWin.minimize()
|
||||
})
|
||||
ipcMain.on('show-help', (event) => {
|
||||
createHelpWindow(mainWin)
|
||||
})
|
||||
ipcMain.on('copy-text', (event, text) => {
|
||||
clipboard.writeText(text)
|
||||
console.log('Copied text to clipboard:', text)
|
||||
})
|
||||
ipcMain.on('copy-multiple-images', (event, imageDataURLs) => {
|
||||
try {
|
||||
console.log('Received', imageDataURLs.length, 'images to copy')
|
||||
|
||||
if (imageDataURLs.length === 1) {
|
||||
// 只有一张图片,直接复制
|
||||
const img = nativeImage.createFromDataURL(imageDataURLs[0])
|
||||
if (!img.isEmpty()) {
|
||||
clipboard.clear()
|
||||
clipboard.writeImage(img)
|
||||
console.log('Copied single image')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建临时文件夹
|
||||
const tempDir = path.join(app.getPath('temp'), 'RefVroid-copy')
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 清理旧文件
|
||||
try {
|
||||
const oldFiles = fs.readdirSync(tempDir)
|
||||
oldFiles.forEach(file => {
|
||||
try {
|
||||
fs.unlinkSync(path.join(tempDir, file))
|
||||
} catch (err) {
|
||||
// 忽略删除错误
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
// 忽略读取目录错误
|
||||
}
|
||||
|
||||
// 保存所有图片为临时文件
|
||||
const filePaths = []
|
||||
imageDataURLs.forEach((dataURL, index) => {
|
||||
try {
|
||||
const img = nativeImage.createFromDataURL(dataURL)
|
||||
if (!img.isEmpty()) {
|
||||
const filePath = path.join(tempDir, `image_${Date.now()}_${index + 1}.png`)
|
||||
fs.writeFileSync(filePath, img.toPNG())
|
||||
filePaths.push(filePath)
|
||||
console.log('Saved temp image:', filePath)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving image', index, ':', err)
|
||||
}
|
||||
})
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
console.log('No valid images to copy')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Copying', filePaths.length, 'files:', filePaths)
|
||||
|
||||
// 清空剪贴板
|
||||
clipboard.clear()
|
||||
|
||||
// 方法1: 使用 HTML 格式(QQ 支持)
|
||||
const htmlImages = filePaths.map(fp =>
|
||||
`<img src="file:///${fp.replace(/\\/g, '/')}" />`
|
||||
).join('')
|
||||
const html = `<html><body>${htmlImages}</body></html>`
|
||||
|
||||
// 方法2: 使用文件路径格式
|
||||
const fileList = filePaths.join('\n')
|
||||
|
||||
// 同时写入多种格式
|
||||
clipboard.write({
|
||||
text: fileList,
|
||||
html: html
|
||||
})
|
||||
|
||||
console.log('Copied', filePaths.length, 'images in multiple formats')
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error copying multiple images:', err)
|
||||
}
|
||||
})
|
||||
ipcMain.on('copy-image', (event, imagePath) => {
|
||||
try {
|
||||
let img = null
|
||||
|
||||
// 如果是 dataURL,直接写入
|
||||
if (imagePath.startsWith('data:')) {
|
||||
img = nativeImage.createFromDataURL(imagePath)
|
||||
console.log('Processing dataURL image, size:', img.getSize())
|
||||
|
||||
// 确保图片不为空
|
||||
if (img.isEmpty()) {
|
||||
console.error('Created image from dataURL is empty')
|
||||
clipboard.writeText('图片复制失败')
|
||||
return
|
||||
}
|
||||
} else if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
||||
// 网络图片暂时只能复制 URL
|
||||
clipboard.writeText(imagePath)
|
||||
console.log('Copied image URL to clipboard:', imagePath)
|
||||
return
|
||||
} else {
|
||||
// 本地文件路径
|
||||
// 处理可能的 file:// 协议
|
||||
let filePath = imagePath
|
||||
if (filePath.startsWith('file://')) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
// Windows 路径处理
|
||||
if (process.platform === 'win32' && filePath.startsWith('/')) {
|
||||
filePath = filePath.substring(1)
|
||||
}
|
||||
|
||||
console.log('Reading image from path:', filePath)
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error('File does not exist:', filePath)
|
||||
clipboard.writeText('文件不存在: ' + filePath)
|
||||
return
|
||||
}
|
||||
|
||||
img = nativeImage.createFromPath(filePath)
|
||||
console.log('Image loaded, size:', img.getSize())
|
||||
}
|
||||
|
||||
if (img && !img.isEmpty()) {
|
||||
// 清空剪贴板
|
||||
clipboard.clear()
|
||||
|
||||
// 写入图片到剪贴板
|
||||
clipboard.writeImage(img)
|
||||
|
||||
console.log('Successfully copied image to clipboard')
|
||||
console.log('Image size:', img.getSize())
|
||||
console.log('Image aspect ratio:', img.getAspectRatio())
|
||||
} else {
|
||||
console.log('Image is empty, copying error message')
|
||||
clipboard.writeText('图片为空,无法复制')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error copying image:', err)
|
||||
clipboard.writeText('复制图片时出错: ' + err.message)
|
||||
}
|
||||
})
|
||||
ipcMain.on('loaded-state', (event, filePath) => {
|
||||
addToRecent(filePath)
|
||||
})
|
||||
ipcMain.on('record-window-size', (event, w, h) => {
|
||||
width = mainWin.getSize()[0]
|
||||
height = mainWin.getSize()[1]
|
||||
})
|
||||
ipcMain.on('move-electron-window', (event, x, y, initPos) => {
|
||||
//var display = screen.getDisplayNearestPoint({x: x, y: y})
|
||||
//var dpiRespected = screen.dipToScreenPoint({x: x, y: y})
|
||||
//mainWin.setPosition(x,y)
|
||||
mainWin.setBounds({
|
||||
width: width,
|
||||
height: height,
|
||||
x: x - initPos.x,
|
||||
y: y - initPos.y
|
||||
});
|
||||
//win.setSize(width, height)
|
||||
//mainWin.setPosition(Math.round(x / 1.25) - Math.round(initPos.x / 1.25), Math.round(y / 1.25) - Math.round(initPos.y / 1.25))
|
||||
})
|
||||
let loopToLoad = function loopToLoad(){
|
||||
if(windowIsReady){
|
||||
console.log("loadMostRecent")
|
||||
//setTimeout(loadMostRecent, 700)
|
||||
//loadMostRecent()
|
||||
}
|
||||
else{
|
||||
console.log("not ready")
|
||||
setTimeout(loopToLoad, 100)
|
||||
}
|
||||
};
|
||||
loopToLoad();
|
||||
})
|
||||
|
||||
let windowIsReady = false;
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('will-quit', () => {
|
||||
globalShortcut.unregisterAll()
|
||||
})
|
||||
|
||||
function validateUrl(value) {
|
||||
return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(value);
|
||||
}
|
||||
12265
package-lock.json
generated
Normal file
12265
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
83
package.json
Normal file
83
package.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "refvroid",
|
||||
"version": "1.0.4",
|
||||
"description": "Pureref clone that has support for gifs/videos.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "小煜",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.4.0",
|
||||
"@electron-forge/maker-deb": "^7.4.0",
|
||||
"@electron-forge/maker-rpm": "^7.4.0",
|
||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||
"@electron-forge/maker-zip": "^7.4.0",
|
||||
"@electron-forge/maker-flatpak": "^7.4.0",
|
||||
"@electron-forge/maker-dmg": "^7.4.0",
|
||||
"electron": "30.0.1",
|
||||
"@electron/rebuild": "^3.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dombo": "^3.2.0",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-store": "^8.0.1",
|
||||
"nouislider": "^15.5.0",
|
||||
"try-require": "^1.2.1"
|
||||
},
|
||||
"config": {
|
||||
"forge": {
|
||||
"packagerConfig": {},
|
||||
"make_targets": {
|
||||
"win32": [
|
||||
"squirrel"
|
||||
],
|
||||
"darwin": [
|
||||
"dmg"
|
||||
],
|
||||
"linux": [
|
||||
"deb",
|
||||
"rpm",
|
||||
"flatpak"
|
||||
]
|
||||
},
|
||||
"makers": [
|
||||
{
|
||||
"name": "@electron-forge/maker-squirrel",
|
||||
"config": {
|
||||
"name": "refvroid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-zip"
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-flatpak",
|
||||
"config": {
|
||||
"options": {
|
||||
"categories": [
|
||||
"Graphics"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-deb",
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-rpm",
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-dmg",
|
||||
"config": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1183
preload.js
Normal file
1183
preload.js
Normal file
File diff suppressed because it is too large
Load Diff
186
renderer.js
Normal file
186
renderer.js
Normal file
@@ -0,0 +1,186 @@
|
||||
|
||||
const sliderElement = document.querySelector('#slider')
|
||||
noUiSlider.create(sliderElement, {
|
||||
start: [0, 100],
|
||||
connect: true,
|
||||
behaviour: 'unconstrained-tap',
|
||||
range: {
|
||||
'min': 0,
|
||||
'max': 100
|
||||
}
|
||||
});
|
||||
slider.noUiSlider.on('start', function () {
|
||||
|
||||
});
|
||||
|
||||
let scalingWithTouch = false
|
||||
let lastDelta = 0
|
||||
|
||||
let initTouches = []
|
||||
//ontouchstart
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
if (e.touches.length > 1) {
|
||||
scalingWithTouch = true
|
||||
initTouches = e.touches
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('touchend', function (e) {
|
||||
if (e.touches.length < 2) {
|
||||
scalingWithTouch = false
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('touchmove', function (e) {
|
||||
if (scalingWithTouch) {
|
||||
//zoom
|
||||
// delta between e.touches[1], e.touches[0] and initTouches[1], initTouches[0]
|
||||
let delta = Math.sqrt(Math.pow(e.touches[0].clientX - e.touches[1].clientX, 2) + Math.pow(e.touches[0].clientY - e.touches[1].clientY, 2)) - Math.sqrt(Math.pow(initTouches[0].clientX - initTouches[1].clientX, 2) + Math.pow(initTouches[0].clientY - initTouches[1].clientY, 2))
|
||||
let deltaDiff = delta - lastDelta
|
||||
deltaDiff = deltaDiff / 100
|
||||
currentScale = parseFloat(document.body.dataset.currentScale) || 1
|
||||
const nextScale = Math.max(currentScale + deltaDiff * (currentScale / 2), 0.01)
|
||||
console.log("touchmove", deltaDiff, currentScale, nextScale)
|
||||
e.clientX = (initTouches[0].clientX + initTouches[0].clientX) / 2
|
||||
e.clientY = (initTouches[0].clientY + initTouches[0].clientY) / 2
|
||||
//zoom(nextScale, e) // this is not working
|
||||
lastDelta = delta
|
||||
}
|
||||
}, true);
|
||||
|
||||
const factor = 0.1
|
||||
document.documentElement.addEventListener("wheel", (e) => {
|
||||
let delta = e.wheelDelta / 120
|
||||
if (e.ctrlKey) { // is pinch zoom on touchpad(idk why it's ctrlKey but it is)
|
||||
delta = e.deltaY * factor
|
||||
}
|
||||
if (document.querySelector('.editVideo')) return;
|
||||
currentScale = parseFloat(document.body.dataset.currentScale) || 1
|
||||
|
||||
const nextScale = Math.max(currentScale + delta * (currentScale / 2), 0.01)
|
||||
console.log("wheel", delta, currentScale, nextScale)
|
||||
zoom(nextScale, e)
|
||||
|
||||
})
|
||||
|
||||
const zoom = (nextScale, event) => {
|
||||
currentScale = parseFloat(document.body.dataset.currentScale) || 1
|
||||
|
||||
const ratio = 1 - nextScale / currentScale
|
||||
|
||||
const {
|
||||
clientX,
|
||||
clientY
|
||||
} = event
|
||||
|
||||
let translateX = (parseFloat(document.body.dataset.translateX) || 0);
|
||||
let translateY = (parseFloat(document.body.dataset.translateY) || 0);
|
||||
|
||||
translateX += (clientX - translateX) * ratio
|
||||
translateY += (clientY - translateY) * ratio
|
||||
console.log('zoom')
|
||||
|
||||
//console.log(translateX, translateY, nextScale, ratio, currentScale)
|
||||
currentScale = nextScale
|
||||
myAPI.updateScaleAndTranslate(currentScale, { translateX, translateY })
|
||||
}
|
||||
|
||||
/**
|
||||
* Youtube Embed Section
|
||||
*/
|
||||
|
||||
var player;
|
||||
function onYouTubeIframeAPIReady() { } // idk why it breaks without this
|
||||
|
||||
function onPlayerReady(event) {
|
||||
event.target.setVolume(0);
|
||||
event.target.mute();
|
||||
|
||||
// lifts youtube video to last element at the root of body
|
||||
event.target.getIframe().contentDocument.body.appendChild(player.getIframe().contentDocument.querySelector('video'))
|
||||
|
||||
event.target.playVideo();
|
||||
}
|
||||
|
||||
|
||||
var done = false;
|
||||
function onPlayerStateChange(event) {
|
||||
if (event.data == YT.PlayerState.PLAYING && !done) {
|
||||
//setTimeout(stopVideo, 6000);
|
||||
let videoElement = event.target.getIframe().contentDocument.querySelector('video')
|
||||
videoElement.loop = true
|
||||
/*
|
||||
videoElement.addEventListener('timeupdate', function(){
|
||||
//if im the editvideo
|
||||
if(document.querySelector(`.editVideo[data-idcode="${this.dataset.idcode}"]`)){
|
||||
onPlayerProgress.apply(this, arguments)
|
||||
}
|
||||
})*/
|
||||
|
||||
let videoWidth = videoElement.videoWidth
|
||||
let videoHeight = videoElement.videoHeight
|
||||
//console.log('inside onPlayerStateChange', videoWidth, videoHeight)
|
||||
let idcode = event.target.getIframe().parentElement.dataset.idcode
|
||||
|
||||
videoElement.dataset.idcode = idcode
|
||||
//console.log('after onPlayerStateChange', videoElement.dataset.idcode, videoWidth, videoHeight)
|
||||
myAPI.updateYoutubeOriginalSize(idcode, videoWidth, videoHeight)
|
||||
//event.target.getIframe().parentElement.style.width = videoWidth + 'px'
|
||||
//event.target.getIframe().parentElement.style.height = videoHeight + 'px'
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
function stopVideo() {
|
||||
player.stopVideo();
|
||||
}
|
||||
const targetNode = document.getElementById('eventTrigger');
|
||||
const config = { attributes: true };
|
||||
const callback = function (mutationsList, observer) {
|
||||
// Use traditional 'for loops' for IE 11
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
console.log('A child node has been added or removed.');
|
||||
}
|
||||
else if (mutation.type === 'attributes') {
|
||||
console.log('The ' + mutation.attributeName + ' attribute was modified.');
|
||||
if (mutation.attributeName == 'data-youtubetrigger') {
|
||||
embedYoutubeVideo()
|
||||
} else if (mutation.attributeName == 'data-changesliders') {
|
||||
//console.log(mutation)
|
||||
//
|
||||
let changesliders = JSON.parse(mutation.target.dataset.changesliders)
|
||||
slider.noUiSlider.set(changesliders);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
const observer = new MutationObserver(callback);
|
||||
// Start observing the target node for configured mutations
|
||||
observer.observe(targetNode, config);
|
||||
function embedYoutubeVideo() {
|
||||
var playerNeedsSetup = document.querySelector('.playerNeedsSetup');
|
||||
if (!playerNeedsSetup) {
|
||||
console.error('embedYoutubeVideo called without player div to setup')
|
||||
};
|
||||
playerNeedsSetup.classList.remove('playerNeedsSetup')
|
||||
var iframediv = playerNeedsSetup.querySelector('.iframeDiv')
|
||||
var code = playerNeedsSetup.dataset.idcode
|
||||
|
||||
//document.querySelector()
|
||||
player = new YT.Player(iframediv, {
|
||||
videoId: code,
|
||||
playerVars: {
|
||||
//'playsinline': 1
|
||||
'controls': 0,
|
||||
'disablekb': 1,
|
||||
'enablejsapi': 1,
|
||||
'fs': 0,
|
||||
'loop': 1
|
||||
},
|
||||
events: {
|
||||
'onReady': onPlayerReady,
|
||||
'onStateChange': onPlayerStateChange
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user