diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css
index 2df7161c..a6cd0dad 100644
--- a/themes/Ying/assets/css/main.css
+++ b/themes/Ying/assets/css/main.css
@@ -2941,9 +2941,11 @@ img[atk-emoticon] {
/* Fix Artalk comment textarea color in light mode */
.atk-textarea {
color: #1a1a1a !important;
+ background: #fff !important;
}
[data-theme="dark"] .atk-textarea {
color: #e5e7eb !important;
+ background: transparent !important;
}
/* About Page Card Style */
@@ -2959,6 +2961,108 @@ img[atk-emoticon] {
box-shadow: none !important;
}
+/* Selection Popup (Quote Button) */
+#selection-popup {
+ position: fixed;
+ display: none;
+ background: rgba(30, 30, 30, 0.9);
+ color: #fff;
+ padding: 8px 16px;
+ border-radius: 8px;
+ font-size: 14px;
+ cursor: pointer;
+ z-index: 2147483647;
+ transform: translate(-50%, -100%);
+ pointer-events: auto;
+ /* Removed backdrop-filter for performance */
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ transition: opacity 0.2s, transform 0.2s;
+ font-weight: 500;
+ line-height: 1.4;
+ user-select: none;
+ -webkit-user-select: none;
+ align-items: center;
+ gap: 6px;
+ white-space: nowrap;
+ will-change: transform, opacity;
+ animation: popIn 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
+}
+
+@keyframes popIn {
+ 0% { transform: translate(-50%, -80%) scale(0.9); opacity: 0; }
+ 100% { transform: translate(-50%, -100%) scale(1); opacity: 1; }
+}
+
+#selection-popup i {
+ font-size: 16px;
+}
+
+#selection-popup::after {
+ content: '';
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ margin-left: -6px;
+ border-width: 6px;
+ border-style: solid;
+ border-color: rgba(30, 30, 30, 0.9) transparent transparent transparent;
+}
+
+#selection-popup:hover {
+ transform: translate(-50%, -110%) scale(1.05);
+ background: #000;
+ box-shadow: 0 6px 16px rgba(0,0,0,0.25);
+}
+
+[data-theme="dark"] #selection-popup {
+ background: rgba(255, 255, 255, 0.95);
+ color: #000;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+}
+
+[data-theme="dark"] #selection-popup::after {
+ border-color: rgba(255, 255, 255, 0.95) transparent transparent transparent;
+}
+
+[data-theme="dark"] #selection-popup:hover {
+ background: #fff;
+}
+
+/* Artalk Comment Blockquote Redesign & Dark Mode Fix */
+.atk-content blockquote {
+ margin: 10px 0 6px 0; /* Reduced bottom margin for tighter layout */
+ padding: 12px 16px;
+ background-color: var(--bankuai-color);
+ border-left: 4px solid var(--text-highlight-color);
+ border-radius: 4px;
+ color: var(--text-content-color);
+ position: relative;
+ font-style: normal;
+ font-weight: normal;
+ transition: all 0.3s ease;
+}
+
+.atk-content blockquote + p {
+ margin-top: 4px !important; /* Reduce spacing for paragraph immediately following blockquote */
+}
+
+.atk-content blockquote p {
+ margin: 0;
+ color: inherit !important;
+ font-size: 0.95rem !important;
+ line-height: 1.6 !important;
+}
+
+[data-theme="dark"] .atk-content blockquote {
+ background-color: rgba(255, 255, 255, 0.05);
+ border-left-color: #4b5563; /* Use a slightly lighter gray for border in dark mode */
+ color: var(--text-content-dark-color);
+}
+
+[data-theme="dark"] .atk-content blockquote p {
+ color: inherit !important;
+}
+
/* Category Page Styles */
.category-container {
max-width: 100%;
diff --git a/themes/Ying/assets/js/main.js b/themes/Ying/assets/js/main.js
index 4661af95..8854b0ee 100644
--- a/themes/Ying/assets/js/main.js
+++ b/themes/Ying/assets/js/main.js
@@ -366,8 +366,141 @@ function initFloatingTools() {
// Listen for PJAX complete to update visibility
document.addEventListener('pjax:complete', function() {
updateFloatingToolsVisibility();
+ hideSelectionPopup();
});
document.addEventListener('DOMContentLoaded', function() {
initFloatingTools();
+ initSelectionPopup();
});
+
+// Selection Popup Logic
+let selectionPopup = null;
+let selectionTimeout;
+
+function initSelectionPopup() {
+ if (document.getElementById('selection-popup')) return;
+
+ selectionPopup = document.createElement('div');
+ selectionPopup.id = 'selection-popup';
+ selectionPopup.innerHTML = ' 引用评论';
+ document.body.appendChild(selectionPopup);
+
+ // Prevent mousedown on popup from clearing selection
+ selectionPopup.addEventListener('mousedown', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ });
+
+ // Handle click on popup
+ selectionPopup.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ let selection = window.getSelection().toString();
+ // Optimize: Remove extra whitespace and limit length
+ if (selection) {
+ // Collapse multiple empty lines into max 2 newlines, and trim each line
+ selection = selection.split('\n')
+ .map(line => line.trim())
+ .filter(line => line !== '')
+ .join('\n');
+
+ if (selection.length > 500) {
+ selection = selection.substring(0, 500) + '...';
+ }
+
+ scrollToCommentsAndQuote(selection);
+ hideSelectionPopup();
+ window.getSelection().removeAllRanges();
+ }
+ });
+
+ // Global listeners for selection
+ document.addEventListener('mouseup', handleSelectionChange);
+ document.addEventListener('keyup', handleSelectionChange);
+}
+
+function hideSelectionPopup() {
+ if (selectionPopup) {
+ selectionPopup.style.display = 'none';
+ }
+}
+
+function handleSelectionChange(e) {
+ // Debounce to avoid rapid firing
+ clearTimeout(selectionTimeout);
+ selectionTimeout = setTimeout(() => {
+ const selection = window.getSelection();
+ const text = selection.toString().trim();
+ const content = document.querySelector('.post-content'); // Main article container
+
+ if (!selectionPopup || !content) return;
+
+ // Check if selection is empty
+ if (!text || selection.rangeCount === 0) {
+ hideSelectionPopup();
+ return;
+ }
+
+ const range = selection.getRangeAt(0);
+
+ // Check if the common ancestor is inside .post-content
+ // Optimization: Check container existence before heavy contains check
+ if (!content.contains(range.commonAncestorContainer)) {
+ hideSelectionPopup();
+ return;
+ }
+
+ // Optimization: Use requestAnimationFrame for UI updates
+ requestAnimationFrame(() => {
+ // Show first to ensure measurements if needed (though we set top/left directly)
+ selectionPopup.style.display = 'flex';
+
+ // Position logic: Follow mouse if available (mouseup), else fallback to selection rect (keyup)
+ if (e && e.type === 'mouseup') {
+ // Optimization: Avoid getBoundingClientRect on mouseup
+ // Position above the mouse cursor
+ selectionPopup.style.top = `${e.clientY - 40}px`;
+ selectionPopup.style.left = `${e.clientX}px`;
+ } else {
+ // Fallback for keyboard selection (needs rect)
+ const rect = range.getBoundingClientRect();
+ // Check if visible and valid
+ if (rect.width === 0 || rect.height === 0) {
+ hideSelectionPopup();
+ return;
+ }
+ selectionPopup.style.top = `${rect.top}px`;
+ selectionPopup.style.left = `${rect.left + rect.width / 2}px`;
+ }
+ });
+ }, 150);
+}
+
+function scrollToCommentsAndQuote(text) {
+ const comments = document.getElementById('Comments');
+ if (comments) {
+ comments.scrollIntoView({ behavior: 'smooth' });
+
+ // Wait for scroll to complete (approximate)
+ setTimeout(() => {
+ // Find Artalk textarea
+ const textarea = document.querySelector('.atk-textarea');
+ if (textarea) {
+ const quote = `> ${text}\n\n`;
+ // Overwrite existing content as requested
+ textarea.value = quote;
+ textarea.focus();
+
+ // Trigger input event for Vue/React binding (Artalk uses Vue)
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
+ textarea.dispatchEvent(new Event('change', { bubbles: true }));
+
+ if (window.Qmsg) {
+ Qmsg.success('引用成功');
+ }
+ }
+ }, 500);
+ }
+}