Browse Source

Merge branch 'master' into whiteboards

Peng Xiao 3 years ago
parent
commit
7038e6cbe6
60 changed files with 935 additions and 596 deletions
  1. 7 1
      .clj-kondo/config.edn
  2. 4 2
      bb.edn
  3. 5 5
      deps.edn
  4. 3 4
      e2e-tests/code-editing.spec.ts
  5. 3 33
      ios/App/App/AppDelegate.swift
  6. 1 1
      ios/App/ShareViewController/ShareViewController.swift
  7. 112 51
      libs/src/LSPlugin.core.ts
  8. 34 19
      libs/src/LSPlugin.ts
  9. 4 3
      libs/src/LSPlugin.user.ts
  10. 5 10
      libs/src/helpers.ts
  11. 2 2
      package.json
  12. 0 0
      resources/js/lsplugin.core.js
  13. 1 1
      resources/package.json
  14. 17 0
      scripts/src/logseq/tasks/dev.clj
  15. 51 0
      src/electron/electron/backup_file.cljs
  16. 8 34
      src/electron/electron/handler.cljs
  17. 8 14
      src/main/frontend/components/block.cljs
  18. 2 2
      src/main/frontend/components/content.cljs
  19. 4 4
      src/main/frontend/components/page.cljs
  20. 4 3
      src/main/frontend/components/page_menu.cljs
  21. 72 47
      src/main/frontend/components/plugins.cljs
  22. 3 8
      src/main/frontend/components/plugins.css
  23. 3 5
      src/main/frontend/components/reference.cljs
  24. 23 1
      src/main/frontend/components/sidebar.cljs
  25. 1 0
      src/main/frontend/components/theme.cljs
  26. 1 1
      src/main/frontend/config.cljs
  27. 2 3
      src/main/frontend/db/debug.cljs
  28. 4 5
      src/main/frontend/db/model.cljs
  29. 2 2
      src/main/frontend/db/query_dsl.cljs
  30. 2 2
      src/main/frontend/db/query_react.cljs
  31. 1 2
      src/main/frontend/db/utils.cljs
  32. 1 1
      src/main/frontend/diff.cljs
  33. 3 4
      src/main/frontend/extensions/slide.cljs
  34. 2 2
      src/main/frontend/extensions/video/youtube.cljs
  35. 2 3
      src/main/frontend/external/roam.cljs
  36. 105 48
      src/main/frontend/fs/sync.cljs
  37. 14 25
      src/main/frontend/handler/editor.cljs
  38. 97 14
      src/main/frontend/handler/file_sync.cljs
  39. 16 8
      src/main/frontend/handler/plugin.cljs
  40. 1 2
      src/main/frontend/handler/route.cljs
  41. 79 62
      src/main/frontend/mobile/core.cljs
  42. 81 0
      src/main/frontend/mobile/deeplink.cljs
  43. 17 12
      src/main/frontend/mobile/intent.cljs
  44. 5 5
      src/main/frontend/modules/outliner/datascript.cljc
  45. 2 3
      src/main/frontend/modules/outliner/tree.cljs
  46. 4 3
      src/main/frontend/modules/shortcut/config.cljs
  47. 2 3
      src/main/frontend/modules/shortcut/core.cljs
  48. 14 11
      src/main/frontend/modules/shortcut/dicts.cljc
  49. 44 18
      src/main/frontend/state.cljs
  50. 9 1
      src/main/frontend/ui.cljs
  51. 8 26
      src/main/frontend/util.cljc
  52. 2 3
      src/main/frontend/util/list.cljs
  53. 2 3
      src/main/frontend/util/property.cljs
  54. 12 14
      src/main/logseq/api.cljs
  55. 10 14
      src/main/logseq/graph_parser/block.cljc
  56. 1 1
      src/main/logseq/graph_parser/date_time_util.cljs
  57. 1 1
      src/main/logseq/graph_parser/text.cljs
  58. 0 31
      src/main/logseq/graph_parser/util.cljs
  59. 8 9
      templates/tutorial-ja.md
  60. 4 4
      yarn.lock

+ 7 - 1
.clj-kondo/config.edn

@@ -2,7 +2,13 @@
  {:unresolved-symbol {:exclude [goog.DEBUG
                                 goog.string.unescapeEntities
                                 ;; TODO:lint: Fix when fixing all type hints
-                                object]}
+                                object
+                                ;; TODO: Remove parse-* and update-* when https://github.com/clj-kondo/clj-kondo/issues/1694 is done
+                                parse-long
+                                parse-double
+                                parse-uuid
+                                update-keys
+                                update-vals]}
   ;; TODO:lint: Remove node-path excludes once we have a cleaner api
   :unresolved-var {:exclude [frontend.util/node-path.basename
                              frontend.util/node-path.dirname

+ 4 - 2
bb.edn

@@ -2,8 +2,7 @@
  :deps
  {org.babashka/spec.alpha
   {:git/url "https://github.com/babashka/spec.alpha"
-   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}
-  medley/medley {:mvn/version "1.3.0"}}
+   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}}
  :pods
  {clj-kondo/clj-kondo {:version "2022.02.09"}}
  :tasks
@@ -24,6 +23,9 @@
   dev:validate-local-storage
   logseq.tasks.spec/validate-local-storage
 
+  dev:lint
+  logseq.tasks.dev/lint
+
   test:load-nbb-compatible-namespaces
   logseq.tasks.nbb/load-compatible-namespaces
 

+ 5 - 5
deps.edn

@@ -7,7 +7,7 @@
   borkdude/rewrite-edn                  {:git/url "https://github.com/borkdude/rewrite-edn"
                                          :sha     "edd87dc7f045f28d7afcbfc44bc0f0a2683dde62"}
   funcool/promesa                       {:mvn/version "4.0.2"}
-  medley/medley                         {:mvn/version "1.2.0"}
+  medley/medley                         {:mvn/version "1.4.0"}
   metosin/reitit-frontend               {:mvn/version "0.3.10"}
   cljs-bean/cljs-bean                   {:mvn/version "1.5.0"}
   prismatic/dommy                       {:mvn/version "1.1.0"}
@@ -20,9 +20,9 @@
   hickory/hickory                       {:git/url "https://github.com/logseq/hickory"
                                          :sha     "9c2c2f1fc2c45efaad906e0faabc3201278deeaa"}
   hiccups/hiccups                       {:mvn/version "0.3.0"}
-  tongue/tongue                         {:mvn/version "0.2.9"}
+  tongue/tongue                         {:mvn/version "0.4.4"}
   org.clojure/core.async                {:mvn/version "1.3.610"}
-  thheller/shadow-cljs                  {:mvn/version "2.17.5"}
+  thheller/shadow-cljs                  {:mvn/version "2.19.0"}
   expound/expound                       {:mvn/version "0.8.6"}
   com.lambdaisland/glogi                {:mvn/version "1.1.144"}
   binaryage/devtools                    {:mvn/version "1.0.5"}
