Browse Source

Merge branch 'master' into disable-webview-resize

llcc 3 years ago
parent
commit
957c895082

+ 3 - 0
bb.edn

@@ -24,6 +24,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
 

+ 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(

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


+ 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)))

+ 3 - 3
src/main/frontend/components/block.cljs

@@ -874,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?)
@@ -889,6 +886,9 @@
 
         (mobile-util/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)

+ 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)

+ 71 - 46
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]

+ 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 {

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

@@ -345,6 +345,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})
 

+ 43 - 33
src/main/frontend/fs/sync.cljs

@@ -353,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"
@@ -364,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 [_]
@@ -390,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)
@@ -754,8 +755,9 @@
             repo (state/get-current-repo)
             file-path (config/get-file-path repo path)
             content (<! (p->c (fs/read-file "" file-path)))]
-        (or (nil? content)
-            (some :removed (diff/diff origin-db-content content)))))))
+        (and origin-db-content
+             (or (nil? content)
+                 (some :removed (diff/diff origin-db-content content))))))))
 
 (defn- apply-filetxns
   [graph-uuid base-path filetxns]
@@ -797,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))
@@ -982,25 +986,25 @@
     [_ 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))
@@ -1029,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}
@@ -1050,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?
@@ -1439,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]

+ 35 - 2
src/main/frontend/handler/file_sync.cljs

@@ -5,6 +5,7 @@
             [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]
@@ -54,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]
 

+ 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)]

+ 82 - 63
src/main/frontend/mobile/core.cljs

@@ -1,92 +1,111 @@
 (ns frontend.mobile.core
-  (:require [frontend.mobile.util :as mobile-util]
-            [frontend.state :as state]
-            ["@capacitor/app" :refer [^js App]]
+  (:require ["@capacitor/app" :refer [^js App]]
             ["@capacitor/keyboard" :refer [^js Keyboard]]
-            ;; ["@capacitor/keyboard" :refer [^js Keyboard]]
-            #_:clj-kondo/ignore
-            ["@capacitor/status-bar" :refer [^js StatusBar]]
-            [frontend.mobile.intent :as intent]
             [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]))
-)
+  (.addEventListener js/window
+                     "load"
+                     (fn [_event]
+                       (when @*url
+                         (js/setTimeout #(deeplink/deeplink @*url)
+                                        1000))))
 
-(defn init!
+  (.removeAllListeners mobile-util/file-sync)
+
+  (.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)))))
 
-  (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))))
+  (.addEventListener js/window "sendIntentReceived"
+                       #(intent/handle-received)))
 
-  (when (mobile-util/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))
+(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 App "appStateChange"
-                  (fn [^js state]
-                    (when (state/get-current-repo)
-                      (let [is-active? (.-isActive state)]
-                        (when is-active?
-                          (editor-handler/save-current-block!))))))
+  (.addListener mobile-util/fs-watcher "watcher"
+                (fn [event]
+                  (state/pub-event! [:file-watcher/changed event])))
 
-    (.addListener Keyboard "keyboardWillShow"
+  (.addListener Keyboard "keyboardWillShow"
                   (fn [^js info]
                     (let [keyboard-height (.-keyboardHeight info)]
                       (state/pub-event! [:mobile/keyboard-will-show keyboard-height]))))
 
-    (.addListener Keyboard "keyboardWillHide"
+  (.addListener Keyboard "keyboardWillHide"
                   (fn []
                     (state/pub-event! [:mobile/keyboard-will-hide])))
-    
-    (.addEventListener js/window "sendIntentReceived"
-                       #(intent/handle-received))))
+
+  (.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))
+  
+  (when (mobile-util/native-platform?)
+    (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)))

+ 13 - 10
src/main/frontend/mobile/intent.cljs

@@ -144,15 +144,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 +166,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)))))

+ 43 - 17
src/main/frontend/state.cljs

@@ -64,8 +64,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/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:
@@ -222,6 +223,8 @@
      :file-sync/sync-uploading-files        nil
      :file-sync/sync-downloading-files      nil
 
+     :file-sync/download-init-progress      nil
+
      :encryption/graph-parsing?             false
      })))
 
@@ -885,30 +888,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]
@@ -918,12 +936,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))
@@ -1664,6 +1676,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)] {}))

+ 6 - 0
src/main/frontend/ui.cljs

@@ -290,6 +290,12 @@
         (.appendChild js/document.head node))
       style)))
 
+(defn apply-custom-theme-effect! [theme]
+  (when-let [custom-theme (state/sub [:ui/custom-theme (keyword theme)])]
+    (js/LSPluginCore.selectTheme (bean/->js custom-theme)
+                                 (bean/->js {:effect false :emit false}))
+    (state/set-state! :plugin/selected-theme (:url custom-theme))))
+
 (defn setup-system-theme-effect!
   []
   (let [^js schemaMedia (js/window.matchMedia "(prefers-color-scheme: dark)")]

+ 1 - 1
src/main/frontend/util/property.cljs

@@ -232,7 +232,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)

+ 1 - 1
src/main/logseq/api.cljs

@@ -120,7 +120,7 @@
 
 (def ^:export set_theme_mode
   (fn [mode]
-    (state/set-theme! mode)))
+    (state/set-theme-mode! mode)))
 
 (def ^:export load_plugin_config
   (fn [path]

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