diff --git a/app/appearance/langs/ar_SA.json b/app/appearance/langs/ar_SA.json index 59ed86af4..d05d29fed 100644 --- a/app/appearance/langs/ar_SA.json +++ b/app/appearance/langs/ar_SA.json @@ -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، على سبيل المثال المعامل --reference-doc", "export27": "‫العلامة المائية لملف PDF المصدّر‬", "export28": "نص العلامة المائية المخصصة أو مسار ملف العلامة المائية", "export29": "موقع العلامة المائية، حجمها وأسلوبها، إلخ", @@ -1333,6 +1335,7 @@ "column": "العمود", "copied": "تم النسخ", "copy": "نسخ", + "copyFile": "نسخ الملف", "copyText": "نسخ النص *", "delete-column": "حذف العمود", "delete-row": "حذف الصف", diff --git a/app/appearance/langs/de_DE.json b/app/appearance/langs/de_DE.json index 40981418c..6fdbace5e 100644 --- a/app/appearance/langs/de_DE.json +++ b/app/appearance/langs/de_DE.json @@ -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 --reference-doc", "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", diff --git a/app/appearance/langs/en_US.json b/app/appearance/langs/en_US.json index 31baa7e1d..e5795456e 100644 --- a/app/appearance/langs/en_US.json +++ b/app/appearance/langs/en_US.json @@ -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 --reference-doc 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", diff --git a/app/appearance/langs/es_ES.json b/app/appearance/langs/es_ES.json index 05dcd12aa..7edc74ccd 100644 --- a/app/appearance/langs/es_ES.json +++ b/app/appearance/langs/es_ES.json @@ -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 --reference-doc", "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", diff --git a/app/appearance/langs/fr_FR.json b/app/appearance/langs/fr_FR.json index c1f0a66d2..60feebe85 100644 --- a/app/appearance/langs/fr_FR.json +++ b/app/appearance/langs/fr_FR.json @@ -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 --reference-doc", "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", diff --git a/app/appearance/langs/he_IL.json b/app/appearance/langs/he_IL.json index 8639a9f85..4e1d1feb5 100644 --- a/app/appearance/langs/he_IL.json +++ b/app/appearance/langs/he_IL.json @@ -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, למשל הפרמטר --reference-doc", "export27": "ייצוא סימן מים PDF", "export28": "טקסט סימן מים או נתיב קובץ סימן מים", "export29": "מיקום סימן מים, גודל וסגנון, וכו'", @@ -1333,6 +1335,7 @@ "column": "עמודה", "copied": "הועתק", "copy": "העתק", + "copyFile": "העתק קובץ", "copyText": "העתק טקסט *", "delete-column": "מחק עמודה", "delete-row": "מחק שורה", diff --git a/app/appearance/langs/it_IT.json b/app/appearance/langs/it_IT.json index d19dc3098..eb90d41ba 100644 --- a/app/appearance/langs/it_IT.json +++ b/app/appearance/langs/it_IT.json @@ -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 --reference-doc", "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", diff --git a/app/appearance/langs/ja_JP.json b/app/appearance/langs/ja_JP.json index 9f79a9a68..6ab0082cd 100644 --- a/app/appearance/langs/ja_JP.json +++ b/app/appearance/langs/ja_JP.json @@ -1,4 +1,6 @@ { + "noSplitScreenWhenOpenTab": "タブを開くときに分割表示しない", + "noSplitScreenWhenOpenTabTip": "検索や PDF などのタブを開いたときに自動で分割表示しない", "useChrome": "Chromium ベースのブラウザ(例えば Chrome/Edge)でのみ使用可能で、他のブラウザでは解決できない互換性の問題が発生することがあります", "clearAllAV": "すべての参照されていないデータベースを削除してもよろしいですか?", "unreferencedAV": "参照されていないデータベース", @@ -1212,8 +1214,8 @@ "export22": "%page は現在のページ番号、%pages は総ページ数で、Sprig テンプレート関数に対応しています", "export23": "YAML フロントマター付きの Markdown をエクスポート", "export24": "エクスポートされる Markdown ファイルの先頭に一般的なメタデータを追加します", -"export25": "Pandoc 実行パラメータ", - "export26": "Word .docx ファイルをエクスポートする際に使用する Pandoc パラメータ", + "export25": "Pandoc 実行パラメータ", + "export26": "Word .docx ファイルをエクスポートする際に使用する Pandoc のパラメータ、例えば --reference-doc パラメータ", "export27": "PDF に透かしを書き出す", "export28": "透かしテキストまたは透かし画像ファイルのパス", "export29": "透かしの位置、サイズ、スタイルなど", @@ -1333,6 +1335,7 @@ "column": "列", "copied": "コピーしました", "copy": "コピー", + "copyFile": "ファイルをコピー", "copyText": "テキストをコピー *", "delete-column": "列を削除", "delete-row": "行を削除", diff --git a/app/appearance/langs/ko_KR.json b/app/appearance/langs/ko_KR.json index 53d3815cd..5bc17503f 100644 --- a/app/appearance/langs/ko_KR.json +++ b/app/appearance/langs/ko_KR.json @@ -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 매개변수, 예: --reference-doc 매개변수", "export27": "PDF 워터마크 내보내기", "export28": "워터마크 텍스트 또는 워터마크 파일 경로", "export29": "워터마크 위치, 크기 및 스타일 등", @@ -1333,6 +1335,7 @@ "column": "열", "copied": "복사됨", "copy": "복사", + "copyFile": "파일 복사", "copyText": "텍스트 복사 *", "delete-column": "열 삭제", "delete-row": "행 삭제", diff --git a/app/appearance/langs/pl_PL.json b/app/appearance/langs/pl_PL.json index 6fd96f5dd..2092c5395 100644 --- a/app/appearance/langs/pl_PL.json +++ b/app/appearance/langs/pl_PL.json @@ -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 --reference-doc", "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", diff --git a/app/appearance/langs/pt_BR.json b/app/appearance/langs/pt_BR.json index e471ed4d2..34cbedf63 100644 --- a/app/appearance/langs/pt_BR.json +++ b/app/appearance/langs/pt_BR.json @@ -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 --reference-doc", "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", diff --git a/app/appearance/langs/ru_RU.json b/app/appearance/langs/ru_RU.json index 9c859ff43..81f081d58 100644 --- a/app/appearance/langs/ru_RU.json +++ b/app/appearance/langs/ru_RU.json @@ -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, например параметр --reference-doc", "export27": "Экспорт водяного знака PDF", "export28": "Текст водяного знака или путь к файлу водяного знака", "export29": "Положение, размер и стиль водяного знака и т.д.", @@ -1333,6 +1335,7 @@ "column": "Столбец", "copied": "Скопировано", "copy": "Копировать", + "copyFile": "Копировать файл", "copyText": "Копировать текст *", "delete-column": "Удалить столбец", "delete-row": "Удалить строку", diff --git a/app/appearance/langs/tr_TR.json b/app/appearance/langs/tr_TR.json index 24c942ce5..3313756d6 100644 --- a/app/appearance/langs/tr_TR.json +++ b/app/appearance/langs/tr_TR.json @@ -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": "Markdown’u 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 --reference-doc 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", diff --git a/app/appearance/langs/zh_CHT.json b/app/appearance/langs/zh_CHT.json index 8e64f3276..5749fd0d4 100644 --- a/app/appearance/langs/zh_CHT.json +++ b/app/appearance/langs/zh_CHT.json @@ -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 參數,例如 --reference-doc 參數", "export27": "導出 PDF 浮水印", "export28": "浮水印文字或浮水印檔案路徑", "export29": "浮水印位置、大小和樣式等", @@ -1333,6 +1335,7 @@ "column": "行", "copied": "已複製", "copy": "複製", + "copyFile": "複製檔案", "copyText": "複製 文本 *", "delete-column": "刪除行", "delete-row": "刪除列", diff --git a/app/appearance/langs/zh_CN.json b/app/appearance/langs/zh_CN.json index 163ed92d4..a3fea0878 100644 --- a/app/appearance/langs/zh_CN.json +++ b/app/appearance/langs/zh_CN.json @@ -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 参数,例如 --reference-doc 参数", "export27": "导出 PDF 水印", "export28": "水印文本或水印文件路径", "export29": "水印位置、大小和样式等", @@ -1333,6 +1335,7 @@ "column": "列", "copied": "已复制", "copy": "复制", + "copyFile": "复制文件", "copyText": "复制 文本 *", "delete-column": "删除列", "delete-row": "删除行", diff --git a/app/src/menus/protyle.ts b/app/src/menus/protyle.ts index fcaa31dca..3e5528111 100644 --- a/app/src/menus/protyle.ts +++ b/app/src/menus/protyle.ts @@ -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; }; diff --git a/app/src/menus/util.ts b/app/src/menus/util.ts index 993b2331c..0cb2d5687 100644 --- a/app/src/menus/util.ts +++ b/app/src/menus/util.ts @@ -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[] = [{ diff --git a/kernel/api/clipboard.go b/kernel/api/clipboard.go index 6d5b8f8c7..db11661d2 100644 --- a/kernel/api/clipboard.go +++ b/kernel/api/clipboard.go @@ -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 + } +} diff --git a/kernel/api/router.go b/kernel/api/router.go index e207a5a41..611bb7ed4 100644 --- a/kernel/api/router.go +++ b/kernel/api/router.go @@ -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) diff --git a/kernel/conf/filetree.go b/kernel/conf/filetree.go index 4548571b5..6a616d3c0 100644 --- a/kernel/conf/filetree.go +++ b/kernel/conf/filetree.go @@ -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, } } diff --git a/kernel/go.mod b/kernel/go.mod index b9ac4b4b5..285eba1ae 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -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 diff --git a/kernel/go.sum b/kernel/go.sum index 4ea8278b4..9166c7757 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -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= diff --git a/kernel/model/assets.go b/kernel/model/assets.go index 1b35dc621..bc87f3bd2 100644 --- a/kernel/model/assets.go +++ b/kernel/model/assets.go @@ -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) diff --git a/kernel/model/conf.go b/kernel/model/conf.go index 8eb1a9bb2..237dce979 100644 --- a/kernel/model/conf.go +++ b/kernel/model/conf.go @@ -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) diff --git a/kernel/model/export.go b/kernel/model/export.go index db514d095..92aba438b 100644 --- a/kernel/model/export.go +++ b/kernel/model/export.go @@ -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 } diff --git a/kernel/model/import.go b/kernel/model/import.go index cf64c9cb1..18eaa0054 100644 --- a/kernel/model/import.go +++ b/kernel/model/import.go @@ -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 diff --git a/kernel/util/clipboard.go b/kernel/util/clipboard.go new file mode 100644 index 000000000..6147d2328 --- /dev/null +++ b/kernel/util/clipboard.go @@ -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 . + +//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") +} diff --git a/kernel/util/clipboard_darwin.go b/kernel/util/clipboard_darwin.go new file mode 100644 index 000000000..38db018a9 --- /dev/null +++ b/kernel/util/clipboard_darwin.go @@ -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 . + +//go:build darwin + +// 本文件实现 macOS NSPasteboard 写入文件路径列表:通过 writeObjects: 写入 NSURL 数组 +//(NSPasteboardTypeFileURL / public.file-url),使 Finder 等应用可识别并粘贴为文件。 +// +// 逻辑依据 Apple 官方「复制到剪贴板」三步: +// 1) 获取 general pasteboard;2) 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 +// - NSPasteboardWriting(NSURL、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 +#import + +// writeFilePathsToPasteboard 将路径列表写入通用剪贴板,遵循 Copying to a Pasteboard 三步: +// 1) generalPasteboard;2) clearContents;3) 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]; + // 步骤 3:writeObjects: 要求对象符合 NSPasteboardWriting,NSURL 已支持 + 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") + } +} diff --git a/kernel/util/clipboard_windows.go b/kernel/util/clipboard_windows.go new file mode 100644 index 000000000..ce75ea58c --- /dev/null +++ b/kernel/util/clipboard_windows.go @@ -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 . + +//go:build windows + +// 本文件实现 Windows Shell 剪贴板格式 CF_HDROP,用于在剪贴板中传输一组已有文件的路径,使资源管理器等可识别并粘贴为文件。 +// +// 参考文档: +// - Shell 剪贴板与 CF_HDROP:https://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 +// - SetClipboardData(hMem 须为 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 Formats:https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats + cfHDROP = 15 + dropfilesSize = 20 // DROPFILES 结构体大小(pFiles 4 + pt 8 + fNC 4 + fWide 4),https://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 格式的字节切片。 +// +// 格式遵循 DROPFILES:pFiles 为偏移,指向双 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 +// 此处使用 Unicode(fWide=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) + // DROPFILES:pFiles=20(路径数组相对本结构起始的偏移), pt=0,0, fNC=0, fWide=1(Unicode) + 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") +} diff --git a/kernel/util/path.go b/kernel/util/path.go index 7cf834497..8711e87ae 100644 --- a/kernel/util/path.go +++ b/kernel/util/path.go @@ -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 } }