@@ -33,14 +33,14 @@
   org.clojars.mmb90/cljs-cache          {:mvn/version "0.1.4"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.10.891"}
+                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                                 org.clojure/tools.namespace      {:mvn/version "0.2.11"}
                                 cider/cider-nrepl                {:mvn/version "0.26.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            :test {:extra-paths ["src/test/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.10.891"}
+                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                                 org.clojure/test.check           {:mvn/version "1.1.1"}
                                 pjstadig/humane-test-output      {:mvn/version "0.11.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}

+ 3 - 4
e2e-tests/code-editing.spec.ts

@@ -164,7 +164,7 @@ test('multiple code block', async ({ page }) => {
   await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
 
   await page.waitForTimeout(500)
-  await page.click('.CodeMirror pre >> nth=1')
+  await page.click('.CodeMirror >> nth=1 >> pre')
   await page.waitForTimeout(500)
 
   await page.type('.CodeMirror textarea >> nth=1', '\n  :key-test 日本語\n', { strict: true })
@@ -191,11 +191,10 @@ test('click outside to exit', async ({ page }) => {
   expect(await page.inputValue('.block-editor textarea')).toBe('Header ``Click``\n```\n  ABC  DEF\n  GHI\n```')
 })
 
-test('click language label to exit #3463', async ({ page }) => {
+test('click language label to exit #3463', async ({ page, block }) => {
   await createRandomPage(page)
 
-  await page.press('.block-editor textarea', 'Enter')
-  await page.waitForTimeout(200)
+  await block.enterNext();
 
   await page.fill('.block-editor textarea', '```cpp\n```')
   await page.waitForTimeout(200)

+ 3 - 33
ios/App/App/AppDelegate.swift

@@ -1,19 +1,14 @@
 import UIKit
 import Capacitor
-import SendIntent
 
 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate {
 
     var window: UIWindow?
-    let store = ShareStore.store
     
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
-        DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
-            NotificationCenter.default
-                .post(name: Notification.Name("triggerSendIntent"), object: nil )
-        }
+        
         return true
     }
     
@@ -33,6 +28,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
     func applicationDidBecomeActive(_ application: UIApplication) {
         // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+        
+        
     }
 
     func applicationWillTerminate(_ application: UIApplication) {
@@ -45,33 +42,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
             if CAPBridge.handleOpenUrl(url, options) {
                 success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
             }
-            
-            guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
-                  let params = components.queryItems else {
-                      return false
-                  }
-            let titles = params.filter { $0.name == "title" }
-            let descriptions = params.filter { $0.name == "description" }
-            let types = params.filter { $0.name == "type" }
-            let urls = params.filter { $0.name == "url" }
-            
-            store.shareItems.removeAll()
-        
-            if(titles.count > 0){
-                for index in 0...titles.count-1 {
-                    var shareItem: JSObject = JSObject()
-                    shareItem["title"] = titles[index].value!
-                    shareItem["description"] = descriptions[index].value!
-                    shareItem["type"] = types[index].value!
-                    shareItem["url"] = urls[index].value!
-                    store.shareItems.append(shareItem)
-                }
-            }
-            
-            store.processed = false
-            
-            NotificationCenter.default.post(name: Notification.Name("triggerSendIntent"), object: nil )
-            
             return success
         }
 

+ 1 - 1
ios/App/ShareViewController/ShareViewController.swift

@@ -44,7 +44,7 @@ class ShareViewController: UIViewController {
                     value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
             ]
         }.flatMap({ $0 })
-        var urlComps = URLComponents(string: "logseq://")!
+        var urlComps = URLComponents(string: "logseq://shared?")!
         urlComps.queryItems = queryItems
         openURL(urlComps.url!)
     }

+ 112 - 51
libs/src/LSPlugin.core.ts

@@ -3,7 +3,6 @@ import {
   deepMerge,
   setupInjectedStyle,
   genID,
-  setupInjectedTheme,
   setupInjectedUI,
   deferred,
   invokeHostExportedApi,
@@ -19,6 +18,7 @@ import {
   IS_DEV,
   cleanInjectedScripts,
   safeSnakeCase,
+  injectTheme,
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -34,11 +34,13 @@ import {
 } from './LSPlugin.caller'
 import {
   ILSPluginThemeManager,
+  LegacyTheme,
   LSPluginPkgConfig,
   SettingSchemaDesc,
   StyleOptions,
   StyleString,
-  ThemeOptions,
+  Theme,
+  ThemeMode,
   UIContainerAttrs,
   UIOptions,
 } from './LSPlugin'
@@ -173,10 +175,13 @@ class PluginLogger extends EventEmitter<'change'> {
 }
 
 interface UserPreferences {
-  theme: ThemeOptions
+  theme: LegacyTheme
+  themes: {
+    mode: ThemeMode
+    light: Theme
+    dark: Theme
+  }
   externals: string[] // external plugin locations
-
-  [key: string]: any
 }
 
 interface PluginLocalOptions {
@@ -310,7 +315,7 @@ function initProviderHandlers(pluginLocal: PluginLocal) {
   let themed = false
 
   // provider:theme
-  pluginLocal.on(_('theme'), (theme: ThemeOptions) => {
+  pluginLocal.on(_('theme'), (theme: Theme) => {
     pluginLocal.themeMgr.registerTheme(pluginLocal.id, theme)
 
     if (!themed) {
@@ -697,7 +702,7 @@ class PluginLocal extends EventEmitter<
     this._options.entry = entry
   }
 
-  async _loadConfigThemes(themes: ThemeOptions[]) {
+  async _loadConfigThemes(themes: Theme[]) {
     themes.forEach((options) => {
       if (!options.url) return
 
@@ -1123,6 +1128,7 @@ class LSPluginCore
     | 'unregistered'
     | 'theme-changed'
     | 'theme-selected'
+    | 'reset-custom-theme'
     | 'settings-changed'
     | 'unlink-plugin'
     | 'beforereload'
@@ -1133,19 +1139,24 @@ class LSPluginCore
   private _isRegistering = false
   private _readyIndicator?: DeferredActor
   private readonly _hostMountedActor: DeferredActor = deferred()
-  private readonly _userPreferences: Partial<UserPreferences> = {}
-  private readonly _registeredThemes = new Map<
-    PluginLocalIdentity,
-    ThemeOptions[]
-  >()
+  private readonly _userPreferences: UserPreferences = {
+    theme: null,
+    themes: {
+      mode: 'light',
+      light: null,
+      dark: null,
+    },
+    externals: [],
+  }
+  private readonly _registeredThemes = new Map<PluginLocalIdentity, Theme[]>()
   private readonly _registeredPlugins = new Map<
     PluginLocalIdentity,
     PluginLocal
   >()
   private _currentTheme: {
-    dis: () => void
     pid: PluginLocalIdentity
-    opt: ThemeOptions
+    opt: Theme | LegacyTheme
+    eject: () => void
   }
 
   /**
@@ -1182,12 +1193,25 @@ class LSPluginCore
     }
   }
 
+  /**
+   * Activate the user preferences.
+   *
+   * Steps:
+   *
+   * 1. Load the custom theme.
+   *
+   * @memberof LSPluginCore
+   */
   async activateUserPreferences() {
-    const { theme } = this._userPreferences
-
-    // 0. theme
-    if (theme) {
-      await this.selectTheme(theme, false)
+    const { theme: legacyTheme, themes } = this._userPreferences
+    const currentTheme = themes[themes.mode]
+
+    // If there is currently a theme that has been set
+    if (currentTheme) {
+      await this.selectTheme(currentTheme, { effect: false })
+    } else if (legacyTheme) {
+      // Otherwise compatible with older versions
+      await this.selectTheme(legacyTheme, { effect: false })
     }
   }
 
@@ -1238,7 +1262,7 @@ class LSPluginCore
 
       await this.loadUserPreferences()
 
-      const externals = new Set(this._userPreferences.externals || [])
+      const externals = new Set(this._userPreferences.externals)
 
       if (initial) {
         plugins = plugins.concat(
@@ -1349,8 +1373,8 @@ class LSPluginCore
       this.emit('unregistered', identity)
     }
 
-    const externals = this._userPreferences.externals || []
-    if (externals.length > 0 && unregisteredExternals.length > 0) {
+    const externals = this._userPreferences.externals
+    if (externals.length && unregisteredExternals.length) {
       await this.saveUserPreferences({
         externals: externals.filter((it) => {
           return !unregisteredExternals.includes(it)
@@ -1472,18 +1496,15 @@ class LSPluginCore
     return this._isRegistering
   }
 
-  get themes(): Map<PluginLocalIdentity, ThemeOptions[]> {
+  get themes() {
     return this._registeredThemes
   }
 
-  async registerTheme(
-    id: PluginLocalIdentity,
-    opt: ThemeOptions
-  ): Promise<void> {
-    debug('registered Theme #', id, opt)
+  async registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void> {
+    debug('Register theme #', id, opt)
 
     if (!id) return
-    let themes: ThemeOptions[] = this._registeredThemes.get(id)!
+    let themes: Theme[] = this._registeredThemes.get(id)!
     if (!themes) {
       this._registeredThemes.set(id, (themes = []))
     }
@@ -1492,41 +1513,81 @@ class LSPluginCore
     this.emit('theme-changed', this.themes, { id, ...opt })
   }
 
-  async selectTheme(opt?: ThemeOptions, effect = true): Promise<void> {
-    // clear current
+  async selectTheme(
+    theme: Theme | LegacyTheme,
+    options: {
+      effect?: boolean
+      emit?: boolean
+    } = {}
+  ) {
+    const { effect, emit } = Object.assign(
+      {},
+      { effect: true, emit: true },
+      options
+    )
+
+    // Clear current theme before injecting.
     if (this._currentTheme) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
     }
 
-    const disInjectedTheme = setupInjectedTheme(opt?.url)
-    this.emit('theme-selected', opt)
-    effect && (await this.saveUserPreferences({ theme: opt?.url ? opt : null }))
-    if (opt?.url) {
+    // Detect if it is the default theme (no url).
+    if (!theme.url) {
+      this._currentTheme = null
+    } else {
+      const ejectTheme = injectTheme(theme.url)
+
       this._currentTheme = {
-        dis: () => {
-          disInjectedTheme()
-          effect && this.saveUserPreferences({ theme: null })
-        },
-        opt,
-        pid: opt.pid,
+        pid: theme.pid,
+        opt: theme,
+        eject: ejectTheme,
       }
     }
+
+    if (effect) {
+      await this.saveUserPreferences(
+        theme.mode
+          ? {
+              themes: {
+                ...this._userPreferences.themes,
+                mode: theme.mode,
+                [theme.mode]: theme,
+              },
+            }
+          : { theme: theme }
+      )
+    }
+
+    if (emit) {
+      this.emit('theme-selected', theme)
+    }
   }
 
-  async unregisterTheme(
-    id: PluginLocalIdentity,
-    effect: boolean = true
-  ): Promise<void> {
-    debug('unregistered Theme #', id)
+  async unregisterTheme(id: PluginLocalIdentity, effect = true) {
+    debug('Unregister theme #', id)
+
+    if (!this._registeredThemes.has(id)) {
+      return
+    }
 
-    if (!this._registeredThemes.has(id)) return
     this._registeredThemes.delete(id)
     this.emit('theme-changed', this.themes, { id })
     if (effect && this._currentTheme?.pid === id) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
       this._currentTheme = null
-      // reset current theme
-      this.emit('theme-selected', null)
+
+      const { theme, themes } = this._userPreferences
+      await this.saveUserPreferences({
+        theme: theme?.pid === id ? null : theme,
+        themes: {
+          ...themes,
+          light: themes.light?.pid === id ? null : themes.light,
+          dark: themes.dark?.pid === id ? null : themes.dark,
+        },
+      })
+
+      // Reset current theme if it is unregistered
+      this.emit('reset-custom-theme', this._userPreferences.themes)
     }
   }
 }

+ 34 - 19
libs/src/LSPlugin.ts

@@ -1,18 +1,24 @@
-import EventEmitter from 'eventemitter3'
 import * as CSS from 'csstype'
+
+import EventEmitter from 'eventemitter3'
 import { LSPluginCaller } from './LSPlugin.caller'
-import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
+import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 
 export type PluginLocalIdentity = string
 
-export type ThemeOptions = {
+export type ThemeMode = 'light' | 'dark'
+
+export interface LegacyTheme {
   name: string
   url: string
   description?: string
-  mode?: 'dark' | 'light'
+  mode?: ThemeMode
+  pid: PluginLocalIdentity
+}
 
-  [key: string]: any
+export interface Theme extends LegacyTheme {
+  mode: ThemeMode
 }
 
 export type StyleString = string
@@ -64,7 +70,7 @@ export interface LSPluginPkgConfig {
   entry: string // alias of main
   title: string
   mode: 'shadow' | 'iframe'
-  themes: Array<ThemeOptions>
+  themes: Theme[]
   icon: string
 
   [key: string]: any
@@ -122,7 +128,7 @@ export interface AppInfo {
  * User's app configurations
  */
 export interface AppUserConfigs {
-  preferredThemeMode: 'dark' | 'light'
+  preferredThemeMode: ThemeMode
   preferredFormat: 'markdown' | 'org'
   preferredDateFormat: string
   preferredStartOfWeek: string
@@ -382,7 +388,7 @@ export interface IAppProxy {
     content: string,
     status?: 'success' | 'warning' | 'error' | string
   ) => void
-  
+
   setZoomFactor: (factor: number) => void
   setFullScreen: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
@@ -614,9 +620,17 @@ export interface IEditorProxy extends Record<string, any> {
 
   getAllPages: (repo?: string) => Promise<any>
 
-  prependBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  prependBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
-  appendBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  appendBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
   getPreviousSiblingBlock: (
     srcBlock: BlockIdentity
@@ -756,9 +770,7 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(
-    exts: string | string[]
-  ): Promise<{
+  listFilesOfCurrentGraph(exts: string | string[]): Promise<{
     path: string
     size: number
     accessTime: number
@@ -768,14 +780,17 @@ export interface IAssetsProxy {
   }>
 }
 
-export interface ILSPluginThemeManager extends EventEmitter {
-  themes: Map<PluginLocalIdentity, Array<ThemeOptions>>
+export interface ILSPluginThemeManager {
+  get themes(): Map<PluginLocalIdentity, Theme[]>
 
-  registerTheme(id: PluginLocalIdentity, opt: ThemeOptions): Promise<void>
+  registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void>
 
-  unregisterTheme(id: PluginLocalIdentity): Promise<void>
+  unregisterTheme(id: PluginLocalIdentity, effect?: boolean): Promise<void>
 
-  selectTheme(opt?: ThemeOptions): Promise<void>
+  selectTheme(
+    opt: Theme | LegacyTheme,
+    options: { effect?: boolean; emit?: boolean }
+  ): Promise<void>
 }
 
 export type LSPluginUserEvents = 'ui:visible:changed' | 'settings:changed'
@@ -837,7 +852,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
   /**
    * Set the theme for the main Logseq app
    */
-  provideTheme(theme: ThemeOptions): this
+  provideTheme(theme: Theme): this
 
   /**
    * Inject custom css for the main Logseq app

+ 4 - 3
libs/src/LSPlugin.user.ts

@@ -15,7 +15,7 @@ import {
   SlashCommandAction,
   BlockCommandCallback,
   StyleString,
-  ThemeOptions,
+  Theme,
   UIOptions,
   IHookEvent,
   BlockIdentity,
@@ -318,7 +318,8 @@ const KEY_MAIN_UI = 0
  */
 export class LSPluginUser
   extends EventEmitter<LSPluginUserEvents>
-  implements ILSPluginUser {
+  implements ILSPluginUser
+{
   // @ts-ignore
   private _version: string = LIB_VERSION
   private _debugTag: string = ''
@@ -436,7 +437,7 @@ export class LSPluginUser
     return this
   }
 
-  provideTheme(theme: ThemeOptions) {
+  provideTheme(theme: Theme) {
     this.caller.call('provider:theme', theme)
     return this
   }

+ 5 - 10
libs/src/helpers.ts

@@ -416,26 +416,21 @@ export function transformableEvent(target: HTMLElement, e: Event) {
   return obj
 }
 
-let injectedThemeEffect: any = null
-
-export function setupInjectedTheme(url?: string) {
-  injectedThemeEffect?.call()
-
-  if (!url) return
-
+export function injectTheme(url: string) {
   const link = document.createElement('link')
   link.rel = 'stylesheet'
   link.href = url
   document.head.appendChild(link)
 
-  return (injectedThemeEffect = () => {
+  const ejectTheme = () => {
     try {
       document.head.removeChild(link)
     } catch (e) {
       console.error(e)
     }
-    injectedThemeEffect = null
-  })
+  }
+
+  return ejectTheme
 }
 
 export function mergeSettingsWithSchema(

+ 2 - 2
package.json

@@ -5,7 +5,7 @@
     "main": "static/electron.js",
     "devDependencies": {
         "@capacitor/cli": "3.2.2",
-        "@logseq/nbb-logseq": "^0.3.99",
+        "@logseq/nbb-logseq": "^0.5.103",
         "@playwright/test": "^1.19.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
@@ -53,7 +53,7 @@
         "cljs:app-watch": "clojure -M:cljs watch app",
         "cljs:electron-watch": "clojure -M:cljs watch app electron --config-merge '{:asset-path \"./js\"}'",
         "cljs:release": "clojure -M:cljs release app publishing electron",
-        "cljs:release-electron": "clojure -M:cljs release app publishing electron --debug",
+        "cljs:release-electron": "clojure -M:cljs release app electron --debug && clojure -M:cljs release publishing",
         "cljs:release-app": "clojure -M:cljs release app",
         "cljs:test": "clojure -M:test compile test",
         "cljs:run-test": "node static/tests.js",

File diff suppressed because it is too large
+ 0 - 0
resources/js/lsplugin.core.js


+ 1 - 1
resources/package.json

@@ -36,7 +36,7 @@
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
-    "@logseq/rsapi": "0.0.11",
+    "@logseq/rsapi": "0.0.14",
     "electron-deeplink": "1.0.10"
   },
   "devDependencies": {

+ 17 - 0
scripts/src/logseq/tasks/dev.clj

@@ -24,3 +24,20 @@
                (shell "yarn dev-electron-app")
                (println "Waiting for app to build..."))
              (Thread/sleep 1000))))
+
+
+(defn lint
+  "Run all lint tasks
+  - clj-kondo lint
+  - carve lint for unused vars
+  - lint for vars that are too large
+  - lint invalid translation entries
+  - Lint datalog rules"
+  []
+  (doseq [cmd ["clojure -M:clj-kondo --parallel --lint src"
+               "scripts/carve.clj"
+               "scripts/large_vars.clj"
+               "bb lang:invalid-translations"
+               "scripts/lint_rules.clj"]]
+    (println cmd)
+    (shell cmd)))

+ 51 - 0
src/electron/electron/backup_file.cljs

@@ -0,0 +1,51 @@
+(ns electron.backup-file
+  (:require [clojure.string :as string]
+            ["path" :as path]
+            ["fs" :as fs]
+            ["fs-extra" :as fs-extra]))
+
+(def backup-dir "logseq/bak")
+(def version-file-dir "version-files/local")
+
+(defn- get-backup-dir*
+  [repo relative-path bak-dir]
+  (let [relative-path* (string/replace relative-path repo "")
+        bak-dir (path/join repo bak-dir)
+        path (path/join bak-dir relative-path*)
+        parsed-path (path/parse path)]
+    (path/join (.-dir parsed-path)
+               (.-name parsed-path))))
+
+(defn get-backup-dir
+  [repo relative-path]
+  (get-backup-dir* repo relative-path backup-dir))
+
+(defn get-version-file-dir
+  [repo relative-path]
+  (get-backup-dir* repo relative-path version-file-dir))
+
+(defn- truncate-old-versioned-files!
+  "reserve the latest 3 version files"
+  [dir]
+  (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
+        files (mapv #(.-name %) files)
+        old-versioned-files (drop 3 (reverse (sort files)))]
+    (doseq [file old-versioned-files]
+      (fs-extra/removeSync (path/join dir file)))))
+
+(defn backup-file
+  "backup CONTENT under DIR :backup-dir or :version-file-dir
+  :backup-dir = `backup-dir`
+  :version-file-dir = `version-file-dir`"
+  [repo dir relative-path ext content]
+  {:pre [(contains? #{:backup-dir :version-file-dir} dir)]}
+  (let [dir* (case dir
+               :backup-dir (get-backup-dir repo relative-path)
+               :version-file-dir (get-version-file-dir repo relative-path))
+        new-path (path/join dir*
+                            (str (string/replace (.toISOString (js/Date.)) ":" "_")
+                                 ext))]
+    (fs-extra/ensureDirSync dir*)
+    (fs/writeFileSync new-path content)
+    (fs/statSync new-path)
+    (truncate-old-versioned-files! dir*)))

+ 8 - 34
src/electron/electron/handler.cljs

@@ -19,7 +19,8 @@
             [electron.git :as git]
             [electron.plugin :as plugin]
             [electron.window :as win]
-            [electron.file-sync-rsapi :as rsapi]))
+            [electron.file-sync-rsapi :as rsapi]
+            [electron.backup-file :as backup-file]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -65,45 +66,18 @@
   (let [result (.diff_main Diff old new)]
     (some (fn [a] (= -1 (first a))) result)))
 
-(defn- truncate-old-versioned-files!
-  [dir]
-  (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
-        files (map #(.-name %) files)
-        old-versioned-files (drop 3 (reverse (sort files)))]
-    (doseq [file old-versioned-files]
-      (fs-extra/removeSync (path/join dir file)))))
-
-(defn- get-backup-dir
-  [repo path]
-  (let [path (string/replace path repo "")
-        bak-dir (str repo "/logseq/bak")
-        path (str bak-dir path)
-        parsed-path (path/parse path)]
-    (path/join (.-dir parsed-path)
-               (.-name parsed-path))))
-
-(defn backup-file
-  [repo path content]
-  (let [path-dir (get-backup-dir repo path)
-        ext (path/extname path)
-        new-path (path/join path-dir
-                            (str (string/replace (.toISOString (js/Date.)) ":" "_")
-                                 ext))]
-    (fs-extra/ensureDirSync path-dir)
-    (fs/writeFileSync new-path content)
-    (fs/statSync new-path)
-    (truncate-old-versioned-files! path-dir)
-    new-path))
-
 (defmethod handle :backupDbFile [_window [_ repo path db-content new-content]]
   (when (and (string? db-content)
              (string? new-content)
              (string-some-deleted? db-content new-content))
-    (backup-file repo path db-content)))
+    (backup-file/backup-file repo :backup-dir path (path/extname path) db-content)))
+
+(defmethod handle :addVersionFile [_window [_ repo path content]]
+  (backup-file/backup-file repo :version-file-dir path (path/extname path) content))
 
 (defmethod handle :openFileBackupDir [_window [_ repo path]]
   (when (string? path)
-    (let [dir (get-backup-dir repo path)]
+    (let [dir (backup-file/get-backup-dir repo path)]
       (.openPath shell dir))))
 
 (defmethod handle :readFile [_window [_ path]]
@@ -129,7 +103,7 @@
       (fs/statSync path)
       (catch :default e
         (let [backup-path (try
-                            (backup-file repo path content)
+                            (backup-file/backup-file repo :backup-dir path (path/extname path) content)
                             (catch :default e
                               (println "Backup file failed")
                               (js/console.dir e)))]

+ 8 - 14
src/main/frontend/components/block.cljs

@@ -710,11 +710,8 @@
 (rum/defc block-reference < rum/reactive
   db-mixins/query
   [config id label]
-  (when (and
-         (not (string/blank? id))
-         (util/uuid-string? id))
-    (let [block-id (uuid id)
-          block (db/pull-block block-id)
+  (when-let [block-id (parse-uuid id)]
+    (let [block (db/pull-block block-id)
           block-type (keyword (get-in block [:block/properties :ls-type]))
           hl-type (get-in block [:block/properties :hl-type])
           repo (state/get-current-repo)]
@@ -877,9 +874,6 @@
       (contains? config/audio-formats ext)
       (audio-link config url s label metadata full_text)
 
-      (contains? (config/doc-formats) ext)
-      (asset-link config label-text s metadata full_text)
-
       (= ext :pdf)
       (cond
         (util/electron?)
@@ -893,6 +887,9 @@
         (mobile-util/is-native-platform?)
         (asset-link config label-text s metadata full_text))
 
+      (contains? (config/doc-formats) ext)
+      (asset-link config label-text s metadata full_text)
+
       (not (contains? #{:mp4 :webm :mov} ext))
       (image-link config url s label metadata full_text)
 
@@ -1107,10 +1104,7 @@
       (when-let [s (-> (string/replace a "((" "")
                        (string/replace "))" "")
                        string/trim)]
-        (when-let [id (and s
-                           (let [s (string/trim s)]
-                             (and (util/uuid-string? s)
-                                  (uuid s))))]
+        (when-let [id (some-> s string/trim parse-uuid)]
           (block-embed (assoc config :link-depth (inc link-depth)) id)))
 
       :else                         ;TODO: maybe collections?
@@ -2820,7 +2814,7 @@
         :else
         (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)]
           (if (:slide? config)
-            (highlight/highlight (str (medley/random-uuid))
+            (highlight/highlight (str (random-uuid))
                                  {:class (str "language-" language)
                                   :data-lang language}
                                  code)
@@ -3068,7 +3062,7 @@
                              (when (> (- (util/time-ms) (:start-time config)) 100)
                                (load-more-blocks! config flat-blocks)))
             has-more? (and
-                       (> (count flat-blocks) model/initial-blocks-length)
+                       (>= (count flat-blocks) model/initial-blocks-length)
                        (some? (model/get-next-open-block (db/get-db) (last flat-blocks) db-id)))
             dom-id (str "lazy-blocks-" (::id state))]
         [:div {:id dom-id}

+ 2 - 2
src/main/frontend/components/content.cljs

@@ -363,13 +363,13 @@
                            e
                            (custom-context-menu-content))
 
-                          (and block-id (util/uuid-string? block-id))
+                          (and block-id (parse-uuid block-id))
                           (let [block (.closest target ".ls-block")]
                             (when block
                               (util/select-highlight! [block]))
                             (common-handler/show-custom-context-menu!
                             e
-                            (block-context-menu-content target (cljs.core/uuid block-id))))
+                            (block-context-menu-content target (uuid block-id))))
 
                           :else
                           nil))))))

+ 4 - 4
src/main/frontend/components/page.cljs

@@ -128,8 +128,8 @@
   (when page-e
     (let [page-name (or (:block/name page-e)
                         (str (:block/uuid page-e)))
-          block? (util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
+          block? (boolean block-id)
           page-blocks (get-blocks repo page-name block-id)]
       (if (empty? page-blocks)
         (dummy-block page-name)
@@ -324,8 +324,8 @@
     (let [current-repo (state/sub :git/current-repo)
           repo (or repo current-repo)
           page-name (util/page-name-sanity-lc path-page-name)
-          block? (util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
+          block? (boolean block-id)
           format (let [page (if block-id
                               (:block/name (:block/page (db/entity [:block/uuid block-id])))
                               page-name)]

+ 4 - 3
src/main/frontend/components/page_menu.cljs

@@ -102,10 +102,11 @@
              {:title   (t :page/open-with-default-app)
               :options {:on-click #(js/window.apis.openPath file-path)}}])
 
-          (when (util/electron?)
+          (when (or (util/electron?)
+                    (mobile-util/is-native-platform?))
             {:title   (t :page/copy-page-url)
-              :options {:on-click #(util/copy-to-clipboard!
-                                    (url-util/get-logseq-graph-page-url nil repo page-original-name))}})
+             :options {:on-click #(util/copy-to-clipboard!
+                                   (url-util/get-logseq-graph-page-url nil repo page-original-name))}})
 
           (when-not contents?
             {:title   (t :page/delete)

+ 72 - 47
src/main/frontend/components/plugins.cljs

@@ -17,62 +17,87 @@
             [clojure.string :as string]))
 
 (rum/defcs installed-themes
-  < rum/reactive
-    (rum/local 0 ::cursor)
-    (rum/local 0 ::total)
-    (mixins/event-mixin
-      (fn [state]
-        (let [*cursor (::cursor state)
-              *total (::total state)
-              ^js target (rum/dom-node state)]
-          (.focus target)
-          (mixins/on-key-down
-            state {38                                       ;; up
-                   (fn [^js _e]
-                     (reset! *cursor
-                             (if (zero? @*cursor)
-                               (dec @*total) (dec @*cursor))))
-                   40                                       ;; down
-                   (fn [^js _e]
-                     (reset! *cursor
-                             (if (= @*cursor (dec @*total))
-                               0 (inc @*cursor))))
-
-                   13                                       ;; enter
-                   #(when-let [^js active (.querySelector target ".is-active")]
-                      (.click active))
-                   }))))
+  <
+  (rum/local [] ::themes)
+  (rum/local 0 ::cursor)
+  (rum/local 0 ::total)
+  {:did-mount (fn [state] (let [*themes        (::themes state)
+                                *cursor        (::cursor state)
+                                *total         (::total state)
+                                mode           (state/sub :ui/theme)
+                                all-themes     (state/sub :plugin/installed-themes)
+                                themes         (->> all-themes
+                                                    (filter #(= (:mode %) mode))
+                                                    (sort-by #(:name %)))
+                                no-mode-themes (->> all-themes
+                                                    (filter #(= (:mode %) nil))
+                                                    (sort-by #(:name %))
+                                                    (map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) "light & dark themes" nil)))))
+                                selected       (state/sub :plugin/selected-theme)
+                                themes         (map-indexed (fn [idx opt]
+                                                              (let [selected? (= (:url opt) selected)]
+                                                                (when selected? (reset! *cursor (+ idx 1)))
+                                                                (assoc opt :mode mode :selected selected?))) (concat themes no-mode-themes))
+                                themes         (cons {:name        (string/join " " ["Default" (string/capitalize mode) "Theme"])
+                                                      :url         nil
+                                                      :description (string/join " " ["Logseq default" mode "theme."])
+                                                      :mode        mode
+                                                      :selected    (nil? selected)
+                                                      :group-first true
+                                                      :group-desc  (str mode " themes")} themes)]
+                            (reset! *themes themes)
+                            (reset! *total (count themes))
+                            state))}
+  (mixins/event-mixin
+   (fn [state]
+     (let [*cursor    (::cursor state)
+           *total     (::total state)
+           ^js target (rum/dom-node state)]
+       (.focus target)
+       (mixins/on-key-down
+        state {38                                       ;; up
+               (fn [^js _e]
+                 (reset! *cursor
+                         (if (zero? @*cursor)
+                           (dec @*total) (dec @*cursor))))
+               40                                       ;; down
+               (fn [^js _e]
+                 (reset! *cursor
+                         (if (= @*cursor (dec @*total))
+                           0 (inc @*cursor))))
+
+               13                                       ;; enter
+               #(when-let [^js active (.querySelector target ".is-active")]
+                  (.click active))}))))
   [state]
   (let [*cursor (::cursor state)
-        *total (::total state)
-        themes (state/sub :plugin/installed-themes)
-        selected (state/sub :plugin/selected-theme)
-        themes (cons {:name "Default Theme" :url nil :description "Logseq default light/dark theme."} themes)
-        themes (sort #(:selected %) (map #(assoc % :selected (= (:url %) selected)) themes))
-        _ (reset! *total (count themes))]
-
+        *themes (::themes state)]
     [:div.cp__themes-installed
      {:tab-index -1}
      [:h1.mb-4.text-2xl.p-1 (t :themes)]
      (map-indexed
-       (fn [idx opt]
-         (let [current-selected (:selected opt)
-               plg (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
+      (fn [idx opt]
+        (let [current-selected? (:selected opt)
+              group-first?      (:group-first opt)
+              plg               (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
+          [:div
+           (when (and group-first? (not= idx 0)) [:hr.my-2])
            [:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
             {:key      (str idx (:url opt))
-             :title    (when current-selected "Cancel selected theme")
+             :title    (:description opt)
              :class    (util/classnames
-                         [{:is-selected current-selected
-                           :is-active   (= idx @*cursor)}])
-             :on-click #(do (js/LSPluginCore.selectTheme (if current-selected nil (clj->js opt)))
+                        [{:is-selected current-selected?
+                          :is-active   (= idx @*cursor)}])
+             :on-click #(do (js/LSPluginCore.selectTheme (bean/->js opt))
                             (state/close-modal!))}
-            [:section
-             [:strong.block
-              [:small.opacity-60 (str (or (:name plg) "Logseq") " • ")]
-              (:name opt)]]
-            [:small.flex-shrink-0.flex.items-center.opacity-10
-             (when current-selected (ui/icon "check"))]]))
-       themes)]))
+            [:div.flex.items-center.text-xs
+             [:div.opacity-60 (str (or (:name plg) "Logseq") " •")]
+             [:div.name.ml-1 (:name opt)]]
+            (when (or group-first? current-selected?)
+              [:div.flex.items-center
+               (when group-first? [:small.opacity-60 (:group-desc opt)])
+               (when current-selected? [:small.inline-flex.ml-1.opacity-60 (ui/icon "check")])])]]))
+      @*themes)]))
 
 (rum/defc unpacked-plugin-loader
   [unpacked-pkg-path]
@@ -708,7 +733,7 @@
         updates (state/all-available-coming-updates)]
 
     [:div.cp__plugins-waiting-updates
-     [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (util/safe-parse-int (count updates)))]
+     [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (count updates))]
 
      (if (seq updates)
        ;; lists

+ 3 - 8
src/main/frontend/components/plugins.css

@@ -590,7 +590,7 @@
     outline: none;
     padding: 1rem;
 
-    > .it {
+    .it {
       user-select: none;
       background-color: var(--ls-secondary-background-color);
       border: 1px solid transparent;
@@ -598,13 +598,8 @@
       cursor: pointer;
       opacity: .8;
 
-      > section {
-        line-height: 1.1em;
-
-        > strong {
-          font-size: 13px;
-          font-weight: 600;
-        }
+      .name {
+        font-weight: 600;
       }
 
       &.is-active {

+ 3 - 5
src/main/frontend/components/reference.cljs

@@ -12,7 +12,6 @@
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [medley.core :as medley]
             [rum.core :as rum]))
 
 (rum/defc filter-dialog-inner < rum/reactive
@@ -82,8 +81,7 @@
           default-collapsed? (>= (count refed-blocks-ids) threshold)
           filters-atom (get state ::filters)
           filter-state (rum/react filters-atom)
-          block? (util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
           page-name (string/lower-case page-name)
           journal? (date/valid-journal-title? (string/capitalize page-name))
           scheduled-or-deadlines (when (and journal?
@@ -142,8 +140,8 @@
                                   (db/get-block-referenced-blocks block-id)
                                   (db/get-page-referenced-blocks page-name))
                      filters (when (seq filter-state)
-                               (->> (group-by second filter-state)
-                                    (medley/map-vals #(map first %))))
+                               (-> (group-by second filter-state)
+                                   (update-vals #(map first %))))
                      filtered-ref-blocks (block-handler/filter-blocks repo ref-blocks filters true)
                      n-ref (apply +
                              (for [[_ rfs] filtered-ref-blocks]

+ 23 - 1
src/main/frontend/components/sidebar.cljs

@@ -348,6 +348,20 @@
                        (:current-parsing-file state))]]]]
     (ui/progress-bar-with-label width left-label (str finished "/" total))))
 
+(rum/defc file-sync-download-progress < rum/static
+  [state]
+  (let [finished (or (:finished state) 0)
+        total (:total state)
+        width (js/Math.round (* (.toFixed (/ finished total) 2) 100))
+        left-label [:div.flex.flex-row.font-bold
+                    "Downloading"
+                    [:div.hidden.md:flex.flex-row
+                     [:span.mr-1 ": "]
+                     [:ul
+                      (for [file (:downloading-files state)]
+                        [:li file])]]]]
+    (ui/progress-bar-with-label width left-label (str finished "/" total))))
+
 (rum/defc main-content < rum/reactive db-mixins/query
   {:init (fn [state]
            (when-not @sidebar-inited?
@@ -371,8 +385,16 @@
         loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
         journals-length (state/sub :journals-length)
         latest-journals (db/get-latest-journals (state/get-current-repo) journals-length)
-        graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
+        graph-parsing-state (state/sub [:graph/parsing-state current-repo])
+        graph-file-sync-download-init-state (state/sub [:file-sync/download-init-progress current-repo])]
     (cond
+      (or
+       (:downloading? graph-file-sync-download-init-state)
+       (not= (:total graph-file-sync-download-init-state) (:finished graph-file-sync-download-init-state)))
+      [:div.flex.items-center.justify-center.full-height-without-header
+       [:div.flex-1
+        (file-sync-download-progress graph-file-sync-download-init-state)]]
+
       (or
        (:graph-loading? graph-parsing-state)
        (not= (:total graph-parsing-state) (:finished graph-parsing-state)))

+ 1 - 0
src/main/frontend/components/theme.cljs

@@ -24,6 +24,7 @@
         (if (= theme "dark") ;; for tailwind dark mode
           (.add cls "dark")
           (.remove cls "dark"))
+        (ui/apply-custom-theme-effect! theme)
         (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
      [theme])
 

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

@@ -92,7 +92,7 @@
                                 (set))]
     (set/union
      config-formats
-     #{:doc :docx :xls :xlsx :ppt :pptx :one :epub})))
+     #{:doc :docx :xls :xlsx :ppt :pptx :one :pdf :epub})))
 
 (def audio-formats #{:mp3 :ogg :mpeg :wav :m4a :flac :wma :aac})
 

+ 2 - 3
src/main/frontend/db/debug.cljs

@@ -1,6 +1,5 @@
 (ns frontend.db.debug
-  (:require [medley.core :as medley]
-            [frontend.db.utils :as db-utils]
+  (:require [frontend.db.utils :as db-utils]
             [frontend.db :as db]
             [datascript.core :as d]
             [frontend.util :as util]))
@@ -8,7 +7,7 @@
 ;; shortcut for query a block with string ref
 (defn qb
   [string-id]
-  (db-utils/pull [:block/uuid (medley/uuid string-id)]))
+  (db-utils/pull [:block/uuid (uuid string-id)]))
 
 (defn check-left-id-conflicts
   []

+ 4 - 5
src/main/frontend/db/model.cljs

@@ -916,8 +916,8 @@
 
 (defn get-page
   [page-name]
-  (if (util/uuid-string? page-name)
-    (db-utils/entity [:block/uuid (uuid page-name)])
+  (if-let [id (parse-uuid page-name)]
+    (db-utils/entity [:block/uuid id])
     (db-utils/entity [:block/name (util/page-name-sanity-lc page-name)])))
 
 (defn get-redirect-page-name
@@ -1225,9 +1225,8 @@
 
 (defn get-referenced-blocks-ids
   [page-name-or-block-uuid]
-  (if (util/uuid-string? (str page-name-or-block-uuid))
-    (let [id (uuid page-name-or-block-uuid)]
-      (get-block-referenced-blocks-ids id))
+  (if-let [id (parse-uuid (str page-name-or-block-uuid))]
+    (get-block-referenced-blocks-ids id)
     (get-page-referenced-blocks-ids page-name-or-block-uuid)))
 
 (defn get-matched-blocks

+ 2 - 2
src/main/frontend/db/query_dsl.cljs

@@ -67,7 +67,7 @@
           (date/journal-title->int input)))
 
       :else
-      (let [duration (util/parse-int (subs input 0 (dec (count input))))
+      (let [duration (parse-long (subs input 0 (dec (count input))))
             kind (last input)
             tf (case kind
                  "y" t/years
@@ -99,7 +99,7 @@
           (date/journal-title->long input)))
 
       :else
-      (let [duration (util/parse-int (subs input 0 (dec (count input))))
+      (let [duration (parse-long (subs input 0 (dec (count input))))
             kind (last input)
             tf (case kind
                  "y" t/years

+ 2 - 2
src/main/frontend/db/query_react.cljs

@@ -33,12 +33,12 @@
     (and (keyword? input)
          (util/safe-re-find #"^\d+d(-before)?$" (name input)))
     (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
+          days (parse-long (subs input 0 (dec (count input))))]
       (date->int (t/minus (t/today) (t/days days))))
     (and (keyword? input)
          (util/safe-re-find #"^\d+d(-after)?$" (name input)))
     (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
+          days (parse-long (subs input 0 (dec (count input))))]
       (date->int (t/plus (t/today) (t/days days))))
 
     (and (string? input) (text/page-ref? input))

+ 1 - 2
src/main/frontend/db/utils.cljs

@@ -4,7 +4,6 @@
             [frontend.state :as state]
             [clojure.string :as string]
             [datascript.transit :as dt]
-            [frontend.util :as util]
             [frontend.date :as date]
             [frontend.db.conn :as conn]
             [frontend.config :as config]
@@ -46,7 +45,7 @@
 
 (defn date->int
   [date]
-  (util/parse-int
+  (parse-long
    (string/replace (date/ymd date) "/" "")))
 
 (defn entity

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

@@ -10,7 +10,7 @@
 
 (defn diff
   [s1 s2]
-  (-> ((gobj/get jsdiff "diffLines") s1 s2)
+  (-> ((gobj/get jsdiff "diffLines") s1 s2 (clj->js {"newlineIsToken" true}))
       bean/->clj))
 
 (def inline-special-chars

+ 3 - 4
src/main/frontend/extensions/slide.cljs

@@ -1,6 +1,5 @@
 (ns frontend.extensions.slide
   (:require [rum.core :as rum]
-            [medley.core :as medley]
             [cljs-bean.core :as bean]
             [frontend.loader :as loader]
             [frontend.ui :as ui]
@@ -20,11 +19,11 @@
   (let [properties (:block/properties block)]
     (if (seq properties)
       (merge m
-             (medley/map-keys
+             (update-keys
+              properties
               (fn [k]
                 (-> (str "data-" (name k))
-                    (string/replace "data-data-" "data-")))
-              properties))
+                    (string/replace "data-data-" "data-")))))
       m)))
 
 (defonce *loading? (atom false))

+ 2 - 2
src/main/frontend/extensions/video/youtube.cljs

@@ -123,9 +123,9 @@ Remember: You can paste a raw YouTube url as embedded video on mobile."
         reg-number #"^\d+$"
         timestamp (str timestamp)
         total-seconds (-> (re-matches reg-number timestamp)
-                          util/safe-parse-int)
+                          parse-long)
         [_ hours minutes seconds] (re-matches reg timestamp)
-        [hours minutes seconds] (map util/safe-parse-int [hours minutes seconds])]
+        [hours minutes seconds] (map parse-long [hours minutes seconds])]
     (cond
       total-seconds
       total-seconds

+ 2 - 3
src/main/frontend/external/roam.cljs

@@ -2,7 +2,6 @@
   (:require [cljs-bean.core :as bean]
             [frontend.external.protocol :as protocol]
             [frontend.date :as date]
-            [medley.core :as medley]
             [clojure.walk :as walk]
             [clojure.string :as string]
             [frontend.util :as util]
@@ -61,7 +60,7 @@
                     (set))]
       (reset! all-refed-uids uids)
       (doseq [uid uids]
-        (swap! uid->uuid assoc uid (medley/random-uuid))))))
+        (swap! uid->uuid assoc uid (random-uuid))))))
 
 (defn transform
   [text]
@@ -76,7 +75,7 @@
 (defn child->text
   [{:keys [uid string children]} level]
   (when-not (and (get @uid->uuid uid) uid)
-    (swap! uid->uuid assoc uid (medley/random-uuid)))
+    (swap! uid->uuid assoc uid (random-uuid)))
   (let [children-text (children->text children (inc level))
         level-pattern (str (apply str (repeat level "\t"))
                            (if (zero? level)

+ 105 - 48
src/main/frontend/fs/sync.cljs

@@ -18,6 +18,9 @@
             [frontend.util.persist-var :as persist-var]
             [frontend.handler.notification :as notification]
             [frontend.context.i18n :refer [t]]
+            [frontend.diff :as diff]
+            [frontend.db :as db]
+            [frontend.fs :as fs]
             [medley.core :refer [dedupe-by]]
             [rum.core :as rum]))
 
@@ -162,7 +165,7 @@
                                          (offer! remote-changes-chan data)))))))
 
 (defn ws-listen!
-  "return channal which output messages from server"
+  "return channel which output messages from server"
   [graph-uuid *ws]
   (let [remote-changes-chan (chan (async/sliding-buffer 1))]
     (ws-listen!* graph-uuid *ws remote-changes-chan)
@@ -350,7 +353,7 @@
    :TXType "update_files"
    :TXContent (string/join "/" [user-uuid graph-uuid relative-path])})
 
-(defn- filepaths->partitioned-filetxns
+(defn filepaths->partitioned-filetxns
   "transducer.
   1. filepaths -> diff
   2. diffs->partitioned-filetxns"
@@ -361,6 +364,7 @@
    (map-indexed filepath->diff)
    (diffs->partitioned-filetxns n)))
 
+
 (deftype FileMetadata [size etag path last-modified remote? ^:mutable normalized-path]
   Object
   (get-normalized-path [_]
@@ -387,7 +391,7 @@
   (-pr-writer [_ w _opts]
     (write-all w (str {:size size :etag etag :path path :remote? remote?}))))
 
-(defn- relative-path [o]
+(defn relative-path [o]
   (cond
     (implements? IRelativePath o)
     (-relative-path o)
@@ -721,23 +725,68 @@
 
 (def remoteapi (->RemoteAPI))
 
+(defn- add-new-version-file
+  [repo path content]
+  (go
+    (println "add-new-version-file: "
+             (<! (p->c (ipc/ipc "addVersionFile" (config/get-local-dir repo) path content))))))
+
+(defn- is-journals-or-pages?
+  [filetxn]
+  (let [rel-path (relative-path filetxn)]
+    (or (string/starts-with? rel-path "journals/")
+        (string/starts-with? rel-path "pages/"))))
+
+(defn- need-add-version-file?
+  "when we need to create a new version file:
+  1. when apply a 'update' filetxn, it already exists(same page name) locally and has delete diffs
+  2. when apply a 'delete' filetxn, its origin remote content and local content are different
+     - TODO: we need to store origin remote content md5 in server db
+  3. create version files only for files under 'journals/', 'pages/' dir"
+  [^FileTxn filetxn origin-db-content]
+  (go
+    (cond
+      (.renamed? filetxn)
+      false
+      (.-deleted? filetxn)
+      false
+      (.-updated? filetxn)
+      (let [path (relative-path filetxn)
+            repo (state/get-current-repo)
+            file-path (config/get-file-path repo path)
+            content (<! (p->c (fs/read-file "" file-path)))]
+        (and origin-db-content
+             (or (nil? content)
+                 (some :removed (diff/diff origin-db-content content))))))))
+
 (defn- apply-filetxns
   [graph-uuid base-path filetxns]
-  (cond
-    (.renamed? (first filetxns))
-    (let [filetxn (first filetxns)]
-      (assert (= 1 (count filetxns)))
-      (rename-local-file rsapi graph-uuid base-path
-                         (relative-path (.-from-path filetxn))
-                         (relative-path (.-to-path filetxn))))
-
-    (.-updated? (first filetxns))
-    (update-local-files rsapi graph-uuid base-path (map relative-path filetxns))
-
-    (.-deleted? (first filetxns))
-    (let [filetxn (first filetxns)]
-      (assert (= 1 (count filetxns)))
-      (go
+  (go
+    (cond
+      (.renamed? (first filetxns))
+      (let [^FileTxn filetxn (first filetxns)
+            from-path (.-from-path filetxn)
+            to-path (.-to-path filetxn)]
+        (assert (= 1 (count filetxns)))
+        (<! (rename-local-file rsapi graph-uuid base-path
+                               (relative-path from-path)
+                               (relative-path to-path))))
+
+      (.-updated? (first filetxns))
+      (let [repo (state/get-current-repo)
+            txn->db-content-vec (->> filetxns
+                                     (mapv
+                                      #(when (is-journals-or-pages? %)
+                                         [% (db/get-file repo (config/get-file-path repo (relative-path %)))]))
+                                     (remove nil?))]
+        (<! (update-local-files rsapi graph-uuid base-path (map relative-path filetxns)))
+        (doseq [[filetxn origin-db-content] txn->db-content-vec]
+          (when (need-add-version-file? filetxn origin-db-content)
+            (add-new-version-file repo (relative-path filetxn) origin-db-content))))
+
+      (.-deleted? (first filetxns))
+      (let [filetxn (first filetxns)]
+        (assert (= 1 (count filetxns)))
         (let [r (<! (delete-local-files rsapi graph-uuid base-path [(relative-path filetxn)]))]
           (if (and (instance? ExceptionInfo r)
                    (string/index-of (str (ex-cause r)) "No such file or directory"))
@@ -750,21 +799,23 @@
          sync-state--remove-current-remote->local-files
          sync-state--stopped?)
 
-(defn- apply-filetxns-partitions
+(defn apply-filetxns-partitions
   "won't call update-graph-txid! when *txid is nil"
-  [*sync-state user-uuid graph-uuid base-path filetxns-partitions repo *txid *stopped]
+  [*sync-state user-uuid graph-uuid base-path filetxns-partitions repo *txid *stopped before-f after-f]
   (go-loop [filetxns-partitions* filetxns-partitions]
     (if @*stopped
       {:stop true}
       (when (seq filetxns-partitions*)
         (let [filetxns (first filetxns-partitions*)
               paths (map relative-path filetxns)
-              _ (swap! *sync-state sync-state--add-current-remote->local-files paths)
+              _ (when (and before-f (fn? before-f)) (before-f filetxns))
+              _ (when *sync-state (swap! *sync-state sync-state--add-current-remote->local-files paths))
               r (<! (apply-filetxns graph-uuid base-path filetxns))
-              _ (swap! *sync-state sync-state--remove-current-remote->local-files paths)]
+              _ (when *sync-state (swap! *sync-state sync-state--remove-current-remote->local-files paths))]
           (if (instance? ExceptionInfo r)
             r
             (let [latest-txid (apply max (map #(.-txid ^FileTxn %) filetxns))]
+              (when (and after-f (fn? after-f)) (after-f filetxns))
               (when *txid
                 (reset! *txid latest-txid)
                 (update-graphs-txid! latest-txid graph-uuid user-uuid repo))
@@ -928,32 +979,32 @@
   if local-txid != remote-txid, return {:need-sync-remote true}"))
 
 (defrecord Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *sync-state
-                              ^:mutable local->remote-syncer *stopped]
+                                ^:mutable local->remote-syncer *stopped]
   Object
   (set-local->remote-syncer! [_ s] (set! local->remote-syncer s))
   (sync-files-remote->local!
     [_ relative-filepaths latest-txid]
     (go
       (let [partitioned-filetxns
-              (sequence (filepaths->partitioned-filetxns 10 graph-uuid user-uuid)
-                        relative-filepaths)
-              r
-              (if (empty? (flatten partitioned-filetxns))
-                {:succ true}
-                (<! (apply-filetxns-partitions
-                     *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo
-                     nil *stopped)))]
-          (cond
-            (instance? ExceptionInfo r)
-            {:unknown r}
+            (sequence (filepaths->partitioned-filetxns 10 graph-uuid user-uuid)
+                      relative-filepaths)
+            r
+            (if (empty? (flatten partitioned-filetxns))
+              {:succ true}
+              (<! (apply-filetxns-partitions
+                   *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo
+                   nil *stopped nil nil)))]
+        (cond
+          (instance? ExceptionInfo r)
+          {:unknown r}
 
-            @*stopped
-            {:stop true}
+          @*stopped
+          {:stop true}
 
-            :else
-            (do (update-graphs-txid! latest-txid graph-uuid user-uuid repo)
-                (reset! *txid latest-txid)
-                {:succ true})))))
+          :else
+          (do (update-graphs-txid! latest-txid graph-uuid user-uuid repo)
+              (reset! *txid latest-txid)
+              {:succ true})))))
 
   IRemote->LocalSync
   (stop-remote->local! [_] (vreset! *stopped true))
@@ -982,7 +1033,8 @@
                               (reset! *txid latest-txid)
                               {:succ true})
                           (<! (apply-filetxns-partitions
-                               *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo *txid *stopped)))))))))]
+                               *sync-state user-uuid graph-uuid base-path
+                               partitioned-filetxns repo *txid *stopped nil nil)))))))))]
         (cond
           (instance? ExceptionInfo r)
           {:unknown r}
@@ -1003,12 +1055,11 @@
             remote-all-files-meta (<! remote-all-files-meta-c)
             local-all-files-meta (<! local-all-files-meta-c)
             diff-remote-files (set/difference remote-all-files-meta local-all-files-meta)
-            latest-txid (:TXId
-                         (<! (get-remote-graph remoteapi nil graph-uuid)))]
+            latest-txid (:TXId (<! (get-remote-graph remoteapi nil graph-uuid)))]
         (println "[full-sync(remote->local)]"
                  (count diff-remote-files) "files need to sync")
         (<! (.sync-files-remote->local!
-             this (map -relative-path diff-remote-files)
+             this (map relative-path diff-remote-files)
              latest-txid))))))
 
 (defn- file-changed?
@@ -1392,10 +1443,16 @@
 
 (defn- check-graph-belong-to-current-user
   [current-user-uuid graph-user-uuid]
-  (let [result (= current-user-uuid graph-user-uuid)]
-    (when-not result
-      (notification/show! (t :file-sync/other-user-graph) :warning false))
-    result))
+  (cond
+    (nil? current-user-uuid)
+    false
+
+    (= current-user-uuid graph-user-uuid)
+    true
+
+    :else
+    (do (notification/show! (t :file-sync/other-user-graph) :warning false)
+        false)))
 
 (defn check-remote-graph-exists
   [local-graph-uuid]

+ 14 - 25
src/main/frontend/handler/editor.cljs

@@ -49,7 +49,6 @@
             [goog.dom.classes :as gdom-classes]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [medley.core :as medley]
             [promesa.core :as p]
             [frontend.util.keycode :as keycode]
             [logseq.graph-parser.util :as gp-util]
@@ -256,10 +255,9 @@
 
 (defn- another-block-with-same-id-exists?
   [current-id block-id]
-  (and (string? block-id)
-       (util/uuid-string? block-id)
-       (not= current-id (cljs.core/uuid block-id))
-       (db/entity [:block/uuid (cljs.core/uuid block-id)])))
+  (when-let [id (and (string? block-id) (parse-uuid block-id))]
+    (and (not= current-id id)
+         (db/entity [:block/uuid id]))))
 
 (defn- attach-page-properties-if-exists!
   [block]
@@ -481,14 +479,9 @@
 (defn- block-self-alone-when-insert?
   [config uuid]
   (let [current-page (state/get-current-page)
-        block-id (or
-                  (and (:id config)
-                       (util/uuid-string? (:id config))
-                       (:id config))
-                  (and current-page
-                       (util/uuid-string? current-page)
-                       current-page))]
-    (= uuid (and block-id (medley/uuid block-id)))))
+        block-id (or (some-> (:id config) parse-uuid)
+                     (some-> current-page parse-uuid))]
+    (= uuid block-id)))
 
 (defn insert-new-block-before-block-aux!
   [config block _value {:keys [ok-handler]}]
@@ -1175,10 +1168,7 @@
   []
   (if (state/editing?)
     (let [page (state/get-current-page)
-          block-id (and
-                    (string? page)
-                    (util/uuid-string? page)
-                    (medley/uuid page))]
+          block-id (and (string? page) (parse-uuid page))]
       (when block-id
         (let [block-parent (db/get-block-parent block-id)]
           (if-let [id (and
@@ -1355,7 +1345,7 @@
 
 (defn get-asset-file-link
   [format url file-name image?]
-  (let [pdf? (and url (string/ends-with? url ".pdf"))]
+  (let [pdf? (and url (string/ends-with? (string/lower-case url) ".pdf"))]
     (case (keyword format)
       :markdown (util/format (str (when (or image? pdf?) "!") "[%s](%s)") file-name url)
       :org (if image?
@@ -2042,8 +2032,8 @@
 (defn- last-top-level-child?
   [{:keys [id]} current-node]
   (when id
-    (when-let [entity (if (util/uuid-string? (str id))
-                        (db/entity [:block/uuid (uuid id)])
+    (when-let [entity (if-let [id' (parse-uuid (str id))]
+                        (db/entity [:block/uuid id'])
                         (db/entity [:block/name (util/page-name-sanity-lc id)]))]
       (= (:block/uuid entity) (tree/-get-parent-id current-node)))))
 
@@ -3110,7 +3100,7 @@
   (when-let [block-id (some-> (state/get-selection-blocks)
                               first
                               (dom/attr "blockid")
-                              medley/uuid)]
+                              uuid)]
     (util/stop e)
     (let [block    {:block/uuid block-id}
           block-id (-> (state/get-selection-blocks)
@@ -3222,8 +3212,7 @@
     :or {collapse? false expanded? false incremental? true root-block nil}}]
   (when-let [page (or (state/get-current-page)
                       (date/today))]
-    (let [block? (util/uuid-string? page)
-          block-id (or root-block (and block? (uuid page)))
+    (let [block-id (or root-block (parse-uuid page))
           blocks (if block-id
                    (db/get-block-and-children (state/get-current-repo) block-id)
                    (db/get-page-blocks-no-cache page))
@@ -3320,7 +3309,7 @@
        (->> (get-selected-blocks)
             (map (fn [dom]
                    (-> (dom/attr dom "blockid")
-                       medley/uuid
+                       uuid
                        expand-block!)))
             doall)
        (and clear-selection? (clear-selection!)))
@@ -3353,7 +3342,7 @@
        (->> (get-selected-blocks)
             (map (fn [dom]
                    (-> (dom/attr dom "blockid")
-                       medley/uuid
+                       uuid
                        collapse-block!)))
             doall)
        (and clear-selection? (clear-selection!)))

+ 97 - 14
src/main/frontend/handler/file_sync.cljs

@@ -1,15 +1,19 @@
 (ns frontend.handler.file-sync
   (:require ["path" :as path]
             [cljs-time.coerce :as tc]
+            [cljs-time.format :as tf]
             [cljs.core.async :as async :refer [go <!]]
+            [cljs.core.async.interop :refer [p->c]]
             [clojure.string :as string]
+            [clojure.set :as set]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.fs.sync :as sync]
             [frontend.handler.notification :as notification]
             [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.handler.user :as user]))
+            [frontend.handler.user :as user]
+            [frontend.fs :as fs]))
 
 (def hiding-login&file-sync (not config/dev?))
 (def refresh-file-sync-component (atom false))
@@ -51,9 +55,41 @@
   []
   (go (:Graphs (<! (sync/list-remote-graphs sync/remoteapi)))))
 
+(defn download-all-files
+  [repo graph-uuid user-uuid base-path]
+  (go
+    (state/reset-file-sync-download-init-state!)
+    (state/set-file-sync-download-init-state! {:total js/NaN :finished 0 :downloading? true})
+    (let [remote-all-files-meta (<! (sync/get-remote-all-files-meta sync/remoteapi graph-uuid))
+          local-all-files-meta (<! (sync/get-local-all-files-meta sync/rsapi graph-uuid base-path))
+          diff-remote-files (set/difference remote-all-files-meta local-all-files-meta)
+          latest-txid (:TXId (<! (sync/get-remote-graph sync/remoteapi nil graph-uuid)))
+          partitioned-filetxns
+          (sequence (sync/filepaths->partitioned-filetxns 10 graph-uuid user-uuid)
+                    (map sync/relative-path diff-remote-files))]
+      (state/set-file-sync-download-init-state! {:total (count diff-remote-files) :finished 0})
+      (let [r (<! (sync/apply-filetxns-partitions
+                   nil user-uuid graph-uuid base-path partitioned-filetxns repo nil (atom false)
+                   (fn [filetxns]
+                     (state/set-file-sync-download-init-state!
+                      {:downloading-files (mapv sync/relative-path filetxns)}))
+                   (fn [filetxns]
+                     (state/set-file-sync-download-init-state!
+                      {:finished (+ (count filetxns)
+                                    (or (:finished (state/get-file-sync-download-init-state)) 0))}))))]
+        (if (instance? ExceptionInfo r)
+          ;; TODO: add re-download button
+          (notification/show! (str "Download graph failed: " (ex-cause r)) :warning)
+          (do (state/reset-file-sync-download-init-state!)
+              (sync/update-graphs-txid! latest-txid graph-uuid user-uuid repo)))))))
+
 (defn switch-graph [graph-uuid]
-  (sync/update-graphs-txid! 0 graph-uuid (user/user-uuid) (state/get-current-repo))
-  (swap! refresh-file-sync-component not))
+  (let [repo (state/get-current-repo)
+        base-path (config/get-repo-dir repo)
+        user-uuid (user/user-uuid)]
+    (sync/update-graphs-txid! 0 graph-uuid user-uuid repo)
+    (download-all-files repo graph-uuid user-uuid base-path)
+    (swap! refresh-file-sync-component not)))
 
 (defn- download-version-file [graph-uuid file-uuid version-uuid]
 
@@ -65,31 +101,78 @@
         (notification/show! (ex-cause r) :error)
         (notification/show! [:div
                              [:div "Downloaded version file at: "]
-                             [:div key]] :success false)))))
+                             [:div key]] :success false))
+      (when-not (instance? ExceptionInfo r)
+        key))))
+
+(defn- list-file-local-versions
+  [page]
+  (go
+    (when-let [path (-> page :block/file :file/path)]
+      (let [base-path           (config/get-repo-dir (state/get-current-repo))
+            rel-path            (string/replace-first path base-path "")
+            version-files-dir   (->> (path/join "version-files/local" rel-path)
+                                     path/parse
+                                     (#(js->clj % :keywordize-keys true))
+                                     ((juxt :dir :name))
+                                     (apply path/join base-path))
+            version-file-paths* (<! (p->c (fs/readdir version-files-dir)))]
+        (when-not (instance? ExceptionInfo version-file-paths*)
+          (let [version-file-paths
+                (filterv
+                 ;; filter dir
+                 (fn [dir-or-file]
+                   (-> (path/parse dir-or-file)
+                       (js->clj :keywordize-keys true)
+                       :ext
+                       seq))
+                 (js->clj (<! (p->c (fs/readdir version-files-dir)))))]
+            (mapv
+             (fn [path]
+               (let [create-time
+                     (-> (path/parse path)
+                         (js->clj :keywordize-keys true)
+                         :name
+                         (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
+                 {:create-time create-time :path path :relative-path (string/replace-first path base-path "")}))
+             version-file-paths)))))))
 
 (defn list-file-versions [graph-uuid page]
   (let [file-id (:db/id (:block/file page))]
     (when-let [path (:file/path (db/entity file-id))]
       (let [base-path (config/get-repo-dir (state/get-current-repo))
-            path* (string/replace-first path base-path "")]
+            path*     (string/replace-first path base-path "")]
         (go
-          (let [version-list (:VersionList
-                              (<! (sync/get-remote-file-versions sync/remoteapi graph-uuid path*)))]
+          (let [version-list       (:VersionList
+                                    (<! (sync/get-remote-file-versions sync/remoteapi graph-uuid path*)))
+                local-version-list (<! (list-file-local-versions page))
+                all-version-list   (->> (concat version-list local-version-list)
+                                        (sort-by #(or (tc/from-string (:CreateTime %))
+                                                      (:create-time %))
+                                                 >))]
             (notification/show! [:div
                                  [:div.font-bold "File history - " path*]
                                  [:hr.my-2]
-                                 (for [version version-list]
-                                   (let [version-uuid (:VersionUUID version)]
+                                 (for [version all-version-list]
+                                   (let [version-uuid (or (:VersionUUID version) (:relative-path version))
+                                         local?       (some? (:relative-path version))]
                                      [:div.my-4 {:key version-uuid}
                                       [:div
                                        [:a.text-xs.inline
-                                        {:on-click #(download-version-file graph-uuid
-                                                                           (:FileUUID version)
-                                                                           (:VersionUUID version))}
+                                        {:on-click #(if local?
+                                                      (js/window.apis.openPath (:path version))
+                                                      (go
+                                                        (let [relative-path
+                                                              (<! (download-version-file graph-uuid
+                                                                                         (:FileUUID version)
+                                                                                         (:VersionUUID version)))]
+                                                          (js/window.apis.openPath (path/join base-path relative-path)))))}
                                         version-uuid]
-                                       [:div.opacity-70 (str "Size: " (:Size version))]]
+                                       (when-not local?
+                                         [:div.opacity-70 (str "Size: " (:Size version))])]
                                       [:div.opacity-50
-                                       (util/time-ago (tc/from-string (:CreateTime version)))]]))]
+                                       (util/time-ago (or (tc/from-string (:CreateTime version))
+                                                          (:create-time version)))]]))]
                                 :success false)))))))
 
 (defn get-current-graph-uuid [] (second @sync/graphs-txid))

+ 16 - 8
src/main/frontend/handler/plugin.cljs

@@ -364,9 +364,7 @@
   [pid]
   (when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
     (when-let [theme (first themes)]
-      (let [theme-mode (:mode theme)]
-        (and theme-mode (state/set-theme! theme-mode))
-        (js/LSPluginCore.selectTheme (bean/->js theme))))))
+      (js/LSPluginCore.selectTheme (bean/->js theme)))))
 
 (defn update-plugin-settings-state
   [id settings]
@@ -599,12 +597,22 @@
                                        (swap! state/state assoc :plugin/installed-themes
                                               (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
 
-                (.on "theme-selected" (fn [^js opts]
-                                        (let [opts (bean/->clj opts)
-                                              url (:url opts)
-                                              mode (:mode opts)]
-                                          (when mode (state/set-theme! mode))
+                (.on "theme-selected" (fn [^js theme]
+                                        (let [theme (bean/->clj theme)
+                                              url (:url theme)
+                                              mode (:mode theme)]
+                                          (when mode
+                                            (state/set-custom-theme! mode theme)
+                                            (state/set-theme-mode! mode))
                                           (state/set-state! :plugin/selected-theme url))))
+                                        
+                (.on "reset-custom-theme" (fn [^js themes]
+                                            (let [themes (bean/->clj themes)
+                                                  custom-theme (dissoc themes :mode)
+                                                  mode (:mode themes)]
+                                              (state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
+                                                                        :dark (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
+                                              (state/set-theme-mode! mode))))
 
                 (.on "settings-changed" (fn [id ^js settings]
                                           (let [id (keyword id)]

+ 1 - 2
src/main/frontend/handler/route.cljs

@@ -9,7 +9,6 @@
             [frontend.state :as state]
             [logseq.graph-parser.text :as text]
             [frontend.util :as util]
-            [medley.core :as medley]
             [reitit.frontend.easy :as rfe]))
 
 (defn redirect!
@@ -85,7 +84,7 @@
     (let [name (:name path-params)
           block? (util/uuid-string? name)]
       (if block?
-        (if-let [block (db/entity [:block/uuid (medley/uuid name)])]
+        (if-let [block (db/entity [:block/uuid (uuid name)])]
           (let [content (text/remove-level-spaces (:block/content block)
                                                   (:block/format block) (config/get-block-pattern (:block/format block)))]
             (if (> (count content) 48)

+ 79 - 62
src/main/frontend/mobile/core.cljs

@@ -1,84 +1,101 @@
 (ns frontend.mobile.core
-  (:require [frontend.mobile.util :as mobile-util]
-            [frontend.state :as state]
-            ["@capacitor/app" :refer [^js App]]
-            ;; ["@capacitor/keyboard" :refer [^js Keyboard]]
-            #_:clj-kondo/ignore
-            ["@capacitor/status-bar" :refer [^js StatusBar]]
-            [frontend.mobile.intent :as intent]
+  (:require ["@capacitor/app" :refer [^js App]]
             [clojure.string :as string]
             [frontend.fs.capacitor-fs :as fs]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.user :as user-handler]
+            [frontend.mobile.deeplink :as deeplink]
+            [frontend.mobile.intent :as intent]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
             [frontend.util :as util]))
 
+(def *url (atom nil))
+;; FIXME: `appUrlOpen` are fired twice when receiving a same intent. 
+;; The following two variable atoms are used to compare whether
+;; they are from the same intent share.
+(def *last-shared-url (atom nil))
+(def *last-shared-seconds (atom 0))
+
 (defn- ios-init
+  "Initialize iOS-specified event listeners"
   []
   (let [path (fs/iOS-ensure-documents!)]
     (println "iOS container path: " path))
 
-  ;; Keyboard watcher
-  ;; (.addListener Keyboard "keyboardWillShow"
-  ;;               #(state/pub-event! [:mobile/keyboard-will-show]))
-  ;; (.addListener Keyboard "keyboardDidShow"
-  ;;               #(state/pub-event! [:mobile/keyboard-did-show]))
-  )
+  (.addEventListener js/window
+                     "load"
+                     (fn [_event]
+                       (when @*url
+                         (js/setTimeout #(deeplink/deeplink @*url)
+                                        1000))))
+
+  (.removeAllListeners mobile-util/file-sync)
 
-(defn init!
+  (.addListener mobile-util/file-sync "debug"
+                (fn [event]
+                  (js/console.log "🔄" event))))
+
+(defn- android-init
+  "Initialize Android-specified event listeners"
   []
   ;; patch back navigation
-  (when (mobile-util/native-android?)
-    (.addListener App "backButton"
-                  #(let [href js/window.location.href]
-                     (when (true? (cond
-                                    (state/get-left-sidebar-open?)
-                                    (state/set-left-sidebar-open! false)
+  (.addListener App "backButton"
+                #(let [href js/window.location.href]
+                   (when (true? (cond
+                                  (state/get-left-sidebar-open?)
+                                  (state/set-left-sidebar-open! false)
 
-                                    (state/settings-open?)
-                                    (state/close-settings!)
+                                  (state/settings-open?)
+                                  (state/close-settings!)
 
-                                    (state/modal-opened?)
-                                    (state/close-modal!)
+                                  (state/modal-opened?)
+                                  (state/close-modal!)
 
-                                    :else true))
+                                  :else true))
 
-                       (if (or (string/ends-with? href "#/")
-                               (string/ends-with? href "/")
-                               (not (string/includes? href "#/")))
-                         (.exitApp App)
-                         (js/window.history.back))))))
+                     (if (or (string/ends-with? href "#/")
+                             (string/ends-with? href "/")
+                             (not (string/includes? href "#/")))
+                       (.exitApp App)
+                       (js/window.history.back)))))
+
+  (.addEventListener js/window "sendIntentReceived"
+                       #(intent/handle-received)))
+
+(defn- general-init
+  "Initialize event listeners used by both iOS and Android"
+  []
+  (.addListener App "appUrlOpen"
+                (fn [^js data]
+                  (when-let [url (.-url data)]
+                    (if-not (= (.-readyState js/document) "complete")
+                      (reset! *url url)
+                      (when-not (and (= @*last-shared-url url)
+                                     (<= (- (.getSeconds (js/Date.)) @*last-shared-seconds) 1))
+                        (reset! *last-shared-url url)
+                        (reset! *last-shared-seconds (.getSeconds (js/Date.)))
+                        (deeplink/deeplink url))))))
+
+  (.addListener mobile-util/fs-watcher "watcher"
+                (fn [event]
+                  (state/pub-event! [:file-watcher/changed event])))
+
+  (.addEventListener js/window "statusTap"
+                     #(util/scroll-to-top true))
+
+  (.addListener App "appStateChange"
+                (fn [^js state]
+                  (when (state/get-current-repo)
+                    (let [is-active? (.-isActive state)]
+                      (when-not is-active?
+                        (editor-handler/save-current-block!)))))))
+
+(defn init! []
+  (when (mobile-util/native-android?)
+    (android-init))
 
   (when (mobile-util/native-ios?)
-    (ios-init)
-    (.removeAllListeners mobile-util/file-sync)
-
-    (.addListener App "appUrlOpen"
-                  (fn [^js data]
-                    (when-let [url (.-url data)]
-                      ;; TODO: handler other logseq:// URLs
-                      (when (string/starts-with? url "logseq://auth-callback")
-                        (let [parsed-url (js/URL. url)
-                              code (.get (.-searchParams parsed-url) "code")]
-                          (user-handler/login-callback code))))))
-
-    (.addListener mobile-util/file-sync "debug"
-                  (fn [event]
-                    (js/console.log "🔄" event))))
+    (ios-init))
 
   (when (mobile-util/is-native-platform?)
-    (.addListener mobile-util/fs-watcher "watcher"
-                  (fn [event]
-                    (state/pub-event! [:file-watcher/changed event])))
-
-    (.addEventListener js/window "statusTap"
-                       #(util/scroll-to-top true))
-
-    (.addListener App "appStateChange"
-                  (fn [^js state]
-                    (when (state/get-current-repo)
-                      (let [is-active? (.-isActive state)]
-                        (when is-active?
-                          (editor-handler/save-current-block!))))))
-
-    (.addEventListener js/window "sendIntentReceived"
-                       #(intent/handle-received))))
+    (general-init)))

+ 81 - 0
src/main/frontend/mobile/deeplink.cljs

@@ -0,0 +1,81 @@
+(ns frontend.mobile.deeplink 
+  (:require
+   [clojure.string :as string]
+   [frontend.config :as config]
+   [frontend.db.model :as db-model]
+   [frontend.handler.editor :as editor-handler]
+   [frontend.handler.notification :as notification]
+   [frontend.handler.route :as route-handler]
+   [frontend.handler.user :as user-handler]
+   [frontend.mobile.intent :as intent]
+   [frontend.state :as state]
+   [logseq.graph-parser.text :as text]))
+
+(def *link-to-another-graph (atom false))
+
+(defn deeplink [url]
+  (let [parsed-url (js/URL. url)
+        hostname (.-hostname parsed-url)
+        pathname (.-pathname parsed-url)
+        search-params (.-searchParams parsed-url)
+        current-repo-url (state/get-current-repo)
+        get-graph-name-fn #(-> (text/get-graph-name-from-path %)
+                               (string/split "/")
+                               last
+                               string/lower-case)
+        current-graph-name (get-graph-name-fn current-repo-url)
+        repos (->> (state/sub [:me :repos])
+                   (remove #(= (:url %) config/local-repo))
+                   (map :url))
+        repo-names (map #(get-graph-name-fn %) repos)]
+    (cond
+      (= hostname "auth-callback")
+      (when-let [code (.get search-params  "code")]
+        (user-handler/login-callback code))
+
+      (= hostname "graph")
+      (let [graph-name (some-> pathname
+                               (string/replace "/" "")
+                               string/lower-case)
+            [page-name block-uuid] (map #(.get search-params %)
+                                        ["page" "block-id"])]
+
+        (when-not (string/blank? graph-name)
+          (when-not (= graph-name current-graph-name)
+            (let [graph-idx (.indexOf repo-names graph-name)
+                  graph-url (when (not= graph-idx -1)
+                              (nth repos graph-idx))]
+              (if graph-url
+                (do (state/pub-event! [:graph/switch graph-url])
+                    (reset! *link-to-another-graph true))
+                (notification/show! (str "Open graph failed. Graph `" graph-name "` doesn't exist.") :error false))))
+
+          (when (or (= graph-name current-graph-name)
+                    @*link-to-another-graph)
+            (js/setTimeout
+             (fn []
+               (cond
+                 page-name
+                 (let [db-page-name (db-model/get-redirect-page-name page-name)]
+                   (editor-handler/insert-first-page-block-if-not-exists! db-page-name))
+
+                 block-uuid
+                 (if (db-model/get-block-by-uuid block-uuid)
+                   (route-handler/redirect-to-page! block-uuid)
+                   (notification/show! (str "Open link failed. Block-id `" block-uuid "` doesn't exist in the graph.") :error false))
+
+                 :else
+                 nil)
+               (reset! *link-to-another-graph false))
+             (if @*link-to-another-graph
+               1000
+               0)))))
+
+      (= hostname "shared")
+      (let [result (into {} (map (fn [key]
+                                   [(keyword key) (.get search-params key)])
+                                 ["title" "url" "type"]))]
+        (intent/handle-result result))
+
+      :else
+      nil)))

+ 17 - 12
src/main/frontend/mobile/intent.cljs

@@ -78,10 +78,12 @@
   (p/let [time (date/get-current-time)
           title (some-> (or title (path/basename url))
                         js/decodeURIComponent
-                        util/node-path.name)
+                        util/node-path.name
+                        util/file-name-sanity
+                        (string/replace "." ""))
           path (path/join (config/get-repo-dir (state/get-current-repo))
                           (config/get-pages-directory)
-                          (path/basename url))
+                          (str (js/encodeURI title) (path/extname url)))
           _ (p/catch
                 (.copy Filesystem (clj->js {:from url :to path}))
                 (fn [error]
@@ -144,15 +146,9 @@
                         (js/decodeURIComponent v)
                         v))])))
 
-(defn handle-received []
-  (p/let [received (p/catch
-                    (.checkSendIntentReceived SendIntent)
-                    (fn [error]
-                      (log/error :intent-received-error {:error error})))]
-    (when received
-      (let [result (-> (js->clj received :keywordize-keys true)
-                       decode-received-result)]
-        (when-let [type (:type result)]
+(defn handle-result [result]
+  (let [result (decode-received-result result)]
+    (when-let [type (:type result)]
           (cond
             (string/starts-with? type "text/")
             (handle-received-text result)
@@ -172,4 +168,13 @@
               [:a {:href "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"
                    :target "_blank"} "Github"]
               ". We will look into it soon."
-              [:pre.code (with-out-str (pprint/pprint result))]] :warning false)))))))
+              [:pre.code (with-out-str (pprint/pprint result))]] :warning false)))))
+
+(defn handle-received []
+  (p/let [received (p/catch
+                       (.checkSendIntentReceived SendIntent)
+                       (fn [error]
+                         (log/error :intent-received-error {:error error})))]
+    (when received
+      (let [result (js->clj received :keywordize-keys true)]
+        (handle-result result)))))

+ 5 - 5
src/main/frontend/modules/outliner/datascript.cljc

@@ -8,8 +8,7 @@
                      [frontend.state :as state]
                      [frontend.config :as config]
                      [logseq.graph-parser.util :as gp-util]
-                     [lambdaisland.glogi :as log]
-                     [medley.core :as medley])))
+                     [lambdaisland.glogi :as log])))
 
 #?(:cljs
    (defn new-outliner-txs-state [] (atom [])))
@@ -33,9 +32,10 @@
      (some->> (gp-util/remove-nils txs)
               (map (fn [x]
                      (if (map? x)
-                       (medley/map-vals (fn [v] (if (vector? v)
-                                                  (remove nil? v)
-                                                  v)) x)
+                       (update-vals x (fn [v]
+                                        (if (vector? v)
+                                          (remove nil? v)
+                                          v)))
                        x))))))
 
 #?(:cljs

+ 2 - 3
src/main/frontend/modules/outliner/tree.cljs

@@ -1,6 +1,5 @@
 (ns frontend.modules.outliner.tree
   (:require [frontend.db :as db]
-            [frontend.util :as util]
             [clojure.string :as string]
             [frontend.state :as state]))
 
@@ -45,8 +44,8 @@
 (defn- get-root-and-page
   [repo root-id]
   (if (string? root-id)
-    (if (util/uuid-string? root-id)
-      [false (db/entity repo [:block/uuid (uuid root-id)])]
+    (if-let [id (parse-uuid root-id)]
+      [false (db/entity repo [:block/uuid id])]
       [true (db/entity repo [:block/name (string/lower-case root-id)])])
     [false root-id]))
 

+ 4 - 3
src/main/frontend/modules/shortcut/config.cljs

@@ -659,9 +659,10 @@
 
 (def category
   "Active list of categories for docs purpose"
-  (medley/map-vals (fn [v]
-                     (vec (remove #(:inactive (get all-default-keyboard-shortcuts %)) v)))
-                   category*))
+  (update-vals
+   category*
+   (fn [v]
+     (vec (remove #(:inactive (get all-default-keyboard-shortcuts %)) v)))))
 
 (defn add-shortcut!
   [handler-id id shortcut-map]

+ 2 - 3
src/main/frontend/modules/shortcut/core.cljs

@@ -8,8 +8,7 @@
             [frontend.util :as util]
             [goog.events :as events]
             [goog.ui.KeyboardShortcutHandler.EventType :as EventType]
-            [lambdaisland.glogi :as log]
-            [medley.core :as medley])
+            [lambdaisland.glogi :as log])
   (:import [goog.events KeyCodes KeyHandler KeyNames]
            [goog.ui KeyboardShortcutHandler]))
 
@@ -117,7 +116,7 @@
                     dispatch-fn (get shortcut-map (keyword (.-identifier e)))]
                 ;; trigger fn
                 (when dispatch-fn (dispatch-fn e))))
-          install-id (medley/random-uuid)
+          install-id (random-uuid)
           data       {install-id
                       {:group      handler-id
                        :dispatch-fn f

+ 14 - 11
src/main/frontend/modules/shortcut/dicts.cljc

@@ -1,10 +1,11 @@
 (ns ^:bb-compatible frontend.modules.shortcut.dicts
-  "Provides dictionary entries for shortcuts"
-  (:require [medley.core :as medley]))
+  "Provides dictionary entries for shortcuts")
+
 (defn- decorate-namespace [k]
   (let [n (name k)
         ns (namespace k)]
     (keyword (str "command." ns) n)))
+
 (def ^:large-vars/data-var all-default-keyboard-shortcuts
   {:date-picker/complete         "Date picker: Choose selected day"
    :date-picker/prev-day         "Date picker: Select previous day"
@@ -113,6 +114,7 @@
    :editor/toggle-open-blocks       "Toggle open blocks (collapse or expand all blocks)"
    :ui/toggle-cards                 "Toggle cards"
    :git/commit                      "Git commit message"})
+
 (def category
   {:shortcut.category/basics "Basics"
    :shortcut.category/formatting "Formatting"
@@ -122,12 +124,13 @@
    :shortcut.category/block-selection "Block selection (press Esc to quit selection)"
    :shortcut.category/toggle "Toggle"
    :shortcut.category/others "Others"})
+
 (def ^:large-vars/data-var dicts
   {:en (merge
         ;; Dynamically add this ns since command descriptions have to
         ;; stay in sync with shortcut.config command ids which do not
         ;; have a namespce
-        (medley/map-keys decorate-namespace all-default-keyboard-shortcuts)
+        (update-keys all-default-keyboard-shortcuts decorate-namespace)
         category)
 
    :zh-CN   {:shortcut.category/formatting            "格式化"
@@ -217,7 +220,7 @@
              :command.ui/toggle-theme                "“在暗色/亮色主題之間切換”"
              :command.ui/toggle-right-sidebar        "啟用/關閉右側欄"
              :command.go/journals                    "跳轉到日記"}
-   
+
    :de      {:shortcut.category/formatting           "Formatierung"
              :command.editor/indent                  "Block einrücken"
              :command.editor/outdent                 "Block ausrücken"
@@ -264,7 +267,7 @@
              :command.ui/toggle-theme                "Intervertir le thème foncé/clair"
              :command.ui/toggle-right-sidebar        "Afficher/cacher la barre latérale"
              :command.go/journals                    "Aller au Journal"}
-   
+
    :af      {:shortcut.category/formatting           "Formatering"
              :command.editor/indent                  "Ingekeepte blok oortjie"
              :command.editor/outdent                 "Oningekeepte blok"
@@ -288,7 +291,7 @@
              :command.go/journals                    "Spring na joernale"
              :command.ui/toggle-theme                "Wissel tussen donker/lig temas"
              :command.ui/toggle-right-sidebar        "Wissel regter sybalk"}
-   
+
    :es      {:shortcut.category/formatting            "Formato"
              :shortcut.category/basics                "Básico"
              :shortcut.category/navigating            "Navegación"
@@ -352,7 +355,7 @@
              :command.editor/open-edit                "Editar bloque seleccionado"
              :command.editor/delete-selection         "Eliminar bloques seleccionados"
              :command.editor/toggle-open-blocks       "Alternar bloques abieros, (colapsar o expandir todos)"}
-   
+
    :ru      {:shortcut.category/formatting            "Форматирование"
              :shortcut.category/basics                "Базовые"
              :shortcut.category/navigating            "Навигация"
@@ -416,7 +419,7 @@
              :command.editor/open-edit                "Редактировать выбранный блок"
              :command.editor/delete-selection         "Удалить выбранные блоки"
              :command.editor/toggle-open-blocks       "Переключить открытые блоки (свернуть или развернуть все)"}
-   
+
    :nb-NO   {:shortcut.category/formatting            "Formatering"
              :shortcut.category/basics                "Basis"
              :shortcut.category/navigating            "Navigasjon"
@@ -481,7 +484,7 @@
              :command.editor/open-edit                "Rediger valgt blokk"
              :command.editor/delete-selection         "Slett valgte blokker"
              :command.editor/toggle-open-blocks       "Veksle åpne blokker (slå sammen eller utvid alle blokker)"}
-   
+
    :pt-PT   {:shortcut.category/formatting            "Formatação"
              :shortcut.category/basics                "Básico"
              :shortcut.category/navigating            "Navegação"
@@ -545,7 +548,7 @@
              :command.editor/open-edit                "Editar bloco selecionado"
              :command.editor/delete-selection         "Eliminar blocos selecionados"
              :command.editor/toggle-open-blocks       "Alternar blocos abertos (colapsar ou expandir todos)"}
-   
+
    :pt-BR   {:shortcut.category/formatting            "Formatação"
              :shortcut.category/basics                "Básico"
              :shortcut.category/navigating            "Navegação"
@@ -659,7 +662,7 @@
              :command.misc/copy                       "Copiar (copiar seleção ou referência do bloco)"
              :command.ui/goto-plugins                 "Ir para o painel de plugins"
              :command.ui/open-new-window              "Abra uma nova janela"}
-   
+
    :ja      {:shortcut.category/formatting                "フォーマット"
              :shortcut.category/basics                "基本操作"
              :shortcut.category/navigating            "ナビゲーション"

+ 44 - 18
src/main/frontend/state.cljs

@@ -63,8 +63,9 @@
      :ui/settings-open?                     false
      :ui/sidebar-open?                      false
      :ui/left-sidebar-open?                 (boolean (storage/get "ls-left-sidebar-open?"))
-     :ui/theme                              (or (storage/get :ui/theme) (if (mobile-util/is-native-platform?) "light" "dark"))
+     :ui/theme                              (or (storage/get :ui/theme) "light")
      :ui/system-theme?                      ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
+     :ui/custom-theme                       (or (storage/get :ui/custom-theme) {:light {:mode "light"} :dark {:mode "dark"}})
      :ui/wide-mode?                         (storage/get :ui/wide-mode)
 
      ;; ui/collapsed-blocks is to separate the collapse/expand state from db for:
@@ -218,6 +219,8 @@
      :file-sync/sync-uploading-files        nil
      :file-sync/sync-downloading-files      nil
 
+     :file-sync/download-init-progress      nil
+
      :encryption/graph-parsing?             false
 
      :ui/whiteboards                        {}
@@ -886,30 +889,45 @@
     (set-edit-content! edit-input-id content)
     (set-state! [:editor/last-saved-cursor (:block/uuid (get-edit-block))] new-pos)))
 
-(defn set-theme!
-  [theme]
-  (set-state! :ui/theme theme)
+(defn set-theme-mode!
+  [mode]
   (when (mobile-util/native-ios?)
-    (if (= theme "light")
+    (if (= mode "light")
       (util/set-theme-light)
       (util/set-theme-dark)))
-  (storage/set :ui/theme theme))
+  (set-state! :ui/theme mode)
+  (storage/set :ui/theme mode))
 
 (defn sync-system-theme!
   []
   (let [system-dark? (.-matches (js/window.matchMedia "(prefers-color-scheme: dark)"))]
-    (set-theme! (if system-dark? "dark" "light"))
+    (set-theme-mode! (if system-dark? "dark" "light"))
     (set-state! :ui/system-theme? true)
     (storage/set :ui/system-theme? true)))
 
 (defn use-theme-mode!
   [theme-mode]
-  (if-not (= theme-mode "system")
+  (if (= theme-mode "system")
+    (sync-system-theme!)
     (do
-      (set-theme! theme-mode)
+      (set-theme-mode! theme-mode)
       (set-state! :ui/system-theme? false)
-      (storage/set :ui/system-theme? false))
-    (sync-system-theme!)))
+      (storage/set :ui/system-theme? false))))
+
+(defn toggle-theme
+  [theme]
+  (if (= theme "dark") "light" "dark"))
+
+(defn toggle-theme!
+  []
+  (use-theme-mode! (toggle-theme (:ui/theme @state))))
+
+(defn set-custom-theme!
+  ([custom-theme]
+   (set-custom-theme! nil custom-theme))
+  ([mode theme]
+   (set-state! (if mode [:ui/custom-theme (keyword mode)] :ui/custom-theme) theme)
+   (storage/set :ui/custom-theme (:ui/custom-theme @state))))
 
 (defn set-editing-block-dom-id!
   [block-dom-id]
@@ -919,12 +937,6 @@
   []
   (:editor/block-dom-id @state))
 
-(defn toggle-theme!
-  []
-  (let [theme (:ui/theme @state)
-        theme' (if (= theme "dark") "light" "dark")]
-    (use-theme-mode! theme')))
-
 (defn set-root-component!
   [component]
   (set-state! :ui/root-component component))
@@ -1258,7 +1270,7 @@
   [pid hook-or-all]
   (when-let [pid (keyword pid)]
     (if (nil? hook-or-all)
-      (swap! state update :plugin/installed-hooks #(medley/map-vals (fn [ids] (disj ids pid)) %))
+      (swap! state update :plugin/installed-hooks #(update-vals % (fn [ids] (disj ids pid))))
       (when-let [coll (get-in @state [:plugin/installed-hooks hook-or-all])]
         (set-state! [:plugin/installed-hooks hook-or-all] (disj coll pid))))
     true))
@@ -1673,6 +1685,20 @@
 (defn get-file-sync-state []
   (:file-sync/sync-state @state))
 
+(defn reset-file-sync-download-init-state!
+  []
+  (set-state! [:file-sync/download-init-progress (get-current-repo)] {}))
+
+(defn set-file-sync-download-init-state!
+  [m]
+  (update-state! [:file-sync/download-init-progress (get-current-repo)]
+                 (if (fn? m) m
+                     (fn [old-value] (merge old-value m)))))
+
+(defn get-file-sync-download-init-state
+  []
+  (get-in @state [:file-sync/download-init-progress (get-current-repo)]))
+
 (defn reset-parsing-state!
   []
   (set-state! [:graph/parsing-state (get-current-repo)] {}))

+ 9 - 1
src/main/frontend/ui.cljs

@@ -324,6 +324,14 @@
         (state/set-visual-viewport-state nil))))
   #())
 
+(defn apply-custom-theme-effect! [theme]
+  (when plugin-handler/lsp-enabled?
+    (when-let [custom-theme (state/sub [:ui/custom-theme (keyword theme)])]
+      (when-let [url (:url custom-theme)]
+        (js/LSPluginCore.selectTheme (bean/->js custom-theme)
+                                     (bean/->js {:effect false :emit false}))
+        (state/set-state! :plugin/selected-theme (:url url))))))
+
 (defn setup-system-theme-effect!
   []
   (let [^js schemaMedia (js/window.matchMedia "(prefers-color-scheme: dark)")]
@@ -930,7 +938,7 @@
                          :style {:min-height @(::height state)}}
    (if visible?
      (when (fn? content-fn) (content-fn))
-     [:div.shadow.rounded-md.p-4.w-full.mx-auto {:style {:height 64}}
+     [:div.shadow.rounded-md.p-4.w-full.mx-auto.fade-in.delay-1000.mb-5 {:style {:min-height 64}}
       [:div.animate-pulse.flex.space-x-4
        [:div.flex-1.space-y-3.py-1
         [:div.h-2.bg-base-4.rounded]

+ 8 - 26
src/main/frontend/util.cljc

@@ -217,39 +217,21 @@
     (str "0" n)
     (str n)))
 
-(defn parse-int
-  [x]
-  #?(:cljs (if (string? x)
-             (js/parseInt x)
-             x)
-     :clj (if (string? x)
-            (Integer/parseInt x)
-            x)))
-
-(defn safe-parse-int
-  [x]
-  #?(:cljs (let [result (parse-int x)]
-             (if (js/isNaN result)
-               nil
-               result))
-     :clj ((try
-             (parse-int x)
-             (catch Exception _
-               nil)))))
-#?(:cljs
-   (defn parse-float
+#?(:cljs
+   (defn safe-parse-int
+     "Use if arg could be an int or string. If arg is only a string, use `parse-long`."
      [x]
      (if (string? x)
-       (js/parseFloat x)
+       (parse-long x)
        x)))
 
 #?(:cljs
    (defn safe-parse-float
+     "Use if arg could be a float or string. If arg is only a string, use `parse-double`"
      [x]
-     (let [result (parse-float x)]
-       (if (js/isNaN result)
-         nil
-         result))))
+     (if (string? x)
+       (parse-double x)
+       x)))
 
 #?(:cljs
    (defn debounce

+ 2 - 3
src/main/frontend/util/list.cljs

@@ -1,8 +1,7 @@
 (ns frontend.util.list
   (:require [frontend.util.thingatpt :as thingatpt]
             [frontend.util.cursor :as cursor]
-            [clojure.string :as string]
-            [frontend.util :as util]))
+            [clojure.string :as string]))
 
 (defn get-prev-item [& [input]]
   (when-not (cursor/textarea-cursor-first-row? input)
@@ -56,7 +55,7 @@
              (map (fn [line] (if (newline? line) "" line)))
              (string/join "\n"))
         (let [[_ num-str] (re-find #"^(\d+){1}\." line)
-              num (if num-str (util/safe-parse-int num-str) nil)
+              num (if num-str (parse-long num-str) nil)
               double-newlines?' (or double-newlines?
                                      (and (newline? line) (seq others) (newline? (first others))))
               [idx' result'] (if (and (not double-newlines?') num)

+ 2 - 3
src/main/frontend/util/property.cljs

@@ -4,7 +4,6 @@
             [frontend.util :as util]
             [clojure.set :as set]
             [frontend.config :as config]
-            [medley.core :as medley]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.property :as gp-property :refer [properties-start properties-end]]
@@ -232,7 +231,7 @@
                                         (mldoc/properties? (second
                                                             (remove
                                                              (fn [[x _]]
-                                                               (= "Hiccup" (first x)))
+                                                               (contains? #{"Hiccup" "Raw_Html"} (first x)))
                                                              ast)))))
                                (mldoc/properties? (first ast)))
            lines (string/split-lines content)
@@ -364,7 +363,7 @@
 
 (defn add-page-properties
   [page-format properties-content properties]
-  (let [properties (medley/map-keys name properties)
+  (let [properties (update-keys properties name)
         lines (string/split-lines properties-content)
         front-matter-format? (contains? #{:markdown} page-format)
         lines (if front-matter-format?

+ 12 - 14
src/main/logseq/api.cljs

@@ -32,11 +32,9 @@
             [frontend.loader :as loader]
             [goog.dom :as gdom]
             [lambdaisland.glogi :as log]
-            [medley.core :as medley]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [sci.core :as sci]
-            [logseq.graph-parser.util :as gp-util]
             [frontend.version :as fv]
             [frontend.handler.shell :as shell]
             [frontend.modules.layout.core]))
@@ -120,7 +118,7 @@
 
 (def ^:export set_theme_mode
   (fn [mode]
-    (state/set-theme! mode)))
+    (state/set-theme-mode! mode)))
 
 (def ^:export load_plugin_config
   (fn [path]
@@ -442,11 +440,11 @@
 
 (defn ^:export open_in_right_sidebar
   [block-uuid]
-  (editor-handler/open-block-in-sidebar! (medley/uuid block-uuid)))
+  (editor-handler/open-block-in-sidebar! (uuid block-uuid)))
 
 (def ^:export edit_block
   (fn [block-uuid ^js opts]
-    (when-let [block-uuid (and block-uuid (medley/uuid block-uuid))]
+    (when-let [block-uuid (and block-uuid (uuid block-uuid))]
       (when-let [block (db-model/query-block-by-uuid block-uuid)]
         (let [{:keys [pos] :or {pos :max}} (bean/->clj opts)]
           (editor-handler/edit-block! block pos block-uuid))))))
@@ -455,7 +453,7 @@
   (fn [block-uuid-or-page-name content ^js opts]
     (let [{:keys [before sibling isPageBlock properties]} (bean/->clj opts)
           page-name (and isPageBlock block-uuid-or-page-name)
-          block-uuid (if isPageBlock nil (medley/uuid block-uuid-or-page-name))
+          block-uuid (if isPageBlock nil (uuid block-uuid-or-page-name))
           new-block (editor-handler/api-insert-new-block!
                       content
                       {:block-uuid block-uuid
@@ -480,7 +478,7 @@
     (let [includeChildren true
           repo (state/get-current-repo)]
       (editor-handler/delete-block-aux!
-        {:block/uuid (medley/uuid block-uuid) :repo repo} includeChildren)
+        {:block/uuid (uuid block-uuid) :repo repo} includeChildren)
       nil)))
 
 (def ^:export update_block
@@ -490,7 +488,7 @@
           editing? (and edit-input (string/ends-with? edit-input block-uuid))]
       (if editing?
         (state/set-edit-content! edit-input content)
-        (editor-handler/save-block! repo (medley/uuid block-uuid) content))
+        (editor-handler/save-block! repo (uuid block-uuid) content))
       nil)))
 
 (def ^:export move_block
@@ -505,8 +503,8 @@
 
                     :else
                     nil)
-          src-block (db-model/query-block-by-uuid (medley/uuid src-block-uuid))
-          target-block (db-model/query-block-by-uuid (medley/uuid target-block-uuid))]
+          src-block (db-model/query-block-by-uuid (uuid src-block-uuid))
+          target-block (db-model/query-block-by-uuid (uuid target-block-uuid))]
       (editor-dnd-handler/move-blocks nil [src-block] target-block move-to) nil)))
 
 (def ^:export get_block
@@ -565,11 +563,11 @@
 
 (def ^:export upsert_block_property
   (fn [block-uuid key value]
-    (editor-handler/set-block-property! (medley/uuid block-uuid) key value)))
+    (editor-handler/set-block-property! (uuid block-uuid) key value)))
 
 (def ^:export remove_block_property
   (fn [block-uuid key]
-    (editor-handler/remove-block-property! (medley/uuid block-uuid) key)))
+    (editor-handler/remove-block-property! (uuid block-uuid) key)))
 
 (def ^:export get_block_property
   (fn [block-uuid key]
@@ -637,7 +635,7 @@
 
 (defn ^:export prepend_block_in_page
   [uuid-or-page-name content ^js opts]
-  (let [page? (not (gp-util/uuid-string? uuid-or-page-name))
+  (let [page? (not (util/uuid-string? uuid-or-page-name))
         page-not-exist? (and page? (nil? (db-model/get-page uuid-or-page-name)))
         _ (and page-not-exist? (page-handler/create! uuid-or-page-name
                                  {:redirect? false
@@ -653,7 +651,7 @@
 
 (defn ^:export append_block_in_page
   [uuid-or-page-name content ^js opts]
-  (let [page? (not (gp-util/uuid-string? uuid-or-page-name))
+  (let [page? (not (util/uuid-string? uuid-or-page-name))
         page-not-exist? (and page? (nil? (db-model/get-page uuid-or-page-name)))
         _ (and page-not-exist? (page-handler/create! uuid-or-page-name
                                  {:redirect? false

+ 10 - 14
src/main/logseq/graph_parser/block.cljc

@@ -129,8 +129,7 @@
 
                         :else
                         nil)]
-    (when (and block-id
-               (gp-util/uuid-string? block-id))
+    (when (some-> block-id parse-uuid)
       block-id)))
 
 (defn- paragraph-block?
@@ -208,7 +207,7 @@
 ;; {"Deadline" {:date {:year 2020, :month 10, :day 20}, :wday "Tue", :time {:hour 8, :min 0}, :repetition [["DoublePlus"] ["Day"] 1], :active true}}
 (defn timestamps->scheduled-and-deadline
   [timestamps]
-  (let [timestamps (gp-util/map-keys (comp keyword string/lower-case) timestamps)
+  (let [timestamps (update-keys timestamps (comp keyword string/lower-case))
         m (some->> (select-keys timestamps [:scheduled :deadline])
                    (map (fn [[k v]]
                           (let [{:keys [date repetition]} v
@@ -325,23 +324,21 @@
          (swap! ref-blocks conj block))
        form)
      (concat title body))
-    (let [ref-blocks (->> @ref-blocks
-                          (filter gp-util/uuid-string?))
-          ref-blocks (map
-                       (fn [id]
-                         [:block/uuid (uuid id)])
-                       ref-blocks)
+    (let [ref-blocks (keep (fn [block]
+                             (when-let [id (parse-uuid block)]
+                               [:block/uuid id]))
+                           @ref-blocks)
           refs (distinct (concat (:refs block) ref-blocks))]
       (assoc block :refs refs))))
 
 (defn- block-keywordize
   [block]
-  (gp-util/map-keys
+  (update-keys
+   block
    (fn [k]
      (if (namespace k)
        k
-       (keyword "block" k)))
-   block))
+       (keyword "block" k)))))
 
 (defn- sanity-blocks-data
   [blocks]
@@ -430,8 +427,7 @@
                                (get-in properties [:properties :custom_id])
                                (get-in properties [:properties :id]))]
         (let [custom-id (and (string? custom-id) (string/trim custom-id))]
-          (when (and custom-id (gp-util/uuid-string? custom-id))
-            (uuid custom-id))))
+          (some-> custom-id parse-uuid)))
       (d/squuid)))
 
 (defn get-page-refs-from-properties

+ 1 - 1
src/main/logseq/graph_parser/date_time_util.cljs

@@ -37,7 +37,7 @@
   (when journal-title
     (let [journal-title (gp-util/capitalize-all journal-title)]
       (journal-title-> journal-title
-                       #(gp-util/parse-int (tf/unparse (tf/formatter "yyyyMMdd") %))
+                       #(parse-long (tf/unparse (tf/formatter "yyyyMMdd") %))
                        formatters))))
 
 (defn format

+ 1 - 1
src/main/logseq/graph_parser/text.cljs

@@ -350,7 +350,7 @@
        false
 
        (and (not= k "alias") (gp-util/safe-re-find #"^\d+$" v))
-       (gp-util/safe-parse-int v)
+       (parse-long v)
 
        (gp-util/wrapped-by-quotes? v) ; wrapped in ""
        v

+ 0 - 31
src/main/logseq/graph_parser/util.cljs

@@ -4,9 +4,6 @@
   (:require [clojure.walk :as walk]
             [clojure.string :as string]))
 
-(def uuid-pattern "[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}")
-(defonce exactly-uuid-pattern (re-pattern (str "(?i)^" uuid-pattern "$")))
-
 (defn safe-re-find
   "Copy of frontend.util/safe-re-find. Too basic to couple to main app"
   [pattern s]
@@ -16,11 +13,6 @@
   (when (string? s)
     (re-find pattern s)))
 
-(defn uuid-string?
-  "Copy of frontend.util/uuid-string?. Too basic to couple to main app"
-  [s]
-  (safe-re-find exactly-uuid-pattern s))
-
 (defn path-normalize
   "Normalize file path (for reading paths from FS, not required by writting)"
   [s]
@@ -67,21 +59,6 @@
   [v]
   (and (string? v) (>= (count v) 2) (= "\"" (first v) (last v))))
 
-(defn parse-int
-  "Copy of frontend.util/parse-int. Too basic to couple to main app"
-  [x]
-  (if (string? x)
-    (js/parseInt x)
-    x))
-
-(defn safe-parse-int
-  "Copy of frontend.util/safe-parse-int. Too basic to couple to main app"
-  [x]
-  (let [result (parse-int x)]
-    (if (js/isNaN result)
-      nil
-      result)))
-
 (defn url?
   [s]
   (and (string? s)
@@ -97,14 +74,6 @@
       (js/JSON.parse)
       (js->clj :keywordize-keys true)))
 
-;; TODO: Use update-keys once its available in cljs and nbb
-(defn map-keys
-  "Maps function `f` over the keys of map `m` to produce a new map."
-  [f m]
-  (reduce-kv
-   (fn [m_ k v]
-     (assoc m_ (f k) v)) {} m))
-
 (defn zero-pad
   "Copy of frontend.util/zero-pad. Too basic to couple to main app"
   [n]

+ 8 - 9
templates/tutorial-ja.md

@@ -1,15 +1,15 @@
 ## こんにちは、Logseq へようこそ!
-- Logseq はプライバシーファーストで知識管理とコラボレーションを実現するオープンソースプラットフォームです。
+- Logseq はプライバシーファーストで知識管理とコラボレーションを実現する[オープンソース](https://github.com/logseq/logseq)プラットフォームです。
 - 以下は Logseq の使い方が3分で判るチュートリアルです。ぜひやってみましょう!
 - 役に立つヒントがありますよ。
 #+BEGIN_TIP
 ・ブロック(段落)を編集するにはクリックしてください。
-・新しいブロックを作成するには `Enter` キーを押してください。
+・編集中に新しいブロックを作成するには `Enter` キーを押してください。
 ・ブロック内で新しい行を入力するには、`Shift+Enter` キーを押してください。
 ・`/` キーを押すと全てのコマンドが表示されます。
 #+END_TIP
-- 1. [[見本のノートの作り方]]というページを開きましょう. 左のリンクをクリックすると開くことができます。`Shift+クリック` すると右のサイドバーで開くことができます!
-クリックで開いた場合は「Linked References」と「Unlinked References」も表示されているはずです。Linked References はこのページへリンクしているページのリストです。Unlinked References はこのページのタイトルを本文中に含むページのリストです。
+- 1. [[見本のノートの作り方]]というページへ書き込んでみましょう。左のリンクをクリックすると開くことができます。`Shift+クリック` すると右のサイドバーで開くことができます!
+「Linked References」と「Unlinked References」も表示されているはずです。Linked References はこのページへリンクしているページのリストです。Unlinked References はこのページのタイトルを本文中に含むページのリストです。
 
 - 2. [[見本のノートの作り方]]上で「参照」をやってみましょう。下のブロック参照(リンク)を `Shift+クリック` して、右のサイドバーで開いてください。サイドバー側でブロックを修正すると、ブロック参照の側も同じように修正されます!
     - ((5f713e91-8a3c-4b04-a33a-c39482428e2d)) : これはブロック参照です。
@@ -18,14 +18,13 @@
 - 3. タグは使えますか?
     - もちろん。これは #ダミー のタグです。
 
-- 4. 「ToDo」「作業中」(Doing)「完了」(Done)や優先度のようなタスク管理はサポートしていますか?
+- 4. todo/doing/done(ToDo/作業中/完了)や優先度といったタスク管理はサポートしていますか?
     - はい。キーボードで`/`とタイプし、表示されるメニューからToDo管理のための TODO、DOING、DONE、NOW、LATER や優先度の A、B、Cという語をタイプするか選んでください。(下はその例です)
     - NOW [#A] "見本のノートの作り方" のチュートリアル
-    - LATER [#A] [:a {:href "https://twitter.com/TechWithEd" :target "_blank"} "@TechWithEd"] の作ったこちらのビデオを見てください(※ビデオは英語です。)これは Logseq でローカルフォルダを開く方法を示しています。
-
-    {{tutorial-video}}
+    - LATER [#A] [:a {:href "https://twitter.com/shuomi3" :target "_blank"} "@shuomi3"] の作ったこちらのビデオを見てください(※ビデオは英語です。)これは Logseq でノートをとって暮らしの計画を立てる方法を示しています。
+    {{youtube https://www.youtube.com/watch?v=BhHfF0P9A80&ab_channel=ShuOmi}}
 
     - DONE ページ作成
     - CANCELED [#C] 1000ブロック以上のページを作成する
 - 以上です!ここから、さらにブロックを作成したり、ローカルディレクトリを開いてノートをインポートすることができます!
-- デスクトップアプリをダウンロードするならこちら: https://github.com/logseq/logseq/releases
+- デスクトップアプリのダウンロードはこちらから: https://github.com/logseq/logseq/releases

+ 4 - 4
yarn.lock

@@ -728,10 +728,10 @@
   resolved "https://registry.yarnpkg.com/@kanru/rage-wasm/-/rage-wasm-0.2.1.tgz#dd8fdd3133992c42bf68c0086d8cad40a13bc329"
   integrity sha512-sYi4F2mL6Mpcz7zbS4myasw11xLBEbgZkDMRVg9jNxTKt6Ct/LT7/vCHDmEzAFcPcPqixD5De6Ql3bJijAX0/w==
 
-"@logseq/nbb-logseq@^0.3.99":
-  version "0.3.99"
-  resolved "https://registry.yarnpkg.com/@logseq/nbb-logseq/-/nbb-logseq-0.3.99.tgz#cf6c05c559963e4e0fb92f214a63228972ef87d3"
-  integrity sha512-Msa6Ck6wqt7sYGExQZgUT/uUG/z5jGaPlytVdgGkCrVogZb2yaChbWeMOCfCsCTi6kbHo15hC3aOeDAXcfnFzw==
+"@logseq/nbb-logseq@^0.5.103":
+  version "0.5.103"
+  resolved "https://registry.yarnpkg.com/@logseq/nbb-logseq/-/nbb-logseq-0.5.103.tgz#1084380cd54c92ca8cc94a8934cc777206e45cc0"
+  integrity sha512-V9UW0XrCaaadHUc6/Hp9wfGpQqkzqzoqnDGeSVZkWR6l3QwyqGi9mkhnhVcfTwAvxIfOgrfz93GcaeepV4pYNA==
   dependencies:
     import-meta-resolve "^1.1.1"
 

Some files were not shown because too many files changed in this diff