Browse Source

Merge branch 'master' into feat/db

Gabriel Horner 2 years ago
parent
commit
097a59d9c6

+ 2 - 2
android/app/build.gradle

@@ -7,8 +7,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 71
-        versionName "0.9.18"
+        versionCode 72
+        versionName "0.9.19"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 13 - 0
externs.js

@@ -8,6 +8,8 @@ fs.unlink = function() {};
 fs.readdir = function() {};
 fs.rmdir = function() {};
 fs.rimraf = function() {};
+fs.lstat = function () {};
+
 var dummy = {};
 dummy.populateStat = function() {};
 dummy.populateHash = function() {};
@@ -141,6 +143,17 @@ dummy.ELEMENT = function() {};
 dummy.TEXT = function() {};
 dummy.isAbsolute = function() {};
 
+var utils = {}
+utils.withFileTypes = true;
+utils.accessTime = 0;
+utils.modifiedTime = 0;
+utils.changeTime = 0;
+utils.birthTime = 0;
+utils.atimeMs = 0;
+utils.mtimeMs = 0;
+utils.ctimeMs = 0;
+utils.birthtimeMs = 0;
+
 /**
  * @typedef {{
  *     recursive: (undefined | boolean),

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -519,7 +519,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.18;
+				MARKETING_VERSION = 0.9.19;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -546,7 +546,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.18;
+				MARKETING_VERSION = 0.9.19;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -571,7 +571,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.18;
+				MARKETING_VERSION = 0.9.19;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -598,7 +598,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.18;
+				MARKETING_VERSION = 0.9.19;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 0
libs/src/LSPlugin.ts

@@ -192,6 +192,7 @@ export interface BlockEntity {
   level?: number
   meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
   title?: Array<any>
+	marker?: string
 }
 
 /**

+ 1 - 1
resources/forge.config.js

@@ -4,7 +4,7 @@ module.exports = {
   packagerConfig: {
     name: 'Logseq',
     icon: './icons/logseq_big_sur.icns',
-    buildVersion: 71,
+    buildVersion: 72,
     protocols: [
       {
         "protocol": "logseq",

+ 16 - 0
resources/js/preload.js

@@ -18,6 +18,20 @@ function getFilePathFromClipboard () {
   }
 }
 
+/**
+ * Read the contents of the clipboard for a custom format.
+ * @param  {string} format The custom format to read.
+ * @returns Buffer containing the contents of the clipboard for the specified format, or null if not available.
+ */
+function getClipboardData (format) {
+  if (clipboard.has(format, "clipboard")) {
+    return clipboard.readBuffer(format)
+  }
+  else {
+    return null;
+  }
+}
+
 contextBridge.exposeInMainWorld('apis', {
   doAction: async (arg) => {
     return await ipcRenderer.invoke('main', arg)
@@ -172,6 +186,8 @@ contextBridge.exposeInMainWorld('apis', {
 
   getFilePathFromClipboard,
 
+  getClipboardData,
+
   setZoomFactor (factor) {
     webFrame.setZoomFactor(factor)
   },

+ 1 - 1
resources/package.json

@@ -1,7 +1,7 @@
 {
   "name": "Logseq",
   "productName": "Logseq",
-  "version": "0.9.18",
+  "version": "0.9.19",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 31 - 37
src/electron/electron/core.cljs

@@ -17,7 +17,6 @@
             ["os" :as os]
             ["electron" :refer [BrowserWindow Menu app protocol ipcMain dialog shell] :as electron]
             ["electron-deeplink" :refer [Deeplink]]
-            [clojure.core.async :as async]
             [electron.state :as state]
             [electron.git :as git]
             [electron.window :as win]
@@ -209,7 +208,7 @@
                                      :label "About Logseq"
                                      :click about-fn}]}))
         ;; Enable Cmd/Ctrl+= Zoom In
-        template (conj template 
+        template (conj template
                        {:role "zoomin"
                         :accelerator "CommandOrControl+="})
         menu (.buildFromTemplate Menu (clj->js template))]
@@ -266,33 +265,28 @@
            (@*setup-fn)
 
            ;; main window events
-           ;; TODO merge with window/on-close-actions!
-           ;; TODO elimilate the difference between main and non-main windows
            (.on win "close" (fn [e]
-                              (when @*quit-dirty? ;; when not updating
-                                (.preventDefault e)
-                                (let [web-contents (. win -webContents)]
-                                  (.send web-contents "persist-zoom-level" (.getZoomLevel web-contents))
-                                  (.send web-contents "persistent-dbs"))
-                                (async/go
-                                  (let [_ (async/<! state/persistent-dbs-chan)]
-                                    (if (or @win/*quitting? (not mac?))
-                                      ;; MacOS: only cmd+q quitting will trigger actual closing
-                                      ;; otherwise, it's just hiding - don't do any actual closing in that case
-                                      ;; except saving transit
-                                      (when-let [win @*win]
-                                        (when-let [dir (state/get-window-graph-path win)]
-                                          (handler/close-watcher-when-orphaned! win dir))
-                                        (state/close-window! win)
-                                        (win/destroy-window! win)
-                                        ;; FIXME: what happens when closing main window on Windows?
-                                        (reset! *win nil))
-                                      ;; Just hiding - don't do any actual closing operation
-                                      (do (.preventDefault ^js/Event e)
-                                          (if (and mac? (.isFullScreen win))
-                                            (do (.once win "leave-full-screen" #(.hide win))
-                                                (.setFullScreen win false))
-                                            (.hide win)))))))))
+                                  (when @*quit-dirty? ;; when not updating
+                                    (.preventDefault e)
+
+                                    (let [windows (win/get-all-windows)
+                                          window @*win
+                                          multiple-windows? (> (count windows) 1)]
+                                      (cond
+                                        (or multiple-windows? (not mac?) @win/*quitting?)
+                                        (when window
+                                          (win/close-handler win handler/close-watcher-when-orphaned! e)
+                                          (reset! *win nil))
+
+                                        (and mac? (not multiple-windows?))
+                                        ;; Just hiding - don't do any actual closing operation
+                                        (do (.preventDefault ^js/Event e)
+                                            (if (and mac? (.isFullScreen win))
+                                              (do (.once win "leave-full-screen" #(.hide win))
+                                                  (.setFullScreen win false))
+                                              (.hide win)))
+                                        :else
+                                        nil)))))
            (.on app "before-quit" (fn [_e]
                                     (reset! win/*quitting? true)))
 
@@ -309,15 +303,15 @@
                       :bypassCSP       true
                       :supportFetchAPI true}]
       (.registerSchemesAsPrivileged
-        protocol (bean/->js [{:scheme     LSP_SCHEME
-                              :privileges privileges}
-                             {:scheme     FILE_LSP_SCHEME
-                              :privileges privileges}
-                             {:scheme     FILE_ASSETS_SCHEME
-                              :privileges {:standard        false
-                                           :secure          false
-                                           :bypassCSP       false
-                                           :supportFetchAPI false}}]))
+       protocol (bean/->js [{:scheme     LSP_SCHEME
+                             :privileges privileges}
+                            {:scheme     FILE_LSP_SCHEME
+                             :privileges privileges}
+                            {:scheme     FILE_ASSETS_SCHEME
+                             :privileges {:standard        false
+                                          :secure          false
+                                          :bypassCSP       false
+                                          :supportFetchAPI false}}]))
 
       (set-app-menu!)
       (setup-deeplink!)

+ 18 - 11
src/electron/electron/search.cljs

@@ -145,6 +145,7 @@
     [db-name (node-path/join search-dir db-name)]))
 
 (defn open-db!
+  "Open a SQLite db for search index"
   [db-name]
   (let [[db-sanitized-name db-full-path] (get-db-full-path db-name)]
     (try (let [db (sqlite3 db-full-path nil)]
@@ -157,7 +158,13 @@
            (swap! databases assoc db-sanitized-name db))
          (catch :default e
            (logger/error (str e ": " db-name))
-           (fs/unlinkSync db-full-path)))))
+           (try
+             (fs/unlinkSync db-full-path)
+             (catch :default e
+               (logger/error "cannot unlink search db:" e)
+               (utils/send-to-renderer "notification"
+                                       {:type    "error"
+                                        :payload (str "Search index error, please manually delete “" db-full-path "”: \n" e)})))))))
 
 (defn open-dbs!
   []
@@ -177,9 +184,15 @@
   (str "(" (->> (map (fn [id] (str "'" id "'")) ids)
                 (string/join ", ")) ")"))
 
+(defn- get-or-open-db [repo]
+  (or (get-db repo)
+      (do
+        (open-db! repo)
+        (get-db repo))))
+
 (defn upsert-pages!
   [repo pages]
-  (if-let [db (get-db repo)]
+  (when-let [db (get-or-open-db repo)]
     ;; TODO: what if a CONFLICT on uuid
     ;; Should update all values on id conflict
     (let [insert (prepare db "INSERT INTO pages (id, uuid, content) VALUES (@id, @uuid, @content) ON CONFLICT (id) DO UPDATE SET (uuid, content) = (@uuid, @content)" repo)
@@ -187,10 +200,7 @@
                                     (fn [pages]
                                       (doseq [page pages]
                                         (.run ^object insert page))))]
-      (insert-many pages))
-    (do
-      (open-db! repo)
-      (upsert-pages! repo pages))))
+      (insert-many pages))))
 
 (defn delete-pages!
   [repo ids]
@@ -201,7 +211,7 @@
 
 (defn upsert-blocks!
   [repo blocks]
-  (if-let [db (get-db repo)]
+  (when-let [db (get-or-open-db repo)]
     ;; TODO: what if a CONFLICT on uuid
     ;; Should update all values on id conflict
     (let [insert (prepare db "INSERT INTO blocks (id, uuid, content, page) VALUES (@id, @uuid, @content, @page) ON CONFLICT (id) DO UPDATE SET (uuid, content, page) = (@uuid, @content, @page)" repo)
@@ -209,10 +219,7 @@
                                     (fn [blocks]
                                       (doseq [block blocks]
                                         (.run ^object insert block))))]
-      (insert-many blocks))
-    (do
-      (open-db! repo)
-      (upsert-blocks! repo blocks))))
+      (insert-many blocks))))
 
 (defn delete-blocks!
   [repo ids]

+ 3 - 4
src/electron/electron/utils.js

@@ -57,15 +57,14 @@ export async function getAllFiles(dir, exts) {
 
       const fileStats = await fse.lstat(filePath)
 
-      const stats = {
+      return {
+        path: filePath,
         size: fileStats.size,
         accessTime: fileStats.atimeMs,
         modifiedTime: fileStats.mtimeMs,
         changeTime: fileStats.ctimeMs,
-        birthTime: fileStats.birthtimeMs,
+        birthTime: fileStats.birthtimeMs
       }
-
-      return { path: filePath, ...stats }
     })
   )
   return files.flat().filter((it) => it != null)

+ 24 - 16
src/electron/electron/window.cljs

@@ -76,25 +76,37 @@
      ;;(when dev? (.. win -webContents (openDevTools)))
      win)))
 
+(defn get-all-windows
+  []
+  (.getAllWindows BrowserWindow))
+
 (defn destroy-window!
   [^js win]
   (.destroy win))
 
+(defn close-handler
+  [^js win close-watcher-f e]
+  (.preventDefault e)
+  (when-let [dir (state/get-window-graph-path win)]
+    (close-watcher-f win dir))
+  (state/close-window! win)
+  (let [web-contents (. win -webContents)]
+    (.send web-contents "persist-zoom-level" (.getZoomLevel web-contents))
+    (.send web-contents "persistent-dbs"))
+  (async/go
+    (let [_ (async/<! state/persistent-dbs-chan)]
+      (destroy-window! win)
+      ;; (if @*quitting?
+      ;;   (doseq [win (get-all-windows)]
+      ;;     (destroy-window! win))
+      ;;   (destroy-window! win))
+      (when @*quitting?
+        (async/put! state/persistent-dbs-chan true)))))
+
 (defn on-close-actions!
   ;; TODO merge with the on close in core
   [^js win close-watcher-f] ;; injected watcher related func
-  (.on win "close" (fn [e]
-                     (.preventDefault e)
-                     (when-let [dir (state/get-window-graph-path win)]
-                       (close-watcher-f win dir))
-                     (state/close-window! win)
-                     (let [web-contents (. win -webContents)]
-                       (.send web-contents "persistent-dbs"))
-                     (async/go
-                       (let [_ (async/<! state/persistent-dbs-chan)]
-                         (destroy-window! win)
-                         (when @*quitting?
-                           (async/put! state/persistent-dbs-chan true)))))))
+  (.on win "close" (fn [e] (close-handler win close-watcher-f e))))
 
 (defn switch-to-window!
   [^js win]
@@ -102,10 +114,6 @@
     (.restore win))
   (.focus win))
 
-(defn get-all-windows
-  []
-  (.getAllWindows BrowserWindow))
-
 (defn get-graph-all-windows
   [graph-path] ;; graph-path == dir
   (->> (group-by second (:window/graph @state/state))

+ 29 - 30
src/main/frontend/extensions/pdf/assets.cljs

@@ -147,44 +147,43 @@
 
       (fs/unlink! repo-cur fpath {}))))
 
-(defn resolve-ref-page
+(defn ensure-ref-page!
   [pdf-current]
-  (let [page-name (:key pdf-current)
-        page-name (string/trim page-name)
-        page-name (str "hls__" page-name)
-        page      (db-model/get-page page-name)
-        file-path (:original-path pdf-current)
-        format    (state/get-preferred-format)
-        repo-dir  (config/get-repo-dir (state/get-current-repo))
-        asset-dir (util/node-path.join repo-dir gp-config/local-assets-dir)
-        url       (if (string/includes? file-path asset-dir)
-                    (str ".." (last (string/split file-path repo-dir)))
-                    file-path)]
-    (if-not page
-      (let [label (:filename pdf-current)]
-        (page-handler/create! page-name {:redirect?        false :create-first-block? false
-                                         :split-namespace? false
-                                         :format           format
-                                         ;; FIXME: file and file-path properties for db version
-                                         :properties       {:file      (case format
-                                                                         :markdown
-                                                                         (util/format "[%s](%s)" label url)
-
-                                                                         :org
-                                                                         (util/format "[[%s][%s]]" url label)
-
-                                                                         url)
-                                                            :file-path url}})
-        (db-model/get-page page-name))
+  (when-let [page-name (util/trim-safe (:key pdf-current))]
+    (let [page-name (str "hls__" page-name)
+          page (db-model/get-page page-name)
+          file-path (:original-path pdf-current)
+          format (state/get-preferred-format)
+          repo-dir (config/get-repo-dir (state/get-current-repo))
+          asset-dir (util/node-path.join repo-dir gp-config/local-assets-dir)
+          url (if (string/includes? file-path asset-dir)
+                (str ".." (last (string/split file-path repo-dir)))
+                file-path)]
+      (if-not page
+        (let [label (:filename pdf-current)]
+          (page-handler/create! page-name {:redirect?        false :create-first-block? false
+                                           :split-namespace? false
+                                           :format           format
+                                           ;; FIXME: file and file-path properties for db version
+                                           :properties       {:file      (case format
+                                                                           :markdown
+                                                                           (util/format "[%s](%s)" label url)
+
+                                                                           :org
+                                                                           (util/format "[[%s][%s]]" url label)
+
+                                                                           url)
+                                                              :file-path url}})
+          (db-model/get-page page-name))
 
       ;; try to update file path
       (property-handler/add-page-property! page-name :file-path url))
-    page))
+    page)))
 
 (defn ensure-ref-block!
   ([pdf hl] (ensure-ref-block! pdf hl nil))
   ([pdf-current {:keys [id content page properties]} insert-opts]
-   (when-let [ref-page (and pdf-current (resolve-ref-page pdf-current))]
+   (when-let [ref-page (and pdf-current (ensure-ref-page! pdf-current))]
      (let [ref-block (db-model/query-block-by-uuid id)]
        (if-not (nil? (:block/content ref-block))
          (do

+ 21 - 14
src/main/frontend/extensions/pdf/core.cljs

@@ -848,7 +848,6 @@
                        (confirm-fn password)))}
         "Submit"]]]]))
 
-
 (rum/defc ^:large-vars/data-var pdf-loader
   [{:keys [url hls-file identity filename] :as pdf-current}]
   (let [*doc-ref       (rum/use-ref nil)
@@ -861,6 +860,13 @@
         set-hls-extra! (fn [extra]
                          (set-hls-state! #(merge % {:extra extra})))]
 
+    ;; current pdf effects
+    (rum/use-effect!
+     (fn []
+       (when pdf-current
+         (pdf-assets/ensure-ref-page! pdf-current)))
+     [pdf-current])
+
     ;; load highlights
     (rum/use-effect!
      (fn []
@@ -888,22 +894,23 @@
     ;; cache highlights
     (let [persist-hls-data!
           (rum/use-callback
-            (util/debounce
-              4000 (fn [latest-hls extra]
-                    (pdf-assets/persist-hls-data$
-                      pdf-current latest-hls extra))) [pdf-current])]
+           (util/debounce
+            4000 (fn [latest-hls extra]
+                   (pdf-assets/persist-hls-data$
+                    pdf-current latest-hls extra))) [pdf-current])]
+
       (rum/use-effect!
-        (fn []
-          (when (= :completed (:status loader-state))
-            (p/catch
-              (when-not (:error hls-state)
-                (p/do! (persist-hls-data! (:latest-hls hls-state) (:extra hls-state))))
+       (fn []
+         (when (= :completed (:status loader-state))
+           (p/catch
+            (when-not (:error hls-state)
+              (p/do! (persist-hls-data! (:latest-hls hls-state) (:extra hls-state))))
 
-              ;; write hls file error
-              (fn [e]
-                (js/console.error "[write hls error]" e)))))
+            ;; write hls file error
+            (fn [e]
+              (js/console.error "[write hls error]" e)))))
 
-        [(:latest-hls hls-state) (:extra hls-state)]))
+       [(:latest-hls hls-state) (:extra hls-state)]))
 
     ;; load document
     (rum/use-effect!

+ 1 - 0
src/main/frontend/mobile/action_bar.cljs

@@ -1,4 +1,5 @@
 (ns frontend.mobile.action-bar
+  "Block Action bar, activated when swipe on a block"
   (:require
    [frontend.db :as db]
    [frontend.extensions.srs :as srs]

+ 9 - 1
src/main/frontend/mobile/core.cljs

@@ -82,8 +82,16 @@
                                   (state/get-left-sidebar-open?)
                                   (state/set-left-sidebar-open! false)
 
-                                  :else true))
+                                  (state/action-bar-open?)
+                                  (state/set-state! :mobile/show-action-bar? false)
+
+                                  (not-empty (state/get-selection-blocks))
+                                  (editor-handler/clear-selection!)
 
+                                  (state/editing?)
+                                  (editor-handler/escape-editing)
+
+                                  :else true))
                      (if (or (string/ends-with? href "#/")
                              (string/ends-with? href "/")
                              (not (string/includes? href "#/")))

+ 1 - 0
src/main/frontend/mobile/index.css

@@ -35,6 +35,7 @@
   border-radius: 10px;
   background-color: var(--ls-secondary-background-color);
   overflow-x: overlay;
+  overflow-y: hidden;
   box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 1px 0px, rgba(27, 31, 35, 0.10) 0px 0px 0px 1px;
   z-index: 100;
 

+ 2 - 2
src/main/frontend/modules/shortcut/config.cljs

@@ -702,7 +702,8 @@
            :sidebar/clear
            :command/run
            :command-palette/toggle
-           :editor/add-property])
+           :editor/add-property
+           :window/close])
         (with-meta {:before m/prevent-default-behavior}))
 
     :shortcut.handler/global-non-editing-only
@@ -729,7 +730,6 @@
            :editor/open-file-in-directory
            :editor/copy-current-file
            :editor/copy-page-url
-           :window/close
            :editor/new-whiteboard
            :ui/toggle-wide-mode
            :ui/select-theme-color

+ 4 - 0
src/main/frontend/state.cljs

@@ -2008,6 +2008,10 @@ Similar to re-frame subscriptions"
             (when (or (util/mobile?) (mobile-util/native-platform?))
               (set-state! :mobile/show-action-bar? false))))))))
 
+(defn action-bar-open?
+  []
+  (:mobile/show-action-bar? @state))
+
 (defn get-git-auto-commit-enabled?
   []
   (false? (sub [:electron/user-cfgs :git/disable-auto-commit?])))

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns ^:no-doc frontend.version)
 
-(defonce version "0.9.18")
+(defonce version "0.9.19")

+ 4 - 0
src/resources/dicts/es.edn

@@ -270,6 +270,7 @@
  :command.whiteboard/zoom-out                       "Alejar"
  :command.whiteboard/zoom-to-fit                    "Zoom al dibujo"
  :command.whiteboard/zoom-to-selection              "Zoom para ajustar a la selección"
+ :command.window/close                              "Cerrar ventana"
  :content/click-to-edit                             "Clic para editar"
  :content/copy-block-emebed                         "Copiar bloque a incrustar (embed)"
  :content/copy-block-ref                            "Copiar referencia de bloque"
@@ -607,6 +608,8 @@
  :select.graph/prompt                               "Seleccione un grafo"
  :settings-page/alpha-features                      "Características Alfa"
  :settings-page/app-updated                         "Tu aplicación está actualizada 🎉"
+ :settings-page/auto-chmod                          "Cambiar automaticamente los permisos de archivo"
+ :settings-page/auto-chmod-desc                     "Desactivar el permitir la edición de múltiples usuarios con permisos otorgados por la membresía del grupo."
  :settings-page/auto-expand-block-refs              "Expandir referencias de bloque automáticamente al hacer un acercamiento"
  :settings-page/auto-expand-block-refs-tip          "Esta opción controla si expandir el bloque de referencias automáticamente al hacer un acercamiento."
  :settings-page/auto-updater                        "Auto actualizador"
@@ -782,6 +785,7 @@
  :whiteboard/stroke-type                            "Tipo de línea"
  :whiteboard/text                                   "Texto"
  :whiteboard/toggle-grid                            "Alternar cuadrícula"
+ :whiteboard/toggle-pen-mode                        "Alternar modo pluma"
  :whiteboard/triangle                               "Triángulo"
  :whiteboard/twitter-url                            "url de Twitter"
  :whiteboard/undo                                   "Deshacer"