Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Vanessa 2026-02-16 15:35:39 +08:00
commit 9fd35ab007
30 changed files with 523 additions and 134 deletions

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "لا تقسم الشاشة عند فتح التبويب",
"noSplitScreenWhenOpenTabTip": "لا تقسم الشاشة تلقائيًا عند فتح تبويبات البحث أو PDF أو غيرها",
"useChrome": "يدعم فقط في المتصفحات المبنية على محرك Chromium (مثل Chrome/Edge)، قد تواجه مشكلات توافق لا يمكن حلها عند استخدام متصفحات أخرى",
"clearAllAV": "هل تؤكد مسح جميع قواعد البيانات غير المشار إليها؟",
"unreferencedAV": "قاعدة بيانات غير مشار إليها",
@ -1213,7 +1215,7 @@
"export23": "‫تصدير Markdown مع YAML front-matter",
"export24": "‫بعد التمكين، سيتم إضافة بعض معلومات البيانات الوصفية العامة في بداية ملف Markdown المصدّر‬",
"export25": "معاملات تشغيل Pandoc",
"export26": "معاملات Pandoc المستخدمة عند تصدير ملف Word .docx",
"export26": "معاملات Pandoc المستخدمة عند تصدير ملفات Word .docx، على سبيل المثال المعامل <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "‫العلامة المائية لملف PDF المصدّر‬",
"export28": "نص العلامة المائية المخصصة أو مسار ملف العلامة المائية",
"export29": "موقع العلامة المائية، حجمها وأسلوبها، إلخ",
@ -1333,6 +1335,7 @@
"column": "العمود",
"copied": "تم النسخ",
"copy": "نسخ",
"copyFile": "نسخ الملف",
"copyText": "نسخ النص *",
"delete-column": "حذف العمود",
"delete-row": "حذف الصف",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Beim Öffnen von Tabs nicht in den Splitscreen wechseln",
"noSplitScreenWhenOpenTabTip": "Beim Öffnen von Such-, PDF- und ähnlichen Tabs nicht automatisch in den Splitscreen wechseln",
"useChrome": "Nur in Browsern mit Chromium-Engine (z. B. Chrome/Edge) unterstützt, die Verwendung anderer Browser kann zu nicht löschbaren Kompatibilitätsproblemen führen",
"clearAllAV": "Bestätigen Sie das Löschen aller nicht referenzierten Datenbanken?",
"unreferencedAV": "Nicht referenzierte Datenbank",
@ -1213,7 +1215,7 @@
"export23": "Exportieren Markdown mit YAML-Vorwort",
"export24": "Nach der Aktivierung einige allgemeine Metadateninformationen am Anfang der exportierten Markdown-Datei hinzufügen",
"export25": "Pandoc-Ausführungsparameter",
"export26": "Pandoc-Parameter, die beim Export einer Word .docx-Datei verwendet werden",
"export26": "Pandoc-Parameter, die beim Export von Word .docx-Dateien verwendet werden, z. B. der Parameter <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "Export PDF-Wasserzeichen",
"export28": "Wasserzeichentext oder Wasserzeichen-Dateipfad",
"export29": "Wasserzeichenposition, Größe und Stil usw.",
@ -1333,6 +1335,7 @@
"column": "Spalte",
"copied": "Kopiert",
"copy": "Kopieren",
"copyFile": "Datei kopieren",
"copyText": "Text kopieren *",
"delete-column": "Spalte löschen",
"delete-row": "Zeile löschen",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Don't split the screen when opening tabs",
"noSplitScreenWhenOpenTabTip": "Don't automatically split the screen when opening search, PDF and other tabs",
"useChrome": "Only supported in browsers based on the Chromium engine (e.g., Chrome/Edge), using other browsers may encounter compatibility issues that cannot be resolved",
"clearAllAV": "Confirm clearing all unreferenced databases?",
"unreferencedAV": "Unreferenced database",
@ -1213,7 +1215,7 @@
"export23": "Export Markdown with YAML front-matter",
"export24": "When enabled, add some general metadata information at the beginning of the exported Markdown file",
"export25": "Pandoc execution parameters",
"export26": "Pandoc parameters used when exporting a Word .docx file",
"export26": "Pandoc parameters used when exporting Word .docx files, for example the <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a> parameter",
"export27": "Export PDF watermark",
"export28": "Watermark text or watermark file path",
"export29": "Watermark position, size and style, etc.",
@ -1333,6 +1335,7 @@
"column": "Column",
"copied": "Copied",
"copy": "Copy",
"copyFile": "Copy file",
"copyText": "Copy text *",
"delete-column": "Delete Column",
"delete-row": "Delete Row",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "No dividir pantalla al abrir pestañas",
"noSplitScreenWhenOpenTabTip": "No dividir automáticamente la pantalla al abrir pestañas de búsqueda, PDF u otras",
"useChrome": "Solo es compatible con navegadores basados en el motor Chromium (por ejemplo Chrome/Edge), usar otros navegadores puede provocar problemas de compatibilidad que no se pueden resolver",
"clearAllAV": "¿Confirmar la limpieza de todas las bases de datos no referenciadas?",
"unreferencedAV": "Base de datos no referenciada",
@ -1213,7 +1215,7 @@
"export23": "Exportar Markdown con YAML front-matter",
"export24": "Después de habilitar, agregue información general de metadatos al comienzo del archivo Markdown exportado",
"export25": "Parámetros de ejecución de Pandoc",
"export26": "Parámetros de Pandoc utilizados al exportar un archivo Word .docx",
"export26": "Parámetros de Pandoc utilizados al exportar archivos Word .docx, por ejemplo el parámetro <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "Exportar marca de agua PDF",
"export28": "Texto de marca de agua o ruta del archivo de marca de agua",
"export29": "Posición, tamaño y estilo de la marca de agua, etc.",
@ -1333,6 +1335,7 @@
"column": "Columna",
"copied": "Copiado",
"copy": "Copiar",
"copyFile": "Copiar archivo",
"copyText": "Copiar texto *",
"delete-column": "Borrar columna",
"delete-row": "Borrar fila",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Ne pas scinder l'écran à l'ouverture d'un onglet",
"noSplitScreenWhenOpenTabTip": "Ne pas scinder automatiquement l'écran lors de l'ouverture d'onglets de recherche, PDF, etc.",
"useChrome": "Pris en charge uniquement dans les navigateurs basés sur le moteur Chromium (par exemple Chrome/Edge), l'utilisation d'autres navigateurs peut engendrer des problèmes de compatibilité insolubles",
"clearAllAV": "Confirmer le nettoyage de toutes les bases de données non référencées ?",
"unreferencedAV": "Base de données non référencée",
@ -1213,7 +1215,7 @@
"export23": "Exporter Markdown avec YAML front-matter",
"export24": "Après l'activation, ajoutez des informations générales sur les métadonnées au début du fichier Markdown exporté",
"export25": "Paramètres d'exécution de Pandoc",
"export26": "Paramètres de Pandoc utilisés lors de l'exportation d'un fichier Word .docx",
"export26": "Paramètres Pandoc utilisés lors de l'exportation de fichiers Word .docx, par exemple le paramètre <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "Exporter le filigrane PDF",
"export28": "Texte du filigrane ou chemin du fichier de filigrane",
"export29": "Position, taille et style du filigrane, etc.",
@ -1333,6 +1335,7 @@
"column": "Colonne",
"copied": "Copié",
"copy": "Copie",
"copyFile": "Copier le fichier",
"copyText": "Copier le texte *",
"delete-column": "Supprimer une Colonne",
"delete-row": "Supprimer la ligne",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "לא לחלק את המסך בעת פתיחת כרטיסייה",
"noSplitScreenWhenOpenTabTip": "לא לחלק את המסך אוטומטית בעת פתיחת כרטיסיות חיפוש, PDF או אחרות",
"useChrome": "נתמך רק בדפדפנים המבוססים על ליבת Chromium (למשל Chrome/Edge), שימוש בדפדפנים אחרים עלול להוביל לבעיות תאימות שאין להן פתרון",
"clearAllAV": "האם לאשר ניקוי של כל מסדי הנתונים שאינם בשימוש?",
"unreferencedAV": "מאגר נתונים ללא הפניות",
@ -1213,7 +1215,7 @@
"export23": "ייצוא Markdown עם YAML ממשק נתונים קדמי",
"export24": "לאחר הפעלת אפשרות זו, הוסף מידע מטא כללי בתחילת קובץ Markdown המיוצא",
"export25": "פרמטרים להרצת Pandoc",
"export26": "פרמטרי Pandoc המשמשים בעת ייצוא קובץ Word .docx",
"export26": "פרמטרי Pandoc בשימוש בעת ייצוא קובץ Word .docx, למשל הפרמטר <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "ייצוא סימן מים PDF",
"export28": "טקסט סימן מים או נתיב קובץ סימן מים",
"export29": "מיקום סימן מים, גודל וסגנון, וכו'",
@ -1333,6 +1335,7 @@
"column": "עמודה",
"copied": "הועתק",
"copy": "העתק",
"copyFile": "העתק קובץ",
"copyText": "העתק טקסט *",
"delete-column": "מחק עמודה",
"delete-row": "מחק שורה",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Non dividere lo schermo all'apertura della scheda",
"noSplitScreenWhenOpenTabTip": "Non dividere automaticamente lo schermo all'apertura di schede come ricerca, PDF, ecc.",
"useChrome": "Supportato solo su browser basati sul motore Chromium (ad es. Chrome/Edge), l'uso di altri browser può causare problemi di compatibilità irrisolvibili",
"clearAllAV": "Confermare la pulizia di tutti i database non referenziati?",
"unreferencedAV": "Database non referenziato",
@ -1213,7 +1215,7 @@
"export23": "Esporta Markdown con YAML front-matter",
"export24": "Dopo l'abilitazione, aggiunge alcune informazioni generali di metadati all'inizio del file Markdown esportato",
"export25": "Parametri di esecuzione di Pandoc",
"export26": "Parametri di Pandoc utilizzati durante l'esportazione di file Word .docx",
"export26": "Parametri di Pandoc usati durante l'esportazione di file Word .docx, ad esempio il parametro <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "Filigrana PDF esportata",
"export28": "Testo della filigrana o percorso del file della filigrana",
"export29": "Posizione, dimensione e stile della filigrana",
@ -1333,6 +1335,7 @@
"column": "Colonna",
"copied": "Copiato",
"copy": "Copia",
"copyFile": "Copia file",
"copyText": "Copia testo *",
"delete-column": "Elimina colonna",
"delete-row": "Elimina riga",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "タブを開くときに分割表示しない",
"noSplitScreenWhenOpenTabTip": "検索や PDF などのタブを開いたときに自動で分割表示しない",
"useChrome": "Chromium ベースのブラウザ(例えば Chrome/Edgeでのみ使用可能で、他のブラウザでは解決できない互換性の問題が発生することがあります",
"clearAllAV": "すべての参照されていないデータベースを削除してもよろしいですか?",
"unreferencedAV": "参照されていないデータベース",
@ -1212,8 +1214,8 @@
"export22": "<code class='fn__code'>%page</code> は現在のページ番号、<code class='fn__code'>%pages</code> は総ページ数で、Sprig テンプレート関数に対応しています",
"export23": "YAML フロントマター付きの Markdown をエクスポート",
"export24": "エクスポートされる Markdown ファイルの先頭に一般的なメタデータを追加します",
"export25": "Pandoc 実行パラメータ",
"export26": "Word .docx ファイルをエクスポートする際に使用する Pandoc パラメータ",
"export25": "Pandoc 実行パラメータ",
"export26": "Word .docx ファイルをエクスポートする際に使用する Pandoc のパラメータ、例えば <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a> パラメータ",
"export27": "PDF に透かしを書き出す",
"export28": "透かしテキストまたは透かし画像ファイルのパス",
"export29": "透かしの位置、サイズ、スタイルなど",
@ -1333,6 +1335,7 @@
"column": "列",
"copied": "コピーしました",
"copy": "コピー",
"copyFile": "ファイルをコピー",
"copyText": "テキストをコピー *",
"delete-column": "列を削除",
"delete-row": "行を削除",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "탭 열기 시 분할 화면 사용 안 함",
"noSplitScreenWhenOpenTabTip": "검색, PDF 등 탭을 열 때 자동으로 분할 화면으로 열지 않음",
"useChrome": "Chromium 엔진 기반 브라우저(예: Chrome/Edge)에서만 지원되며, 다른 브라우저를 사용할 경우 해결할 수 없는 호환성 문제가 발생할 수 있습니다",
"clearAllAV": "모든 참조되지 않은 데이터베이스를 정리하시겠습니까?",
"unreferencedAV": "참조되지 않은 데이터베이스",
@ -1213,7 +1215,7 @@
"export23": "YAML front-matter와 함께 Markdown 내보내기",
"export24": "활성화하면 내보낸 Markdown 파일의 시작 부분에 몇 가지 일반적인 메타데이터 정보를 추가합니다",
"export25": "Pandoc 실행 매개변수",
"export26": "Word .docx 파일을 내보낼 때 사용하는 Pandoc 매개변수",
"export26": "Word .docx 파일을 내보낼 때 사용하는 Pandoc 매개변수, 예: <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a> 매개변수",
"export27": "PDF 워터마크 내보내기",
"export28": "워터마크 텍스트 또는 워터마크 파일 경로",
"export29": "워터마크 위치, 크기 및 스타일 등",
@ -1333,6 +1335,7 @@
"column": "열",
"copied": "복사됨",
"copy": "복사",
"copyFile": "파일 복사",
"copyText": "텍스트 복사 *",
"delete-column": "열 삭제",
"delete-row": "행 삭제",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Nie dziel ekranu przy otwieraniu kart",
"noSplitScreenWhenOpenTabTip": "Nie dziel automatycznie ekranu przy otwieraniu kart wyszukiwania, PDF itp.",
"useChrome": "Obsługiwane tylko w przeglądarkach opartych na silniku Chromium (np. Chrome/Edge), korzystanie z innych przeglądarek może spowodować problemy z kompatybilnością, których nie da się rozwiązać",
"clearAllAV": "Potwierdzić wyczyszczenie wszystkich niepowiązanych baz danych?",
"unreferencedAV": "Baza danych bez odwołań",
@ -1213,7 +1215,7 @@
"export23": "Eksportuj Markdown z metadanymi YAML",
"export24": "Po włączeniu, dodaj kilka ogólnych informacji metadanych na początku wyeksportowanego pliku Markdown",
"export25": "Parametry uruchamiania Pandoc",
"export26": "Parametry Pandoc używane przy eksporcie pliku Word .docx",
"export26": "Parametry Pandoc używane przy eksportowaniu plików Word .docx, na przykład parametr <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "Eksportuj znak wodny PDF",
"export28": "Tekst znaku wodnego lub ścieżka pliku znaku wodnego",
"export29": "Pozycja znaku wodnego, rozmiar i styl itp.",
@ -1333,6 +1335,7 @@
"column": "Kolumna",
"copied": "Skopiowane",
"copy": "Kopiuj",
"copyFile": "Kopiuj plik",
"copyText": "Skopiuj tekst *",
"delete-column": "Usuń kolumnę",
"delete-row": "Usuń wiersz",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Não dividir a tela ao abrir abas",
"noSplitScreenWhenOpenTabTip": "Não dividir automaticamente a tela ao abrir abas de pesquisa, PDF e outras",
"useChrome": "Suporta apenas navegadores baseados no mecanismo Chromium (por exemplo, Chrome/Edge), usar outros navegadores pode causar problemas de compatibilidade sem solução",
"clearAllAV": "Confirmar a limpeza de todos os bancos de dados não referenciados?",
"unreferencedAV": "Banco de dados não referenciado",
@ -1213,7 +1215,7 @@
"export23": "Exportar Markdown com front-matter YAML",
"export24": "Quando ativado, adiciona algumas informações gerais de metadados no início do arquivo Markdown exportado",
"export25": "Parâmetros de execução do Pandoc",
"export26": "Parâmetros do Pandoc utilizados ao exportar arquivo Word .docx",
"export26": "Parâmetros do Pandoc usados ao exportar arquivos Word .docx, por exemplo o parâmetro <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "Marca d'água para exportar PDF",
"export28": "Texto da marca d'água ou caminho do arquivo de marca d'água",
"export29": "Posição, tamanho e estilo da marca d'água, etc.",
@ -1333,6 +1335,7 @@
"column": "Coluna",
"copied": "Copiado",
"copy": "Copiar",
"copyFile": "Copiar arquivo",
"copyText": "Copiar texto *",
"delete-column": "Excluir Coluna",
"delete-row": "Excluir Linha",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Не разделять экран при открытии вкладки",
"noSplitScreenWhenOpenTabTip": "Не разделять экран автоматически при открытии вкладок поиска, PDF и других",
"useChrome": "Поддерживается только в браузерах на базе Chromium (например, Chrome/Edge), при использовании других браузеров могут возникнуть несовместимости, которые невозможно решить",
"clearAllAV": "Подтвердить очистку всех неиспользуемых баз данных?",
"unreferencedAV": "Неиспользуемая база данных",
@ -1213,7 +1215,7 @@
"export23": "Экспорт Markdown с YAML заголовком",
"export24": "После включения добавьте некоторую общую метаинформацию в начале экспортированного файла Markdown",
"export25": "Параметры запуска Pandoc",
"export26": "Параметры Pandoc, используемые при экспорте Word .docx",
"export26": "Параметры Pandoc, используемые при экспорте файла Word .docx, например параметр <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a>",
"export27": "Экспорт водяного знака PDF",
"export28": "Текст водяного знака или путь к файлу водяного знака",
"export29": "Положение, размер и стиль водяного знака и т.д.",
@ -1333,6 +1335,7 @@
"column": "Столбец",
"copied": "Скопировано",
"copy": "Копировать",
"copyFile": "Копировать файл",
"copyText": "Копировать текст *",
"delete-column": "Удалить столбец",
"delete-row": "Удалить строку",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "Sekme açarken ekranı bölme",
"noSplitScreenWhenOpenTabTip": "Arama, PDF gibi sekmeler açıldığında ekranı otomatik olarak bölme",
"useChrome": "Sadece Chromium tabanlı tarayıcılarda (ör. Chrome/Edge) kullanılabilir; diğer tarayıcılarda çözülemeyen uyumluluk sorunlarıyla karşılaşılabilir",
"clearAllAV": "Tüm atıfta bulunulmayan veritabanlarını temizlemek istediğinize emin misiniz?",
"unreferencedAV": "Başvurulmayan veritabanı",
@ -1213,7 +1215,7 @@
"export23": "Markdownu YAML üstbilgisiyle dışa aktar",
"export24": "Etkinleştirildiğinde dışa aktarılan Markdown başına genel meta bilgileri ekler",
"export25": "Pandoc çalıştırma parametreleri",
"export26": "Word .docx dosyası dışa aktarılırken kullanılan Pandoc parametreleri",
"export26": "Word .docx dosyası dışa aktarılırken kullanılan Pandoc parametreleri, örneğin <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a> parametresi",
"export27": "PDF filigranı",
"export28": "Filigran metni veya dosya yolu",
"export29": "Filigran konumu, boyutu ve stili",
@ -1333,6 +1335,7 @@
"column": "Sütun",
"copied": "Kopyalandı",
"copy": "Kopyala",
"copyFile": "Dosyayı kopyala",
"copyText": "Metni kopyala *",
"delete-column": "Sütunu sil",
"delete-row": "Satırı sil",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "打開分頁時不分屏",
"noSplitScreenWhenOpenTabTip": "打開搜尋、PDF 等分頁時不自動分屏",
"useChrome": "僅支援在基於 Chromium 核心的瀏覽器(例如 Chrome/Edge中使用使用其他瀏覽器會遇到無法解決的相容性問題",
"clearAllAV": "確認清理所有未引用的資料庫?",
"unreferencedAV": "未引用的資料庫",
@ -1213,7 +1215,7 @@
"export23": "導出 Markdown 添加 YAML front-matter",
"export24": "啟用後在導出的 Markdown 文件開頭處添加一些較為通用的元資料資訊",
"export25": "Pandoc 執行參數",
"export26": "匯出 Word .docx 檔案時使用的 Pandoc 參數",
"export26": "導出 Word .docx 檔案時使用的 Pandoc 參數,例如 <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a> 參數",
"export27": "導出 PDF 浮水印",
"export28": "浮水印文字或浮水印檔案路徑",
"export29": "浮水印位置、大小和樣式等",
@ -1333,6 +1335,7 @@
"column": "行",
"copied": "已複製",
"copy": "複製",
"copyFile": "複製檔案",
"copyText": "複製 文本 *",
"delete-column": "刪除行",
"delete-row": "刪除列",

