原本的UI确实有那么一点点的丑,拿claude3.7重新糊了个UI,作者大大可看看是否有什么问题
(因为是AI一把梭哈,所以UI可能还是有点小问题的)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据中转站</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=SF+Pro+Display:wght@300;400;500;600&display=swap');
:root {
--background-light: #ffffff;
--text-light: #1d1d1f;
--secondary-light: #6e6e73;
--border-light: rgba(0, 0, 0, 0.1);
--card-hover-light: rgba(0, 0, 0, 0.03);
--card-select-light: rgba(0, 0, 0, 0.05);
--tab-inactive-light: rgba(0, 0, 0, 0.05);
--button-hover-light: rgba(0, 0, 0, 0.06);
--accent-light: #0071e3;
--danger-light: #ff3b30;
--shadow-light: 0 1px 5px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
/* Dark mode colors */
--background-dark: #1d1d1f;
--text-dark: #f5f5f7;
--secondary-dark: #a1a1a6;
--border-dark: rgba(255, 255, 255, 0.15);
--card-hover-dark: rgba(255, 255, 255, 0.05);
--card-select-dark: rgba(255, 255, 255, 0.08);
--tab-inactive-dark: rgba(255, 255, 255, 0.08);
--button-hover-dark: rgba(255, 255, 255, 0.1);
--accent-dark: #2997ff;
--danger-dark: #ff453a;
--shadow-dark: 0 1px 5px rgba(0, 0, 0, 0.2);
/* Default theme */
--background: var(--background-light);
--text: var(--text-light);
--secondary: var(--secondary-light);
--border: var(--border-light);
--card-hover: var(--card-hover-light);
--card-select: var(--card-select-light);
--tab-inactive: var(--tab-inactive-light);
--button-hover: var(--button-hover-light);
--accent: var(--accent-light);
--danger: var(--danger-light);
--shadow: var(--shadow-light);
/* Animation timing */
--transition-fast: 0.15s;
--transition-medium: 0.25s;
--transition-slow: 0.4s;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Border radius */
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 8px;
}
.dark::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
.dark::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
::-webkit-scrollbar-track {
background: transparent;
}
html,
body {
margin: 0;
height: 100%;
width: 100%;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--text);
background-color: var(--background);
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color var(--transition-medium), color var(--transition-medium);
}
body {
display: flex;
flex-direction: column;
}
#tab-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-xs) var(--spacing-sm);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.8);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
.dark #tab-bar {
background-color: rgba(29, 29, 31, 0.85);
}
#tab-bar,
#tool-menu {
border-bottom: 1px solid var(--border);
transition: border-color var(--transition-medium);
}
#tool-menu {
display: none;
padding: var(--spacing-xs) var(--spacing-md);
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.dark #tool-menu {
background-color: rgba(29, 29, 31, 0.85);
}
body.manage #tool-menu {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
#tab-head-container {
display: flex;
gap: var(--spacing-xs);
align-items: center;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
#tab-head-container::-webkit-scrollbar {
display: none;
}
#tab-plus,
.tab-head {
border-radius: var(--radius-md);
padding: var(--spacing-xs) var(--spacing-sm);
transition: background-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-medium);
border: none;
display: flex;
align-items: center;
cursor: pointer;
height: 28px;
user-select: none;
}
#tab-plus {
background-color: var(--tab-inactive);
color: var(--secondary);
font-weight: 500;
font-size: 16px;
justify-content: center;
min-width: 28px;
}
.tab-head:not(.active) {
background-color: var(--tab-inactive);
color: var(--secondary);
}
.tab-head.active {
background-color: var(--accent);
color: white;
font-weight: 500;
box-shadow: 0 3px 10px rgba(0, 113, 227, 0.3);
}
.dark .tab-head.active {
box-shadow: 0 3px 10px rgba(41, 151, 255, 0.3);
}
#tab-plus:hover,
.tab-head:not(.active):hover {
background-color: var(--button-hover);
transform: translateY(-1px);
}
.tab-head.active:hover {
transform: translateY(-1px);
}
.tab-head.active:active,
#tab-plus:active,
.tab-head:not(.active):active {
transform: translateY(0);
}
body.manage {
.card {
padding-right: var(--spacing-md);
position: relative;
.card-extra {
display: none;
}
}
.card-close-button {
position: absolute;
right: var(--spacing-xs);
top: 50%;
transform: translateY(-50%);
opacity: 0.7;
transition: opacity var(--transition-fast);
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: var(--card-hover);
&:hover {
opacity: 1;
background-color: var(--danger);
color: white;
}
}
.tab-head-close-button {
margin-left: var(--spacing-xs);
opacity: 0.7;
transition: opacity var(--transition-fast), transform var(--transition-fast);
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
&:hover {
opacity: 1;
transform: scale(1.1);
background-color: rgba(255, 255, 255, 0.3);
}
}
.tab-head-close-button::after,
.card-close-button::after {
content: "×";
font-size: 16px;
line-height: 1;
cursor: pointer;
}
}
.button-control,
.text-control {
border-radius: var(--radius-md);
padding: var(--spacing-xs) var(--spacing-sm);
background-color: transparent;
transition: background-color var(--transition-fast), transform var(--transition-fast);
user-select: none;
height: 28px;
display: flex;
align-items: center;
cursor: pointer;
}
.text-control {
padding-right: var(--spacing-md);
}
.tab-head span {
outline: none;
margin-right: var(--spacing-xs);
}
#settings-menu {
border-left: 1px solid var(--border);
padding: var(--spacing-md);
min-width: 200px;
background-color: var(--background);
transition: border-color var(--transition-medium);
}
.emoji {
font-family: 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
margin-right: 4px;
}
.text-control:hover,
.button-control:hover {
background-color: var(--button-hover);
transform: translateY(-1px);
}
.text-control:active,
.button-control:active {
transform: translateY(0);
}
#trash-button.dragover {
background-color: var(--danger);
color: white;
transform: scale(1.1);
}
.tab-content {
overflow-y: auto;
flex: 1;
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
transition: all var(--transition-medium);
}
.tab-content:not(.active) {
display: none;
}
.card.selected {
background-color: var(--card-select);
border-left: 3px solid var(--accent);
}
.card.selected:hover {
background-color: var(--card-hover);
}
.card.selected[draggable=true] {
cursor: grab;
}
.card {
position: relative;
border-radius: var(--radius-md);
background-color: var(--background);
box-shadow: var(--shadow);
max-height: max(50%, 100px);
box-sizing: border-box;
overflow: hidden;
display: flex;
transition: transform var(--transition-fast),
box-shadow var(--transition-medium),
background-color var(--transition-fast),
border-left var(--transition-fast);
border-left: 3px solid transparent;
}
.card:hover {
background-color: var(--card-hover);
transform: translateY(-2px);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
}
.dark .card:hover {
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
}
.card-checkbox {
margin: var(--spacing-md);
appearance: none;
width: 18px;
height: 18px;
border-radius: 4px;
border: 1.5px solid var(--secondary);
background-color: var(--background);
position: relative;
cursor: pointer;
transition: all var(--transition-fast);
&:checked {
background-color: var(--accent);
border-color: var(--accent);
&::after {
content: "";
position: absolute;
top: 3px;
left: 6px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
&:hover {
border-color: var(--accent);
}
}
.card-wrapper {
width: 100%;
box-sizing: border-box;
overflow-y: auto;
padding: var(--spacing-md);
flex: 1;
.card-content {
outline: none;
width: 100%;
padding-block: var(--spacing-xs);
transition: all var(--transition-medium);
* {
pointer-events: none;
}
}
.card-extra:empty {
display: none;
}
}
.card.text {
.card-content {
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Monaco, monospace;
white-space: pre-wrap;
word-break: break-all;
font-size: 13px;
}
}
.card.html {
.card-content * {
margin: 0 !important
}
}
body:not(.manage) {
.card.file .card-content:hover {
color: var(--accent);
}
}
.card.file {
.card-content {
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Monaco, monospace;
white-space: nowrap;
overflow-x: auto;
font-size: 13px;
display: flex;
align-items: center;
&.file::before,
&.non-exist::before {
content: "📄";
margin-right: var(--spacing-sm);
font-size: 16px;
}
&.directory::before {
content: "📁";
margin-right: var(--spacing-sm);
font-size: 16px;
}
&.directory.open::before {
content: "📂";
margin-right: var(--spacing-sm);
font-size: 16px;
}
&.non-exist {
text-decoration: line-through;
opacity: 0.7;
}
&::-webkit-scrollbar {
height: 4px;
}
}
.card-extra {
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Monaco, monospace;
white-space: nowrap;
overflow-y: auto;
margin-top: var(--spacing-sm);
padding: var(--spacing-sm);
border-radius: var(--radius-sm);
border-top: 1px solid var(--border);
max-height: 250px;
background-color: rgba(0, 0, 0, 0.02);
transition: border-color var(--transition-medium);
.dark & {
background-color: rgba(255, 255, 255, 0.05);
}
img {
max-width: 100%;
max-height: 100%;
border-radius: var(--radius-sm);
object-fit: contain;
}
.card:last-of-type {
border-bottom: none;
}
}
}
#card-container {
display: flex;
flex: 1;
overflow-y: auto;
}
.config-option {
display: flex;
align-items: center;
padding: var(--spacing-xs) 0;
cursor: pointer;
transition: color var(--transition-fast);
&:hover {
color: var(--accent);
}
}
select.config-option[size] {
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background-color: var(--background);
color: var(--text);
outline: none;
padding: var(--spacing-xs);
margin: var(--spacing-xs) 0 var(--spacing-md) 0;
cursor: pointer;
transition: all var(--transition-medium);
&:focus {
border-color: var(--accent);
}
option {
padding: var(--spacing-xs);
}
}
label {
display: block;
margin: var(--spacing-sm) 0 var(--spacing-xs) 0;
font-weight: 500;
}
input[type="checkbox"] {
appearance: none;
width: 18px;
height: 18px;
border-radius: 4px;
border: 1.5px solid var(--secondary);
margin-right: var(--spacing-sm);
position: relative;
cursor: pointer;
transition: all var(--transition-fast);
background-color: var(--background);
&:checked {
background-color: var(--accent);
border-color: var(--accent);
&::after {
content: "";
position: absolute;
top: 3px;
left: 6px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
&:hover {
border-color: var(--accent);
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(0, 113, 227, 0.3);
}
}
.dark input[type="checkbox"] {
background-color: var(--background-dark);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.card {
animation: slideIn var(--transition-medium) ease-out;
}
.tab-head {
animation: fadeIn var(--transition-fast) ease-out;
}
</style>
<style>
.dark {
--background: var(--background-dark);
--text: var(--text-dark);
--secondary: var(--secondary-dark);
--border: var(--border-dark);
--card-hover: var(--card-hover-dark);
--card-select: var(--card-select-dark);
--tab-inactive: var(--tab-inactive-dark);
--button-hover: var(--button-hover-dark);
--accent: var(--accent-dark);
--danger: var(--danger-dark);
--shadow: var(--shadow-dark);
}
</style>
</head>
<body>
<nav id="tab-bar">
<div id="tab-head-container">
<div id="tab-plus">+</div>
</div>
<div style="display: flex; gap: var(--spacing-xs);">
<div class="button-control emoji" id="trash-button" title="点击删除选中条目 或拖动到这里删除">🗑</div>
<div class="button-control emoji" id="settings-trigger"
onclick="document.querySelector('#settings-menu').hidden^=1" title="设置">⚙
</div>
<div class="button-control emoji" id="edit-toggle" title="编辑内容" onclick="toggleManage()">📝</div>
<div class="button-control emoji" id="tool-refreshfile" title="刷新文件状态">🔄</div>
<div class="button-control emoji" id="tool-readclipboard" title="添加剪贴板条目">➕</div>
</div>
</nav>
<div id="tool-menu" hidden>
<div class="text-control emoji" id="tool-clearformat">🧹 清除格式</div>
<div class="text-control emoji" id="tool-cardjoin">🔗 合并</div>
</div>
<div style="display:flex;flex:1;overflow-y:auto;">
<div id="card-container"></div>
<div id="settings-menu" style="overflow-y:auto;" hidden>
<label class="config-option"><input type="checkbox" id="config-richin" checked>拖入保留格式</label>
<label class="config-option"><input type="checkbox" id="config-richout" checked>拖出保留格式</label>
<label class="config-option"><input type="checkbox" id="config-copynotify">复制后提示消息</label>
<label class="config-option"><input type="checkbox" id="config-droponjoin">合并后删除原卡片</label>
<label class="config-option"><input type="checkbox" id="config-autopreview" checked>拖入文件自动预览</label>
<label>单击效果</label>
<select class="config-option" id="config-clickevent" size="2">
<option value="none" selected>无</option>
<option value="copy">复制</option>
</select>
<label>双击效果——文本</label>
<select class="config-option" id="config-text-dblclick" size="3">
<option value="none">无</option>
<option value="copy">复制</option>
<option value="edit" selected>编辑</option>
</select>
<label>双击效果——文件</label>
<select class="config-option" id="config-file-dblclick" size="5">
<option value="none">无</option>
<option value="copy">复制</option>
<option value="open">打开</option>
<option value="preview" selected>预览</option>
<option value="locate">定位</option>
</select>
<label class="config-option"><input type="checkbox" id="config-previewfallback" checked>预览失败时打开文件</label>
<label>拖放效果</label>
<select class="config-option" id="config-dropeffect" size="4">
<option value="all" selected>自动</option>
<option value="copy">复制</option>
<option value="move">移动</option>
<option value="link">链接</option>
</select>
<label>主题颜色</label>
<select class="config-option" id="config-colorscheme" size="3" onchange="updateTheme()">
<option value="light">亮</option>
<option value="dark">暗</option>
<option value="system" selected>系统</option>
</select>
</div>
</div>
<script>
function createElement(tagName, property = {}) {
let ele = document.createElement(tagName);
for (let attribute in property) {
if (attribute == "style") {
for (let key in property.style) {
ele.style[key] = property.style[key]
}
} else {
if (attribute) ele[attribute] = property[attribute]
}
}
return ele
}
function getDateString(date) {
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}_${date.getHours()}${date.getMinutes()}${date.getSeconds()}`
}
function getRandomId(prefix) {
let id = prefix + "-" + Math.random().toString(36).slice(2);
if (document.getElementById(id)) return getRandomId(prefix);
return id;
}
function getFullPathAsync(file) {
let timestamp = +new Date();
let { resolve, reject, promise } = Promise.withResolvers();
let listener = function (e) {
if (e.data.op == "customMessage" && e.data.action == "responseFullPath" && e.data.timestamp == timestamp) {
chrome.webview.removeEventListener("message", listener);
resolve(e.data.paths[0]);
}
}
chrome.webview.addEventListener("message", listener);
try {
chrome.webview.postMessageWithAdditionalObjects({
op: "customMessage", action: "requestFullPath", timestamp
}, [file]);
} catch (e) { reject(e); }
return promise;
}
function getFileSystemHandlerAsync(path) {
let timestamp = +new Date();
let { resolve, reject, promise } = Promise.withResolvers();
let listener = function (e) {
if (e.data.op == "customMessage" && e.data.action == "responseFile" && e.data.timestamp == timestamp) {
chrome.webview.removeEventListener("message", listener);
resolve(e.additionalObjects[0]);
}
}
chrome.webview.addEventListener("message", listener);
chrome.webview.postMessage({
op: "customMessage", action: "requestFile", timestamp, paths: [path]
});
return promise;
}
async function saveTempfileAsync(file) {
let { promise, resolve, reject } = Promise.withResolvers();
const reader = new FileReader();
reader.onload = (event) => resolve(event.target.result);
reader.onerror = (error) => reject(error);
reader.readAsDataURL(file)
let data = await promise;
let { path } = await $quickerSp('保存临时文件', {
data: data.split(",").pop(),
ext: file.type.split("/").pop()
});
return path;
}
async function getFileInfoAsync(path) {
let res = await $quickerSp('获取文件信息', { path });
res.type = !res.exists ? "non-exist" : res.isDirectory ? "directory" : "file";
return res;
}
function notify(message, type = "Info") {
$quickerSp('提示消息', { type, message });
}
// Extend Array with fromAsync utility
Array.fromAsync = async function(items, mapFn) {
const results = [];
for (const item of items) {
results.push(await mapFn(item));
}
return results;
};
</script>
<script>
function createTab(name) {
let id = "tab-" + Math.random().toString(36).slice(2);
if (document.getElementById(id)) return createTab(name);
let thContainer = document.querySelector('#tab-head-container');
if (!name) name = "标签" + thContainer.childElementCount;
let tbPlus = document.querySelector('#tab-plus');
let tabHead = createElement('div', {
id, className: 'tab-head',
draggable: true,
ondragstart: function (e) {
e.dataTransfer.setData("text/plain", this.innerText);
e.dataTransfer.setData("datatransfer/tab", id);
e.dataTransfer.effectAllowed = "all";
e.dataTransfer.setDragImage(new Image(), 0, 0);
},
});
tbPlus.before(tabHead);
tabHead.append(createElement('span', {
className: 'tab-head-content',
innerText: name,
onblur: function (e) {
if (this.innerText == "") this.innerText = "标签";
},
onkeydown: function (e) {
if (e.key == "Enter") {
e.preventDefault();
this.blur();
}
}
}));
tabHead.append(createElement('span', {
className: 'tab-head-close-button',
onclick: (e) => {
e.stopPropagation();
tabHead.dispose();
}
}));
let tabContent = createElement('div', { className: 'tab-content' });
document.querySelector('#card-container').append(tabContent);
tabHead.contentDiv = tabContent;
tabContent.headDiv = tabHead;
tabHead.ondragover = function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
};
tabHead.ondrop = function (e) {
e.preventDefault();
let data;
data = e.dataTransfer.getData("datatransfer/tab");
if (data) {
let targetTab = e.target.closest('.tab-head');
let draggingElement = document.getElementById(data);
let appendFunc = e.offsetX < targetTab.clientWidth / 2 ? "before" : "after";
targetTab[appendFunc](draggingElement);
draggingElement.activate();
return
}
};
tabHead.dispose = tabContent.dispose = function () {
if (tabHead.classList.contains("active")) {
if (tabHead.previousElementSibling) tabHead.previousElementSibling.activate();
else if (tabHead.nextElementSibling != tbPlus) tabHead.nextElementSibling.activate();
}
// Add slide out animation before removal
tabHead.style.transition = "all 0.3s";
tabContent.style.transition = "all 0.3s";
tabHead.style.transform = "translateY(-20px)";
tabHead.style.opacity = "0";
tabContent.style.transform = "translateY(20px)";
tabContent.style.opacity = "0";
setTimeout(() => {
tabHead.remove();
tabContent.remove();
if (!document.querySelector('.tab-head.active')) createTab().activate();
}, 300);
};
tabContent.activate = tabHead.activate = tabHead.onclick = function () {
document.querySelectorAll('.tab-head.active').forEach(e => {
if (e != tabHead) e.classList.remove('active')
});
document.querySelectorAll('.tab-content').forEach(e => {
if (e != tabContent) {
e.classList.remove('active');
e.querySelectorAll('.card.selected').forEach(cd => cd.classList.remove("selected"));
}
});
tabHead.classList.add('active');
tabContent.classList.add('active');
};
tabHead.ondragenter = function (e) {
let activeHead = document.querySelector('.tab-head.active')
let activeContent = document.querySelector('.tab-content.active')
if (activeHead != tabHead) {
activeHead.classList.remove('active');
tabHead.classList.add('active');
}
if (activeContent != tabContent) {
activeContent.classList.remove('active');
tabContent.classList.add('active');
}
}
tabContent.ondrop = async function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
targetCard = e.target.closest('.card');
if (e.dataTransfer.types.includes("datatransfer/card") && e.dataTransfer.effectAllowed != "copy") {
let elements = e.dataTransfer.getData("datatransfer/card").split("\n").map(x => document.getElementById(x));
if (targetCard) {
let appendFunc = e.offsetY < targetCard.clientHeight / 2 ? "before" : "after";
targetCard[appendFunc](...elements);
} else {
tabContent.append(...elements);
}
} else {
if (targetCard) {
let appendFunc = e.offsetY < targetCard.clientHeight / 2 ? "before" : "after";
targetCard[appendFunc](...await createCardFromDataTransfer(e.dataTransfer));
} else {
tabContent.append(...await createCardFromDataTransfer(e.dataTransfer));
}
}
};
tabContent.ondragover = function (e) {
e.preventDefault();
if (e.dataTransfer.types.includes("datatransfer/card")) {
e.dataTransfer.dropEffect = "all";
} else {
e.dataTransfer.dropEffect = "copy";
}
};
tabContent.onclick = function (e) {
if (e.target == e.currentTarget) document.querySelectorAll('.card.selected').forEach(ele => ele.selected = false);
};
return tabHead;
}
</script>
<script>
let editing = false;
function toggleManage() {
editing = !editing;
document.body.classList.toggle('manage', editing);
for (let card of document.querySelectorAll('.card')) {
card.editable = editing;
}
for (let ele of document.querySelectorAll('.tab-head-content')) {
if (!editing) {
ele.contentEditable = false;
} else {
ele.contentEditable = "plaintext-only";
}
}
}
function getSelectedCardData() {
let selectedCards = Array.from(document.querySelector('.tab-content.active').querySelectorAll('.card.selected'));
let data = {
"datatransfer/card": selectedCards.map(ele => ele.id).join("\n"),
};
if (selectedCards.some(x => x.type == "file")) {
data["Files"] = selectedCards.filter(ele => ele.type == "file").map(ele => ele.contentDiv.innerText);
}
data["text/plain"] = selectedCards.map(ele => ele.contentDiv.innerText).join("\n");
if (config.richout) {
data["text/html"] = selectedCards.map(ele => ele.contentDiv.innerHTML).join("<br>");
}
return data;
}
function getSelectedCardJoin() {
let selectedCards = Array.from(document.querySelector('.tab-content.active').querySelectorAll('.card.selected'));
if (selectedCards.some(x => x.type == "html")) {
return createHTMLCard(selectedCards.map(x => x.contentDiv.innerHTML).join("<br>"));
} else {
return createTextCard(selectedCards.map(x => x.contentDiv.innerText).join("\n"));
}
}
function selectCard(card, dropOthers = false) {
if (dropOthers) document.querySelectorAll('.card.selected').forEach(ele => ele.selected = false);
card.selected = true;
}
function onCardDragStart(e) {
selectCard(e.currentTarget, document.querySelectorAll('.card.selected').length <= 1 && !e.currentTarget.selected && !e.ctrlKey);
e.preventDefault();
let effect = e.ctrlKey ? "copy" : config.dropeffect;
chrome.webview.postMessage({
op: "customMessage", action: "doDragDrop",
effect, data: getSelectedCardData()
});
}
async function onCardClick(e) {
if (editing) return;
e.stopPropagation();
selectCard(e.currentTarget, !e.ctrlKey);
if (config.clickevent == "copy") {
clipboardWrite("copy");
}
}
async function onCardContextMenu(e) {
if (editing) return;
e.preventDefault();
e.stopPropagation();
let activeCard = e.currentTarget;
selectCard(activeCard, document.querySelectorAll('.card.selected').length <= 1 && !activeCard.selected && !e.ctrlKey);
let selectedCards = Array.from(document.querySelector('.tab-content.active').querySelectorAll('.card.selected'));
let types = cardTypes.filter(x => selectedCards.some(ele => ele.type == x));
let menuData = [
"[fa:Regular_Copy]复制|复制",
"[fa:Regular_Trash]删除|删除",
];
let paths = selectedCards.filter(x => x.type == "file").map(x => x.contentDiv.innerText);
if (selectedCards.length == 1) {
if (activeCard.type == "html") {
menuData.push("[fa:Solid_Edit]编辑HTML源码|编辑HTML源码");
}
if (activeCard.type == "file") {
let contentDiv = activeCard.contentDiv
let { exists, isDirectory } = await getFileInfoAsync(contentDiv.innerText);
if (exists) {
menuData.push("[fa:Solid_PaperPlane]用默认打开方式打开|打开文件");
menuData.push("[fa:Regular_FolderOpen]在资源管理器中定位|在资源管理器中定位文件");
}
if (isDirectory) {
if (contentDiv.classList.contains("open"))
menuData.push("[fa:Regular_FolderTree]收起|收起文件夹");
else
menuData.push("[fa:Regular_FolderTree]列出文件|展开文件夹");
}
}
}
menuData.push("[fa:Regular_ArrowCircleRight]转换为");
if (types.some(x => x != "text")) {
menuData.push(" [fa:Regular_Text]纯文本|转换为纯文本");
}
if (types.some(x => x != "html")) {
menuData.push(" [fa:Regular_Globe]HTML|转换为HTML");
}
if (types.some(x => x != "file")) {
menuData.push(" [fa:Regular_File]文件|转换为文件");
}
if (selectedCards.length > 1) {
menuData.push("[fa:Regular_Archive]合并|合并");
}
menuData.push("[fa:Regular_Text]将文本另存为|保存文本");
if (types.includes("file")) {
menuData.push("[fa:Regular_FileAlt]文件操作");
menuData.push(" [fa:Brands_Windows]资源管理器菜单|资源管理器菜单");
menuData.push(" [fa:Regular_Copy]将所选文件(夹)复制到|文件复制到");
menuData.push(" [fa:Regular_FolderOpen]将所选文件(夹)移动到|文件移动到");
}
let { action } = await $quickerSp('右键菜单', { menuData });
if (action == "复制") {
clipboardWrite("copy");
} else if (action == "转换为纯文本") {
selectedCards.forEach(ele => ele?.assign?.("type", "text"));
} else if (action == "转换为文件") {
selectedCards.forEach(ele => ele?.assign?.("type", "file"));
} else if (action == "转换为HTML") {
selectedCards.forEach(ele => ele?.assign?.("type", "html"));
} else if (action == "删除") {
// Add animation before removal
for (let card of selectedCards) {
card.style.transition = "all 0.3s";
card.style.transform = "translateX(50px)";
card.style.opacity = "0";
}
// Remove after animation completes
setTimeout(() => {
selectedCards.forEach(ele => ele?.remove());
}, 300);
} else if (action == "编辑") {
selectedCards.forEach(ele => ele?.editContent());
} else if (action == "编辑HTML源码") {
selectedCards.forEach(ele => ele?.editSource());
} else if (action == "展开文件夹") {
selectedCards.forEach(ele => ele?.assign?.("open", true));
} else if (action == "收起文件夹") {
selectedCards.forEach(ele => ele?.assign?.("open", false));
} else if (action == "打开文件") {
$quickerSp('运行或打开', { path: paths });
} else if (action == "在资源管理器中定位文件") {
$quickerSp('在资源管理器中定位文件', { path: paths });
} else if (action == "资源管理器菜单") {
$quickerSp('资源管理器菜单', { paths });
} else if (action == "文件复制到") {
$quickerSp('文件和目录操作', { paths, action: "copy" });
} else if (action == "文件移动到") {
$quickerSp('文件和目录操作', { paths, action: "move" });
} else if (action == "保存文本") {
$quickerSp('将文本另存为', { text: selectedCards.map(x => x.contentDiv.innerText).join("\n") });
} else if (action == "合并") {
const newCard = await getSelectedCardJoin();
activeCard.before(newCard);
// Add slide-in animation for new card
newCard.style.transform = "translateY(20px)";
newCard.style.opacity = "0";
setTimeout(() => {
newCard.style.transform = "translateY(0)";
newCard.style.opacity = "1";
}, 10);
if (config.droponjoin) {
// Add slide-out animation before removal
for (let card of selectedCards) {
card.style.transition = "all 0.3s";
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
}
// Remove after animation completes
setTimeout(() => {
selectedCards.forEach(ele => ele.remove());
}, 300);
}
}
}
async function clipboardWrite(effect = "all") {
await chrome.webview.postMessage({
op: "customMessage", action: "doCopy",
effect, data: getSelectedCardData()
});
if (config.copynotify) {
notify("已复制到剪贴板", "Success");
}
}
async function createDirectoryDiv(path) {
let div = createElement('div', { className: 'items', innerText: path });
path = path.replace(/\\+$/g, "");
let handle = await getFileSystemHandlerAsync(path)
div.replaceChildren(...await Array.fromAsync(handle.values(), h => createFileCard(`${path}\\${h.name}`)))
return div;
}
const cardTypes = ["html", "file", "text", "image"];
function createCardBase() {
let card = createElement('div', {
id: getRandomId("card"), className: 'card', draggable: true,
ondragstart: onCardDragStart, onclick: onCardClick, oncontextmenu: onCardContextMenu
});
let checkbox = createElement('input', {
type: 'checkbox', className: 'card-checkbox',
onclick: (e) => e.stopPropagation(),
onchange: async function (e) {
card.classList.toggle('selected', this.checked)
if (config.clickevent == "copy") {
clipboardWrite("copy");
}
}
});
card.append(checkbox);
let cardWrapper = createElement('div', { className: 'card-wrapper' });
let cardContent = createElement('div', { className: 'card-content' });
let cardExtra = createElement('div', { className: 'card-extra' });
card.append(cardWrapper);
cardWrapper.append(cardContent);
cardWrapper.append(cardExtra);
card.append(createElement('div', {
className: 'card-close-button',
onclick: function (e) {
e.stopPropagation();
// Add slide-out animation before removal
card.style.transition = "all 0.3s";
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
// Remove after animation completes
setTimeout(() => {
card.remove();
}, 300);
}
}));
Object.defineProperty(card, "selected", {
get: function () { return checkbox.checked; },
set: function (value) {
let v = !!value;
checkbox.checked = v;
card.classList.toggle("selected", v);
// Add subtle animation when selected
if (v) {
card.animate([
{ transform: 'scale(1)' },
{ transform: 'scale(1.02)' },
{ transform: 'scale(1)' }
], {
duration: 300,
easing: 'ease-in-out'
});
}
}
});
Object.defineProperty(card, "editable", {
get: function () { return card.draggable },
set: function (value) {
let type = card.type;
if (value) {
card.draggable = false;
cardContent.contentEditable = type == "html" ? true : 'plaintext-only';
// Add edit mode styling
cardContent.style.boxShadow = "inset 0 0 0 2px var(--accent)";
cardContent.style.padding = "var(--spacing-sm)";
cardContent.style.borderRadius = "var(--radius-sm)";
// Focus on the content
setTimeout(() => {
cardContent.focus();
// Place cursor at end
const range = document.createRange();
range.selectNodeContents(cardContent);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}, 10);
} else {
card.draggable = true;
cardContent.contentEditable = false;
// Remove edit mode styling
cardContent.style.boxShadow = "";
cardContent.style.padding = "";
}
},
configurable: true
});
Object.defineProperty(card, "type", {
get: function () { return cardTypes.find(x => [...card.classList].includes(x)); },
set: function (new_type) {
let old_type = card.type;
card.classList.remove(...cardTypes);
cardExtra.replaceChildren();
if (new_type == "html") {
card.classList.add("html");
}
if (new_type == "file") {
card.classList.add("file");
cardContent.textContent = cardContent.innerText;
}
if (new_type == "text") {
card.classList.add("text");
if (old_type == "html") {
cardContent.textContent = cardContent.innerText.replace(/\r/g, "").replace(/\n\n+/g, "\n\n");
} else {
cardContent.textContent = cardContent.innerText;
}
}
// Add animation when changing type
card.animate([
{ opacity: 0.7 },
{ opacity: 1 }
], {
duration: 300,
easing: 'ease-in-out'
});
card.updateInfo();
}
});
Object.defineProperty(card, "open", {
get: function () { return cardContent.classList.contains("open"); },
set: async function (value) {
if (editing) return;
const wasOpen = cardContent.classList.contains("open");
cardContent.classList.toggle("open", value);
if (!value) {
// Add animation for closing
if (wasOpen) {
cardExtra.style.transition = "all 0.3s";
cardExtra.style.maxHeight = "0";
cardExtra.style.opacity = "0";
// Wait for animation then clear
setTimeout(() => {
cardExtra.replaceChildren();
cardExtra.style = "";
}, 300);
}
return;
}
if (card.type != "file") return;
let path = cardContent.innerText;
let { exists, isDirectory, mime } = await getFileInfoAsync(path);
if (!exists) return;
// Start with zero height for animation
if (!wasOpen) {
cardExtra.style.maxHeight = "0";
cardExtra.style.opacity = "0";
cardExtra.style.transition = "all 0.3s";
}
if (isDirectory) {
let div = await createDirectoryDiv(path);
cardExtra.replaceChildren(div);
} else {
let file = await getFileSystemHandlerAsync(path);
file = await file.getFile();
if (file == null) return;
if (file.type.startsWith("image/")) {
let img = createElement('img', { src: URL.createObjectURL(file) });
img.ondblclick = function (e) {
window.open(this.src, "_blank", `popup=yes,width=${window.innerWidth},height=${window.innerHeight},top=${window.screenY},left=${Math.max(window.screenX - window.innerWidth - 20, 0)}`);
}
cardExtra.replaceChildren(img);
} else {
throw new Error("Unsupported file type");
}
}
// Animate opening
if (!wasOpen) {
setTimeout(() => {
cardExtra.style.maxHeight = "250px";
cardExtra.style.opacity = "1";
}, 10);
}
},
configurable: true
});
card.toggleExtra = async function (state) {
if (editing) return;
const wasOpen = cardContent.classList.contains("open");
state = cardContent.classList.toggle("open", state);
if (!state) {
// Add animation for closing
if (wasOpen) {
cardExtra.style.transition = "all 0.3s";
cardExtra.style.maxHeight = "0";
cardExtra.style.opacity = "0";
// Wait for animation then clear
setTimeout(() => {
cardExtra.replaceChildren();
cardExtra.style = "";
}, 300);
}
return;
}
if (card.type != "file") return;
let path = cardContent.innerText;
let { exists, isDirectory, mime } = await getFileInfoAsync(path);
if (!exists) return;
// Start with zero height for animation
if (!wasOpen) {
cardExtra.style.maxHeight = "0";
cardExtra.style.opacity = "0";
cardExtra.style.transition = "all 0.3s";
}
if (isDirectory) {
let div = await createDirectoryDiv(path);
cardExtra.replaceChildren(div);
} else {
let file = await getFileSystemHandlerAsync(path);
file = await file.getFile();
if (file == null) return;
if (file.type.startsWith("image/")) {
let img = createElement('img', { src: URL.createObjectURL(file) });
img.ondblclick = function (e) {
window.open(this.src, "_blank", `popup=yes,width=${window.innerWidth},height=${window.innerHeight},top=${window.screenY},left=${Math.max(window.screenX - window.innerWidth - 20, 0)}`);
}
cardExtra.replaceChildren(img);
} else {
cardContent.classList.toggle("open", false);
throw new Error("Unsupported file type");
}
}
// Animate opening
if (!wasOpen) {
setTimeout(() => {
cardExtra.style.maxHeight = "250px";
cardExtra.style.opacity = "1";
}, 10);
}
};
cardContent.ondblclick = async function () {
if (editing) return;
let type = card.type;
if (type == "file") {
let path = cardContent.innerText
if (config.filedblclick == "copy") {
clipboardWrite("copy");
} else if (config.filedblclick == "locate") {
$quickerSp('在资源管理器中定位文件', { path });
} else if (config.filedblclick == "preview") {
card.toggleExtra().catch(e => {
if (config.previewfallback)
$quickerSp('运行或打开', { path });
})
} else if (config.filedblclick == "open") {
$quickerSp('运行或打开', { path });
}
} else {
if (config.textdblclick == "copy") {
clipboardWrite("copy");
} else if (config.textdblclick == "edit") {
if (type == "html") card.editContent();
if (type == "text") card.editText();
}
}
}
cardContent.onblur = function (e) {
let type = card.type;
if (type == "file") {
card.updateInfo();
} else {
if (cardContent.innerText.trim() == "") {
// Add slide-out animation before removal
card.style.transition = "all 0.3s";
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
// Remove after animation completes
setTimeout(() => {
card.remove();
}, 300);
}
}
// Remove edit mode styling
cardContent.style.boxShadow = "";
cardContent.style.padding = "";
}
card.updateInfo = async function () {
if (card.type == "file") {
let { type } = await getFileInfoAsync(cardContent.innerText);
cardContent.classList.remove("file", "directory", "non-exist");
cardContent.classList.add(type);
if (type == "directory" && cardContent.classList.contains("open")) {
let div = await createDirectoryDiv(cardContent.innerText)
cardExtra.replaceChildren(div)
} else {
cardContent.classList.remove("open");
cardExtra.replaceChildren();
}
} else {
cardContent.classList.remove("file", "directory", "non-exist", "open");
}
}
card.editText = function () {
let win = window.open("about:blank", "_blank", `popup=yes,width=${window.innerWidth},height=${window.innerHeight},top=${window.screenY},left=${Math.max(window.screenX - window.innerWidth - 20, 0)}`);
win.document.head.innerHTML = `
<style>
body {
margin: 0;
padding: 20px;
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Monaco, monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
height: 100vh;
box-sizing: border-box;
}
body:focus {
outline: none;
}
/* For dark mode */
@media (prefers-color-scheme: dark) {
body.system {
background-color: #1d1d1f;
color: #f5f5f7;
}
}
</style>
`;
if (document.documentElement.classList.contains("dark")) {
win.document.body.style.background = "#1d1d1f";
win.document.body.style.color = "#f5f5f7";
} else if (config.colorscheme === "system") {
win.document.body.classList.add("system");
}
win.document.body.textContent = cardContent.innerText;
win.document.body.contentEditable = "plaintext-only";
win.document.body.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
win.close();
}
});
win.onunload = function (e) {
cardContent.innerHTML = win.document.body.innerText;
}
}
card.editContent = function () {
let win = window.open("about:blank", "_blank", `popup=yes,width=${window.innerWidth},height=${window.innerHeight},top=${window.screenY},left=${Math.max(window.screenX - window.innerWidth - 20, 0)}`);
win.document.head.innerHTML = `
<style>
body {
margin: 0;
padding: 20px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.5;
height: 100vh;
box-sizing: border-box;
}
body:focus {
outline: none;
}
/* For dark mode */
@media (prefers-color-scheme: dark) {
body.system {
background-color: #1d1d1f;
color: #f5f5f7;
}
}
</style>
`;
if (document.documentElement.classList.contains("dark")) {
win.document.body.style.background = "#1d1d1f";
win.document.body.style.color = "#f5f5f7";
} else if (config.colorscheme === "system") {
win.document.body.classList.add("system");
}
win.document.body.innerHTML = cardContent.innerHTML;
win.document.body.contentEditable = true;
win.document.body.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
win.close();
}
});
win.onunload = function (e) {
cardContent.innerHTML = win.document.body.innerHTML;
}
}
card.editSource = function () {
let win = window.open("about:blank", "_blank", `popup=yes,width=${window.innerWidth},height=${window.innerHeight},top=${window.screenY},left=${Math.max(window.screenX - window.innerWidth - 20, 0)}`);
win.document.head.innerHTML = `
<style>
body {
margin: 0;
padding: ID;
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Monaco, monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
height: 100vh;
box-sizing: border-box;
}
body:focus {
outline: none;
}
/* For dark mode */
@media (prefers-color-scheme: dark) {
body.system {
background-color: #1d1d1f;
color: #f5f5f7;
}
}
</style>
`;
if (document.documentElement.classList.contains("dark")) {
win.document.body.style.background = "#1d1d1f";
win.document.body.style.color = "#f5f5f7";
} else if (config.colorscheme === "system") {
win.document.body.classList.add("system");
}
win.document.body.textContent = cardContent.innerHTML;
win.document.body.contentEditable = "plaintext-only";
win.document.body.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
win.close();
}
});
win.onunload = function (e) {
cardContent.innerHTML = win.document.body.innerText;
}
}
card.assign = function (property, value) {
card[property] = value;
}
card.contentDiv = cardContent;
return { card, cardContent };
}
async function createFileCard(path, { open } = {}) {
if (path instanceof File) path = await getFullPathAsync(path);
if (!path) return;
let { card, cardContent } = createCardBase();
card.classList.add("file");
let { type } = await getFileInfoAsync(path);
cardContent.classList.add(type);
cardContent.innerText = path;
if (open) card.toggleExtra(true).catch(e => { });
// Add entry animation
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
setTimeout(() => {
card.style.transform = "translateY(0)";
card.style.opacity = "1";
}, 10);
return card;
}
function createHTMLCard(html) {
let { card, cardContent } = createCardBase();
card.classList.add("html");
cardContent.innerHTML = html;
// Add entry animation
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
setTimeout(() => {
card.style.transform = "translateY(0)";
card.style.opacity = "1";
}, 10);
return card;
}
function createTextCard(text) {
if (!text) text = "新卡片";
let { card, cardContent } = createCardBase();
card.classList.add("text");
cardContent.textContent = text;
// Add entry animation
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
setTimeout(() => {
card.style.transform = "translateY(0)";
card.style.opacity = "1";
}, 10);
return card;
}
async function createCard(type, data, args) {
if (type == "html") return createHTMLCard(data);
if (type == "file") return createFileCard(data, args);
return createTextCard(data);
}
async function createCardFromDataTransfer(dataTransfer) {
let data;
if (dataTransfer.types.includes("Files")) {
return Array.fromAsync(
dataTransfer.files, async (file) =>
getFullPathAsync(file)
.catch(e => saveTempfileAsync(file))
.then(path => createFileCard(path, { open: config.autopreview }))
);
}
if (config.richin) {
data = dataTransfer.getData("text/html")
.replace(/^<html>\s*/m, "").replace(/\s*<\/html>$/m, "")
.replace(/^<body>\s*/m, "").replace(/\s*<\/body>$/m, "")
.replace(new RegExp("<\!--StartFragment-->", "g"), "").replace(new RegExp("<\!--EndFragment-->", "g"), "");
if (data != "") return [createHTMLCard(data)];
}
data = dataTransfer.getData("text/plain");
return [createTextCard(data)];
}
document.querySelector('#tool-clearformat').onclick = (e) => {
document.querySelectorAll('.card.selected').forEach(ele => ele.type = "text");
};
document.querySelector('#tool-cardjoin').onclick = async function (e) {
let activeCard = document.querySelector('.tab-content.active').querySelector('.card.selected');
if (!activeCard) return;
let card = await getSelectedCardJoin();
activeCard.before(card);
card.editable = editing;
// Add slide-in animation for new card
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
setTimeout(() => {
card.style.transform = "translateY(0)";
card.style.opacity = "1";
}, 10);
if (config.droponjoin) {
let selectedCards = document.querySelector('.tab-content.active').querySelectorAll('.card.selected');
// Add slide-out animation before removal
for (let c of selectedCards) {
c.style.transition = "all 0.3s";
c.style.transform = "translateY(20px)";
c.style.opacity = "0";
}
// Remove after animation completes
setTimeout(() => {
selectedCards.forEach(ele => ele.remove());
}, 300);
}
};
document.querySelector('#tool-readclipboard').onclick = () => {
$quickerSp('添加剪贴板条目').catch(e => notify('剪贴板中没有内容', "Error"));
};
document.querySelector("#tool-refreshfile").onclick = async function (e) {
// Add refresh animation
const refreshButton = document.querySelector("#tool-refreshfile");
refreshButton.style.transition = "transform 0.5s";
refreshButton.style.transform = "rotate(360deg)";
setTimeout(() => {
refreshButton.style.transition = "none";
refreshButton.style.transform = "rotate(0deg)";
}, 500);
let cardContents = document.querySelector('.tab-content.active').querySelectorAll('.card.selected.file>.card-wrapper>.card-content');
if (cardContents.length == 0)
cardContents = document.querySelectorAll('.card.file .card-content')
for (let content of cardContents) {
let card = content.closest('.card');
let { type } = await getFileInfoAsync(content.innerText);
content.classList.remove("file", "directory", "non-exist");
content.classList.add(type);
}
};
let tbPlus = document.querySelector('#tab-plus');
let trashButton = document.querySelector("#trash-button");
trashButton.onclick = function (e) {
let selectedCards = document.querySelector(".tab-content.active").querySelectorAll('.card.selected');
if (selectedCards.length != 0) {
// Add slide-out animation before removal
for (let card of selectedCards) {
card.style.transition = "all 0.3s";
card.style.transform = "translateY(20px)";
card.style.opacity = "0";
}
// Remove after animation completes
setTimeout(() => {
selectedCards.forEach(ele => ele.remove());
}, 300);
}
};
trashButton.ondrop = function (e) {
e.preventDefault();
trashButton.classList.remove("dragover");
let data;
data = e.dataTransfer.getData("datatransfer/card");
if (data) {
for (let id of data.split("\n")) {
let card = document.getElementById(id);
// Add slide-out animation before removal
card.style.transition = "all 0.3s";
card.style.transform = "translateX(50px)";
card.style.opacity = "0";
// Remove after animation completes
setTimeout(() => {
card.remove();
}, 300);
}
return
}
data = e.dataTransfer.getData("datatransfer/tab");
if (data) {
for (let id of data.split("\n")) {
document.getElementById(id).dispose();
}
return
}
};
trashButton.ondragover = function (e) {
e.preventDefault();
trashButton.classList.add("dragover");
e.dataTransfer.dropEffect = "move";
};
trashButton.ondragleave = function (e) {
trashButton.classList.remove("dragover");
};
tbPlus.onclick = e => {
createTab().activate();
};
tbPlus.ondragover = function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "All";
};
tbPlus.ondrop = async function (e) {
e.preventDefault();
let tab = createTab();
tab.contentDiv.append(...await createCardFromDataTransfer(e.dataTransfer));
tab.activate();
}
document.oncontextmenu = function (e) {
e.preventDefault();
};
</script>
<script>
const config = {
get richin() { return document.querySelector('#config-richin').checked },
set richin(value) { document.querySelector('#config-richin').checked = value },
get richout() { return document.querySelector('#config-richout').checked },
set richout(value) { document.querySelector('#config-richout').checked = value },
get copynotify() { return document.querySelector('#config-copynotify').checked },
set copynotify(value) { document.querySelector('#config-copynotify').checked = value },
get clickevent() { return document.querySelector('#config-clickevent').value },
set clickevent(value) { document.querySelector('#config-clickevent').value = value },
get textdblclick() { return document.querySelector('#config-text-dblclick').value },
set textdblclick(value) { document.querySelector('#config-text-dblclick').value = value },
get filedblclick() { return document.querySelector('#config-file-dblclick').value },
set filedblclick(value) { document.querySelector('#config-file-dblclick').value = value },
get dropeffect() { return document.querySelector('#config-dropeffect').value },
set dropeffect(value) { document.querySelector('#config-dropeffect').value = value },
get droponjoin() { return document.querySelector('#config-droponjoin').checked },
set droponjoin(value) { document.querySelector('#config-droponjoin').checked = value },
get previewfallback() { return document.querySelector('#config-previewfallback').checked },
set previewfallback(value) { document.querySelector('#config-previewfallback').checked = value },
get autopreview() { return document.querySelector('#config-autopreview').checked },
set autopreview(value) { document.querySelector('#config-autopreview').checked = value },
get colorscheme() { return document.querySelector('#config-colorscheme').value },
set colorscheme(value) { document.querySelector('#config-colorscheme').value = value },
}
function saveData() {
let metadata = { ...config };
let tabHeaders = Array.from(document.querySelectorAll('.tab-head'))
metadata.activeTabN = tabHeaders.indexOf(document.querySelector('.tab-head.active'));
let data = tabHeaders.map(tc => {
let name = tc.innerText
let cards = Array.from(tc.contentDiv.querySelectorAll('&>.card')).map(card => {
let type = card.type;
let content;
if (type == "html") {
content = card.contentDiv.innerHTML;
} else {
content = card.contentDiv.innerText;
}
let args = {};
if (type == "file") {
args.open = card.contentDiv.classList.contains("open");
}
return { type, content, args };
})
return { name, cards };
});
return $quickerSp("保存数据", { data: JSON.stringify({ metadata, data }) });
}
window.onkeydown = (e) => {
if (e.ctrlKey && e.key == "s") {
e.preventDefault();
saveData().then(() => {
notify("已保存", "Success");
// Add save animation
const flash = document.createElement('div');
flash.style.position = "fixed";
flash.style.top = "0";
flash.style.left = "0";
flash.style.right = "0";
flash.style.bottom = "0";
flash.style.backgroundColor = "rgba(255, 255, 255, 0.2)";
flash.style.pointerEvents = "none";
flash.style.zIndex = "9999";
flash.style.animation = "flash 0.5s ease-out forwards";
document.body.appendChild(flash);
// Add keyframes for flash animation
const style = document.createElement('style');
style.textContent = `
@keyframes flash {
0% { opacity: 1; }
100% { opacity: 0; }
}
`;
document.head.appendChild(style);
setTimeout(() => {
flash.remove();
style.remove();
}, 500);
});
}
if (e.ctrlKey && e.key == "e") {
e.preventDefault();
chrome.webview.postMessage({
op: "customMessage", action: "moveWindowToCursor"
});
}
if (!editing && e.ctrlKey && e.key == "a") {
e.preventDefault();
document.querySelector('.tab-content.active').querySelectorAll('.card').forEach(e => e.classList.add("selected"));
}
if (!editing && e.key == "Delete") {
const selectedCards = document.querySelector('.tab-content.active').querySelectorAll('.card.selected');
// Add slide-out animation before removal
for (let card of selectedCards) {
card.style.transition = "all 0.3s";
card.style.transform = "translateX(50px)";
card.style.opacity = "0";
}
// Remove after animation completes
setTimeout(() => {
selectedCards.forEach(e => e.remove());
}, 300);
}
if (!editing && e.ctrlKey && e.key == "x") {
e.preventDefault();
clipboardWrite("move");
const selectedCards = document.querySelector('.tab-content.active').querySelectorAll('.card.selected');
// Add slide-out animation before removal
for (let card of selectedCards) {
card.style.transition = "all 0.3s";
card.style.transform = "translateX(50px)";
card.style.opacity = "0";
}
// Remove after animation completes
setTimeout(() => {
selectedCards.forEach(e => e.remove());
}, 300);
}
if (!editing && e.ctrlKey && e.key == "c") {
e.preventDefault();
clipboardWrite('copy');
// Add copy animation
const selectedCards = document.querySelector('.tab-content.active').querySelectorAll('.card.selected');
for (let card of selectedCards) {
card.animate([
{ boxShadow: '0 0 0 2px var(--accent)' },
{ boxShadow: '0 0 0 0 var(--accent)' }
], {
duration: 300,
easing: 'ease-out'
});
}
}
if (e.key == "Escape") {
document.querySelectorAll('.card.selected').forEach(e => e.classList.remove("selected"));
}
if (!editing && e.key == "F2") {
document.querySelector('.tab-content.active').querySelector('.card.selected .card-content')?.ondblclick?.();
}
};
document.onpaste = async function (e) {
if (!editing) {
const cards = await createCardFromDataTransfer(e.clipboardData);
document.querySelector('.tab-content.active').append(...cards);
}
};
let unloadPromise;
let loadPromise;
async function updateTheme() {
let isDark = config.colorscheme == "dark"
if (config.colorscheme == "system") {
let { colorscheme } = await $quickerSp('获取主题颜色', {});
isDark = /dark/.test(colorscheme) || colorscheme == null && window.matchMedia("(prefers-color-scheme: dark)").matches
}
document.documentElement.classList.toggle("dark", isDark)
}
window.chrome.webview.addEventListener('message', async (event) => {
if (event.data.op != "customMessage") return
if (event.data.action == "closingWindow") {
if (unloadPromise) await unloadPromise;
window.close();
return;
}
if (event.data.action == "loadData") {
if (loadPromise) await loadPromise;
let { resolve, reject, promise } = Promise.withResolvers();
loadPromise = promise;
let { data = [], metadata = {} } = event.data || {};
document.querySelectorAll('.tab-head').forEach(e => e.remove());
document.querySelectorAll('.tab-content').forEach(e => e.remove());
document.body.classList.remove('manage');
Object.assign(config, metadata);
updateTheme();
for (let tc of data) {
let tab = createTab(tc.name);
for (let c of tc.cards) {
let card = await createCard(c.type, c.content, c.args);
tab.contentDiv.append(card)
}
}
if (data.length == 0) createTab().activate();
else (document.querySelectorAll('.tab-head')[metadata.activeTabN] ?? document.querySelector('.tab-head')).activate();
window.onbeforeunload = (e) => { unloadPromise = saveData(); }
resolve();
}
if (event.data.action == "createCard") {
let tab = document.querySelector('.tab-head.active');
let { type, content } = event.data;
let card;
if (type == "file") {
for (let file of content.replace(/\r/g, "").split("\n")) {
card = await createFileCard(file);
}
} else if (type == "html") {
card = await createHTMLCard(content);
if (!config.richin) card.type = "text";
} else {
card = await createTextCard(content);
}
tab.contentDiv.append(card);
}
});
window.onload = () => { $quickerSp('加载数据', {}) }
</script>
</body>
</html>