View file

@ -1,4 +1,6 @@
{
"noSplitScreenWhenOpenTab": "打开页签时不分屏",
"noSplitScreenWhenOpenTabTip": "打开搜索、PDF 等页签时不自动分屏",
"useChrome": "仅支持在基于 Chromium 内核的浏览器(比如 Chrome/Edge中使用使用其他浏览器会遇到无法解决的兼容性问题",
"clearAllAV": "确认清理所有未引用的数据库?",
"unreferencedAV": "未引用的数据库",
@ -1213,7 +1215,7 @@
"export23": "导出 Markdown 添加 YAML front-matter",
"export24": "启用后在导出的 Markdown 文件开头处添加一些较为通用的元数据信息",
"export25": "Pandoc 执行参数",
"export26": "导出 Word .docx 文件时使用的 Pandoc 参数",
"export26": "导出 Word .docx 文件时使用的 Pandoc 参数,例如 <a href=\"https://pandoc.org/MANUAL.html#option--reference-doc\" target=\"_blank\">--reference-doc</a> 参数",
"export27": "导出 PDF 水印",
"export28": "水印文本或水印文件路径",
"export29": "水印位置、大小和样式等",
@ -1333,6 +1335,7 @@
"column": "列",
"copied": "已复制",
"copy": "复制",
"copyFile": "复制文件",
"copyText": "复制 文本 *",
"delete-column": "删除列",
"delete-row": "删除行",

View file

@ -44,7 +44,7 @@ import {blockRender} from "../protyle/render/blockRender";
import {renameAsset} from "../editor/rename";
import {electronUndo} from "../protyle/undo";
import {pushBack} from "../mobile/util/MobileBackFoward";
import {copyPNGByLink, exportAsset} from "./util";
import {copyAsset, copyPNGByLink, exportAsset} from "./util";
import {removeInlineType} from "../protyle/toolbar/util";
import {alignImgCenter, alignImgLeft} from "../protyle/wysiwyg/commonHotkey";
import {checkFold, renameTag} from "../util/noRelyPCFunction";
@ -1418,6 +1418,11 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
const dataSrc = imgElement.getAttribute("data-src");
if (dataSrc && dataSrc.startsWith("assets/")) {
window.siyuan.menus.menu.append(new MenuItem(exportAsset(dataSrc)).element);
/// #if !BROWSER
if (["windows", "darwin"].includes(window.siyuan.config.system.os)) {
window.siyuan.menus.menu.append(new MenuItem(copyAsset(dataSrc)).element);
}
/// #endif
}
if (protyle?.app?.plugins) {
emitOpenMenu({
@ -1691,6 +1696,11 @@ style="margin:4px 0;width: ${isMobile() ? "100%" : "360px"}" class="b3-text-fiel
openMenu(protyle.app, linkAddress, false, true);
if (linkAddress?.startsWith("assets/")) {
window.siyuan.menus.menu.append(new MenuItem(exportAsset(linkAddress)).element);
/// #if !BROWSER
if (["windows", "darwin"].includes(window.siyuan.config.system.os)) {
window.siyuan.menus.menu.append(new MenuItem(copyAsset(linkAddress)).element);
}
/// #endif
}
}
@ -2114,6 +2124,11 @@ export const videoMenu = (protyle: IProtyle, nodeElement: Element, type: string)
}
if (src && src.startsWith("assets/")) {
subMenus.push(exportAsset(src));
/// #if !BROWSER
if (["windows", "darwin"].includes(window.siyuan.config.system.os)) {
subMenus.push(copyAsset(src));
}
/// #endif
}
return subMenus;
};

View file

@ -11,6 +11,7 @@ import {MenuItem} from "./Menu";
import {App} from "../index";
import {exportByMobile, isInAndroid, updateHotkeyTip} from "../protyle/util/compatibility";
import {checkFold} from "../util/noRelyPCFunction";
import {showMessage} from "../dialog/message";
export const exportAsset = (src: string) => {
return {
@ -27,13 +28,39 @@ export const exportAsset = (src: string) => {
properties: ["showOverwriteConfirmation"],
});
if (!result.canceled) {
fetchPost("/api/file/copyFile", {src, dest: result.filePath});
fetchPost("/api/file/copyFile", {src, dest: result.filePath}, (response) => {
if (response.code === 0) {
showMessage(window.siyuan.languages.exported);
}
});
}
/// #endif
}
};
};
// 复制资源文件到系统剪贴板,在文件资源管理器中可粘贴为文件(仅 Windows、macOS 桌面端支持)
export const copyAsset = (src: string) => {
return {
id: "copy",
label: window.siyuan.languages.copyFile,
icon: "iconCopy",
click: () => {
/// #if !BROWSER
fetchPost("/api/clipboard/writeFilePath", {path: src}, (response) => {
if (response.code === 0) {
showMessage(window.siyuan.languages.copied);
} else {
showMessage(response.msg || "", response.data?.closeTimeout ?? 5000, "error");
}
});
/// #else
showMessage("Copy as file is only supported in the Windows and macOS desktop app");
/// #endif
}
};
};
export const openEditorTab = (app: App, ids: string[], notebookId?: string, pathString?: string, onlyGetMenus = false) => {
/// #if !MOBILE
const openSubmenus: IMenu[] = [{

View file

@ -17,17 +17,20 @@
package api
import (
"net/http"
"os"
"github.com/88250/clipboard"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func readFilePaths(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(200, ret)
defer c.JSON(http.StatusOK, ret)
var paths []string
if !gulu.OS.IsLinux() { // Linux 端不再支持 `粘贴为纯文本` 时处理文件绝对路径 https://github.com/siyuan-note/siyuan/issues/5825
@ -52,3 +55,37 @@ func readFilePaths(c *gin.Context) {
}
ret.Data = data
}
func writeFilePath(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
pathArg, ok := arg["path"].(string)
if !ok || pathArg == "" {
ret.Code = -1
ret.Msg = "path is required"
return
}
absPath, err := model.GetAssetAbsPath(pathArg)
if err != nil {
logging.LogErrorf("get asset [%s] abs path failed: %s", pathArg, err)
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
if err = util.WriteFilePaths([]string{absPath}); err != nil {
logging.LogErrorf("write file path to clipboard failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}

View file

@ -297,6 +297,7 @@ func ServeAPI(ginServer *gin.Engine) {
ginServer.Handle("POST", "/api/extension/copy", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, extensionCopy)
ginServer.Handle("POST", "/api/clipboard/readFilePaths", model.CheckAuth, model.CheckAdminRole, readFilePaths)
ginServer.Handle("POST", "/api/clipboard/writeFilePath", model.CheckAuth, model.CheckAdminRole, writeFilePath)
ginServer.Handle("POST", "/api/asset/uploadCloud", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uploadCloud)
ginServer.Handle("POST", "/api/asset/uploadCloudByAssetsPaths", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, uploadCloudByAssetsPaths)

View file

@ -21,36 +21,38 @@ import (
)
type FileTree struct {
AlwaysSelectOpenedFile bool `json:"alwaysSelectOpenedFile"` // 是否自动选中当前打开的文件
OpenFilesUseCurrentTab bool `json:"openFilesUseCurrentTab"` // 在当前页签打开文件
RefCreateSaveBox string `json:"refCreateSaveBox"` // 块引时新建文档存储笔记本
RefCreateSavePath string `json:"refCreateSavePath"` // 块引时新建文档存储路径
DocCreateSaveBox string `json:"docCreateSaveBox"` // 新建文档存储笔记本
DocCreateSavePath string `json:"docCreateSavePath"` // 新建文档存储路径
MaxListCount int `json:"maxListCount"` // 最大列出数量
MaxOpenTabCount int `json:"maxOpenTabCount"` // 最大打开页签数量
AllowCreateDeeper bool `json:"allowCreateDeeper"` // 允许创建超过 7 层深度的子文档
RemoveDocWithoutConfirm bool `json:"removeDocWithoutConfirm"` // 删除文档时是否不需要确认
CloseTabsOnStart bool `json:"closeTabsOnStart"` // 启动时关闭所有页签
UseSingleLineSave bool `json:"useSingleLineSave"` // 使用单行保存文档 .sy 和属性视图 .json
LargeFileWarningSize int `json:"largeFileWarningSize"` // 大文件警告大小单位MB
CreateDocAtTop *bool `json:"createDocAtTop"` // 在顶部创建新文档 https://github.com/siyuan-note/siyuan/issues/16327
Sort int `json:"sort"` // 排序方式
RecentDocsMaxListCount int `json:"recentDocsMaxListCount"` // 最近的文档最大列出数量
AlwaysSelectOpenedFile bool `json:"alwaysSelectOpenedFile"` // 是否自动选中当前打开的文件
OpenFilesUseCurrentTab bool `json:"openFilesUseCurrentTab"` // 在当前页签打开文件
RefCreateSaveBox string `json:"refCreateSaveBox"` // 块引时新建文档存储笔记本
RefCreateSavePath string `json:"refCreateSavePath"` // 块引时新建文档存储路径
DocCreateSaveBox string `json:"docCreateSaveBox"` // 新建文档存储笔记本
DocCreateSavePath string `json:"docCreateSavePath"` // 新建文档存储路径
MaxListCount int `json:"maxListCount"` // 最大列出数量
MaxOpenTabCount int `json:"maxOpenTabCount"` // 最大打开页签数量
AllowCreateDeeper bool `json:"allowCreateDeeper"` // 允许创建超过 7 层深度的子文档
RemoveDocWithoutConfirm bool `json:"removeDocWithoutConfirm"` // 删除文档时是否不需要确认
CloseTabsOnStart bool `json:"closeTabsOnStart"` // 启动时关闭所有页签
UseSingleLineSave bool `json:"useSingleLineSave"` // 使用单行保存文档 .sy 和属性视图 .json
LargeFileWarningSize int `json:"largeFileWarningSize"` // 大文件警告大小单位MB
CreateDocAtTop *bool `json:"createDocAtTop"` // 在顶部创建新文档 https://github.com/siyuan-note/siyuan/issues/16327
Sort int `json:"sort"` // 排序方式
RecentDocsMaxListCount int `json:"recentDocsMaxListCount"` // 最近的文档最大列出数量
NoSplitScreenWhenOpenTab bool `json:"noSplitScreenWhenOpenTab"` // 打开页签时不分屏 https://github.com/siyuan-note/siyuan/issues/16833
}
func NewFileTree() *FileTree {
return &FileTree{
AlwaysSelectOpenedFile: false,
OpenFilesUseCurrentTab: false,
Sort: util.SortModeCustom,
MaxListCount: 512,
MaxOpenTabCount: 8,
AllowCreateDeeper: false,
CloseTabsOnStart: false,
UseSingleLineSave: util.UseSingleLineSave,
LargeFileWarningSize: util.LargeFileWarningSize,
CreateDocAtTop: func() *bool { b := false; return &b }(),
AlwaysSelectOpenedFile: false,
OpenFilesUseCurrentTab: false,
Sort: util.SortModeCustom,
MaxListCount: 512,
MaxOpenTabCount: 8,
AllowCreateDeeper: false,
CloseTabsOnStart: false,
UseSingleLineSave: util.UseSingleLineSave,
LargeFileWarningSize: util.LargeFileWarningSize,
CreateDocAtTop: func() *bool { b := false; return &b }(),
NoSplitScreenWhenOpenTab: false,
}
}

View file

@ -38,6 +38,7 @@ require (
github.com/go-ole/go-ole v1.3.0
github.com/gofrs/flock v0.13.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/gonutz/w32/v2 v2.12.1
github.com/google/uuid v1.6.0
github.com/gorilla/css v1.0.1
github.com/gorilla/websocket v1.5.3

View file

@ -208,6 +208,8 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/gonutz/w32/v2 v2.12.1 h1:ZTWg6ZlETDfWK1Qxx+rdWQdQWZwfhiXoyvxzFYdgsUY=
github.com/gonutz/w32/v2 v2.12.1/go.mod h1:MgtHx0AScDVNKyB+kjyPder4xIi3XAcHS6LDDU2DmdE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=

View file

@ -92,6 +92,9 @@ func HandleAssetsRemoveEvent(assetAbsPath string) {
if filelock.IsHidden(assetAbsPath) {
return
}
if strings.HasSuffix(assetAbsPath, ".tmp") {
return
}
removeIndexAssetContent(assetAbsPath)
removeAssetThumbnail(assetAbsPath)
@ -114,6 +117,9 @@ func HandleAssetsChangeEvent(assetAbsPath string) {
if filelock.IsHidden(assetAbsPath) {
return
}
if strings.HasSuffix(assetAbsPath, ".tmp") {
return
}
indexAssetContent(assetAbsPath)
removeAssetThumbnail(assetAbsPath)

View file

@ -321,20 +321,6 @@ func InitConf() {
Conf.Export.PandocBin = util.PandocBinPath
}
docxTemplate := util.RemoveInvalid(Conf.Export.DocxTemplate)
if "" != docxTemplate {
params := util.RemoveInvalid(Conf.Export.PandocParams)
if gulu.File.IsExist(docxTemplate) && !strings.Contains(params, "--reference-doc") {
if !strings.HasPrefix(docxTemplate, "\"") {
docxTemplate = "\"" + docxTemplate + "\""
}
params += " --reference-doc " + docxTemplate
Conf.Export.PandocParams = strings.TrimSpace(params)
}
Conf.Export.DocxTemplate = ""
Conf.Save()
}
if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global {
Conf.Graph = conf.NewGraph()
}
@ -373,10 +359,6 @@ func InitConf() {
Conf.System.DisabledFeatures = []string{}
}
if nil == Conf.Snippet {
Conf.Snippet = conf.NewSnpt()
}
Conf.System.AppDir = util.WorkingDir
Conf.System.ConfDir = util.ConfDir
Conf.System.HomeDir = util.HomeDir
@ -390,6 +372,24 @@ func InitConf() {
Conf.System.OS = runtime.GOOS
Conf.System.OSPlatform = util.GetOSPlatform()
docxTemplate := util.RemoveInvalid(Conf.Export.DocxTemplate)
if "" != docxTemplate {
params := util.RemoveInvalid(Conf.Export.PandocParams)
if gulu.File.IsExist(docxTemplate) && !strings.Contains(params, "--reference-doc") && !Conf.System.IsMicrosoftStore {
if !strings.HasPrefix(docxTemplate, "\"") {
docxTemplate = "\"" + docxTemplate + "\""
}
params += " --reference-doc " + docxTemplate
Conf.Export.PandocParams = strings.TrimSpace(params)
}
Conf.Export.DocxTemplate = ""
Conf.Save()
}
if nil == Conf.Snippet {
Conf.Snippet = conf.NewSnpt()
}
if "" != Conf.UserData {
Conf.SetUser(loadUserFromConf())
}
@ -1267,7 +1267,7 @@ func subscribeConfEvents() {
Conf.Export.PandocBin = util.PandocBinPath
params := util.RemoveInvalid(Conf.Export.PandocParams)
if !strings.Contains(params, "--reference-doc") && "" != util.PandocTemplatePath {
if !strings.Contains(params, "--reference-doc") && "" != util.PandocTemplatePath && !Conf.System.IsMicrosoftStore {
params += " --reference-doc"
params += " \"" + util.PandocTemplatePath + "\""
Conf.Export.PandocParams = strings.TrimSpace(params)

View file

@ -3484,15 +3484,20 @@ func exportPandocConvertZip(baseFolderName string, docPaths, defBlockIDs []strin
continue
}
oldAsset := assetsNewOld[newAsset]
// 导出 Markdown 时链接路径中的空格被编码为 `%20`,需要替换回空格后才能正确获取原始资源路径
// Improve export of Markdown hyperlink spaces https://github.com/siyuan-note/siyuan/issues/9792
// No assets were exported when exporting Markdown https://github.com/siyuan-note/siyuan/issues/17046
spaceEncodedNewAsset := strings.ReplaceAll(newAsset, " ", "%20")
oldAsset := assetsNewOld[spaceEncodedNewAsset]
if "" == oldAsset {
logging.LogWarnf("get asset old path for new asset [%s] failed", newAsset)
logging.LogWarnf("get asset old path for new asset [%s] failed", spaceEncodedNewAsset)
continue
}
srcPath := assetsPathMap[oldAsset]
spaceDecodedOldAsset := strings.ReplaceAll(oldAsset, "%20", " ")
srcPath := assetsPathMap[spaceDecodedOldAsset]
if "" == srcPath {
logging.LogWarnf("get asset [%s] abs path failed", oldAsset)
logging.LogWarnf("get asset [%s] abs path failed", spaceDecodedOldAsset)
continue
}

View file

@ -581,11 +581,13 @@ func ImportSY(zipPath, boxID, toPath string) (err error) {
if err != nil {
return err
}
if d == nil {
if d == nil || unzipRootPath == path {
return nil
}
if strings.Contains(path, "assets") && d.IsDir() {
assetsDirs = append(assetsDirs, path)
if d.Name() == "assets" && d.IsDir() {
if syFiles, _ := filepath.Glob(filepath.Join(path, "*/*.sy")); 1 > len(syFiles) {
assetsDirs = append(assetsDirs, path)
}
}
return nil
})
@ -625,11 +627,13 @@ func ImportSY(zipPath, boxID, toPath string) (err error) {
if err != nil {
return err
}
if d == nil {
if d == nil || unzipRootPath == path {
return nil
}
if strings.Contains(path, "emojis") && d.IsDir() {
emojiDirs = append(emojiDirs, path)
if d.Name() == "emojis" && d.IsDir() {
if syFiles, _ := filepath.Glob(filepath.Join(path, "*/*.sy")); 1 > len(syFiles) {
emojiDirs = append(emojiDirs, path)
}
}
return nil
})
@ -995,7 +999,7 @@ func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
}
if strings.HasSuffix(absolutePath, ".md") || strings.HasSuffix(absolutePath, ".markdown") {
if !strings.Contains(absolutePath, "assets") {
if !strings.Contains(filepath.ToSlash(absolutePath), "/assets/") {
// 链接 .md 文件的情况下只有路径中包含 assets 才算作资源文件,其他情况算作文档链接,后续在 convertMdHyperlinks2WikiLinks 中处理
// Supports converting relative path hyperlinks into document block references after importing Markdown https://github.com/siyuan-note/siyuan/issues/13817
return ast.WalkContinue

26
kernel/util/clipboard.go Normal file
View file

@ -0,0 +1,26 @@
// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !windows && !darwin
package util
import "errors"
// 当前仅在 Windows、macOS 上实现,其他平台返回错误
func WriteFilePaths(paths []string) error {
return errors.New("writing file paths to clipboard is not supported on this platform")
}

View file

@ -0,0 +1,104 @@
// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build darwin
// 本文件实现 macOS NSPasteboard 写入文件路径列表:通过 writeObjects: 写入 NSURL 数组
//NSPasteboardTypeFileURL / public.file-url使 Finder 等应用可识别并粘贴为文件。
//
// 逻辑依据 Apple 官方「复制到剪贴板」三步:
// 1) 获取 general pasteboard2) clearContents 清空3) writeObjects: 写入符合 NSPasteboardWriting 的对象。
// NSURL 为系统内置支持类型,写入 file URL 后系统会自动提供 public.file-url、
// NSFilenamesPboardType、public.utf8-plain-text 等表示,兼容 Finder 与旧版 API。
//
// 官方文档与参考:
// - Pasteboard Programming Guide (macOS)
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/PasteboardGuide106/Introduction/Introduction.html
// - Copying to a Pasteboard三步流程与 writeObjects:
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/PasteboardGuide106/Articles/pbCopying.html
// - NSPasteboard
// https://developer.apple.com/documentation/appkit/nspasteboard
// - NSPasteboardWritingNSURL、NSString 等已实现)
// https://developer.apple.com/documentation/appkit/nspasteboardwriting
//
// 下文 /* ... */ 内为 CGO 内联的 Objective-C 代码,由 cgo 提取并编译,并非被注释掉的代码。
package util
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework AppKit -framework Foundation
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
// writeFilePathsToPasteboard 将路径列表写入通用剪贴板,遵循 Copying to a Pasteboard 三步:
// 1) generalPasteboard2) clearContents3) writeObjects: 传入 NSURL 数组。
// NSURL 符合 NSPasteboardWriting写入后系统自动提供 public.file-url、NSFilenamesPboardType 等。
// paths 为 UTF-8 路径字符串数组count 为数量。
static int writeFilePathsToPasteboard(const char** paths, int count) {
if (count <= 0) return 0;
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:(NSUInteger)count];
for (int i = 0; i < count; i++) {
NSString *path = [NSString stringWithUTF8String:paths[i]];
if (!path) continue;
NSURL *url = [NSURL fileURLWithPath:path];
if (url) [arr addObject:url];
}
// 若无一有效路径(如全为非法 UTF-8 或无法转为 NSURL返回 -2 以便 Go 侧报错
if ([arr count] == 0) return -2;
// 步骤 1获取通用剪贴板cut/copy/paste 用)
NSPasteboard *pb = [NSPasteboard generalPasteboard];
// 步骤 2清空已有内容再只写入本次文件路径
[pb clearContents];
// 步骤 3writeObjects: 要求对象符合 NSPasteboardWritingNSURL 已支持
BOOL ok = [pb writeObjects:arr];
return ok ? 0 : -1;
}
*/
import "C"
import (
"errors"
"unsafe"
)
// WriteFilePaths 将文件路径列表写入系统剪贴板general pasteboard
// 使 Finder 等可粘贴为文件。实现见 Pasteboard Guide — Copying to a Pasteboard。
func WriteFilePaths(paths []string) error {
if len(paths) == 0 {
return nil
}
// 分配 C 的 char* 数组,便于传入 Objective-C
cPaths := make([]*C.char, len(paths))
for i, p := range paths {
cPaths[i] = C.CString(p)
}
defer func() {
for _, c := range cPaths {
C.free(unsafe.Pointer(c))
}
}()
// 取首元素地址作为 const char** 传入
ret := C.writeFilePathsToPasteboard((**C.char)(unsafe.Pointer(&cPaths[0])), C.int(len(paths)))
switch ret {
case 0:
return nil
case -2:
return errors.New("no valid file paths to write (invalid UTF-8 or path)")
default:
return errors.New("failed to write file paths to pasteboard")
}
}

View file

@ -0,0 +1,162 @@
// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build windows
// 本文件实现 Windows Shell 剪贴板格式 CF_HDROP用于在剪贴板中传输一组已有文件的路径使资源管理器等可识别并粘贴为文件。
//
// 参考文档:
// - Shell 剪贴板与 CF_HDROPhttps://learn.microsoft.com/en-us/windows/win32/shell/clipboard
// - DROPFILES 结构https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-dropfiles
// - SetClipboardDatahMem 须为 GMEM_MOVEABLE且 “memory must be unlocked before the Clipboard is closed”
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
// - 官方示例 “Copy information to the clipboard”GlobalUnlock 再 SetClipboardData
// https://learn.microsoft.com/en-us/windows/win32/dataxchg/using-the-clipboard
//
// CF_HDROP 为预定义格式,无需 RegisterClipboardFormat。数据为全局内存对象hGlobal
// 其内容为 DROPFILES 结构 + 双 null 结尾的路径字符数组。
package util
import (
"encoding/binary"
"errors"
"runtime"
"syscall"
"time"
"unsafe"
"github.com/gonutz/w32/v2"
)
const (
// cfHDROP 为 CF_HDROP 剪贴板格式(预定义值 15用于传输一组已有文件的位置。
// 见 Standard Clipboard Formatshttps://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
cfHDROP = 15
dropfilesSize = 20 // DROPFILES 结构体大小pFiles 4 + pt 8 + fNC 4 + fWide 4https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-dropfiles
)
// WriteFilePaths 将文件路径列表写入系统剪贴板,使资源管理器中可粘贴为文件。
//
// 按文档要求CF_HDROP 数据为 STGMEDIUM 的 hGlobal 指向的全局内存,内存内容为 DROPFILES 结构。
// 剪贴板 API 要求在同一线程内完成 OpenClipboard、写入、CloseClipboard故需 LockOSThread。
// 调用顺序先准备数据GlobalAlloc → GlobalLock → 写入 → GlobalUnlock再 OpenClipboard → EmptyClipboard → SetClipboardData → CloseClipboard。
// 与官方示例 “Copy information to the clipboard” 不同,此处将内存准备提前到 OpenClipboard 之前,以缩短占用剪贴板的时间。
func WriteFilePaths(paths []string) error {
if len(paths) == 0 {
return nil
}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
data, err := buildDropfilesData(paths)
if err != nil {
return err
}
if len(data) == 0 {
return nil
}
// 全局内存对象SetClipboardData 文档要求 hMem 须由 GlobalAlloc(GMEM_MOVEABLE) 分配
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
size := uint32(len(data))
hMem := w32.GlobalAlloc(w32.GMEM_MOVEABLE, size)
if hMem == 0 {
return syscall.Errno(w32.GetLastError())
}
ptr := w32.GlobalLock(hMem)
if ptr == nil {
w32.GlobalFree(hMem)
return syscall.Errno(w32.GetLastError())
}
w32.MoveMemory(ptr, unsafe.Pointer(&data[0]), size)
// 必须在 SetClipboardData 之前 Unlock否则系统无法正确管理已接管的句柄。
// 文档:"The memory must be unlocked before the Clipboard is closed."
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
w32.GlobalUnlock(hMem)
if err := waitOpenClipboard(); err != nil {
w32.GlobalFree(hMem)
return err
}
defer w32.CloseClipboard()
if !w32.EmptyClipboard() {
w32.GlobalFree(hMem)
return syscall.Errno(w32.GetLastError())
}
if w32.SetClipboardData(cfHDROP, w32.HANDLE(hMem)) == 0 {
w32.GlobalFree(hMem)
return syscall.Errno(w32.GetLastError())
}
// 成功时系统接管 hMem应用不得再写或 free失败时由上面分支 GlobalFree。
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
return nil
}
// buildDropfilesData 构建 CF_HDROP 格式的字节切片。
//
// 格式遵循 DROPFILESpFiles 为偏移,指向双 null 结尾的路径字符数组。
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-dropfiles
// 数组由若干条“完整路径 + 结尾 NULL”组成最后再跟一个 NULL 结束整表。
// 例如两文件时为c:\temp1.txt\0 c:\temp2.txt\0 \0
// 此处使用 UnicodefWide=1故路径为 UTF-16每条路径含结尾 null最后再 2 字节 null。
func buildDropfilesData(paths []string) ([]byte, error) {
var totalLen = dropfilesSize
for _, p := range paths {
u16, err := syscall.UTF16FromString(p)
if err != nil {
return nil, err
}
totalLen += len(u16) * 2
}
totalLen += 2 // 数组末尾的 null双 null 结尾中的最后一个)
buf := make([]byte, totalLen)
// DROPFILESpFiles=20路径数组相对本结构起始的偏移, pt=0,0, fNC=0, fWide=1Unicode
binary.LittleEndian.PutUint32(buf[0:4], 20)
// pt.x, pt.y, fNC, fWide
binary.LittleEndian.PutUint32(buf[16:20], 1)
offset := dropfilesSize
for _, p := range paths {
u16, err := syscall.UTF16FromString(p)
if err != nil {
return nil, err
}
for _, c := range u16 {
binary.LittleEndian.PutUint16(buf[offset:offset+2], c)
offset += 2
}
}
return buf, nil
}
// waitOpenClipboard 在限定时间内重试打开剪贴板。
// 同一时刻仅一进程可持有剪贴板OpenClipboard 成功)。
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openclipboard
func waitOpenClipboard() error {
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if w32.OpenClipboard(0) {
return nil
}
time.Sleep(time.Millisecond)
}
return errors.New("open clipboard timeout")
}

View file

@ -355,33 +355,13 @@ func IsSensitivePath(p string) bool {
}
pp := filepath.Clean(strings.ToLower(p))
// 精确敏感文件
exact := []string{
"/etc/passwd",
"/etc/shadow",
"/etc/gshadow",
"/var/run/secrets/kubernetes.io/serviceaccount/token",
}
for _, e := range exact {
if pp == e {
return true
}
}
// 敏感目录前缀UNIX 风格)
prefixes := []string{
"/etc/ssh",
"/root",
"/etc/ssl",
"/etc/cron.d/",
"/etc/letsencrypt",
"/var/lib/docker",
"/.gnupg",
"/.ssh",
"/.aws",
"/.kube",
"/.docker",
"/.config/gcloud",
"/etc",
"/var/lib/",
"/.",
}
for _, pre := range prefixes {
if strings.HasPrefix(pp, pre) {
@ -393,7 +373,6 @@ func IsSensitivePath(p string) bool {
winPrefixes := []string{
`c:\windows\system32`,
`c:\windows\system`,
`c:\users\`,
}
for _, wp := range winPrefixes {
if strings.HasPrefix(pp, strings.ToLower(wp)) {
@ -401,42 +380,15 @@ func IsSensitivePath(p string) bool {
}
}
// 文件名级别检查
base := filepath.Base(pp)
n := strings.ToLower(base)
sensitiveNames := map[string]struct{}{
".bashrc": {},
".env": {},
".env.local": {},
".npmrc": {},
".netrc": {},
"id_rsa": {},
"id_dsa": {},
"id_ecdsa": {},
"id_ed25519": {},
"authorized_keys": {},
"passwd": {},
"shadow": {},
"pgpass": {},
"hosts": {},
"credentials": {}, // 如 aws credentials
"config.json": {}, // docker config.json 可能含 token
homePrefixes := []string{
strings.ToLower(filepath.Join(HomeDir, ".ssh")),
strings.ToLower(filepath.Join(HomeDir, ".config")),
strings.ToLower(filepath.Join(HomeDir, ".bashrc")),
strings.ToLower(filepath.Join(HomeDir, ".zshrc")),
strings.ToLower(filepath.Join(HomeDir, ".profile")),
}
if _, ok := sensitiveNames[n]; ok {
return true
}
// 支持 .env.* 之类的模式
if n == ".env" || strings.HasPrefix(n, ".env.") {
return true
}
// 扩展名级别检查
ext := strings.ToLower(filepath.Ext(n))
sensitiveExts := []string{
".pem", ".key", ".p12", ".pfx", ".ppk", ".asc", ".gpg",
}
for _, se := range sensitiveExts {
if ext == se {
for _, hp := range homePrefixes {
if strings.HasPrefix(pp, hp) {
return true
}
}