mirror of
https://github.com/logseq/logseq.git
synced 2026-04-25 06:35:02 +00:00
Refactor to use deps.edn
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,8 +11,10 @@ pom.xml.asc
|
||||
.hgignore
|
||||
.hg/
|
||||
|
||||
web/node_modules/
|
||||
web/public/js/main.js
|
||||
node_modules/
|
||||
resources/public/js/main.js
|
||||
resources/public/js/cljs-runtime
|
||||
resources/public/js/manifest.edn
|
||||
|
||||
/.cpcache
|
||||
/target
|
||||
|
||||
15
.projectile
15
.projectile
@@ -1,9 +1,8 @@
|
||||
-/.git
|
||||
-/api/.cpcache
|
||||
-/api/.shadow-cljs/
|
||||
-/api/node_modules/
|
||||
-/web/.cpcache
|
||||
-/web/.shadow-cljs/
|
||||
-/web/node_modules/
|
||||
-/web/public/js/cljs-runtime/
|
||||
-/web/public/js/main.js
|
||||
-/.cpcache
|
||||
-/.shadow-cljs/
|
||||
-/node_modules/
|
||||
-/.cpcache
|
||||
-/.shadow-cljs/
|
||||
-/resources/public/js/cljs-runtime/
|
||||
-/resources/public/js/main.js
|
||||
|
||||
4
bin/build
Executable file
4
bin/build
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
yarn install
|
||||
yarn release
|
||||
clj -A:uberdeps
|
||||
89
dev/user.clj
89
dev/user.clj
@@ -1,89 +0,0 @@
|
||||
(ns user
|
||||
(:require [com.stuartsierra.component :as component]
|
||||
[clojure.tools.namespace.repl :as namespace]
|
||||
[backend.config :as config]
|
||||
[backend.db-migrate :as migrate]
|
||||
[io.pedestal.service-tools.dev :as dev]
|
||||
[clj-time
|
||||
[coerce :as tc]
|
||||
[core :as t]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(namespace/disable-reload!)
|
||||
(namespace/set-refresh-dirs "src" "dev")
|
||||
(defonce *system (atom nil))
|
||||
(defonce *db (atom nil))
|
||||
|
||||
(defn migrate []
|
||||
(migrate/migrate @*db))
|
||||
|
||||
(defn rollback []
|
||||
(migrate/rollback @*db))
|
||||
|
||||
(defn stop []
|
||||
(some-> @*system (component/stop))
|
||||
(reset! *system nil))
|
||||
|
||||
(defn refresh []
|
||||
(let [res (namespace/refresh)]
|
||||
(when (not= res :ok)
|
||||
(throw res))
|
||||
:ok))
|
||||
|
||||
(defn go
|
||||
[]
|
||||
(require 'backend.core)
|
||||
(dev/watch)
|
||||
(when-some [f (resolve 'backend.system/new-system)]
|
||||
(when-some [system (f config/config)]
|
||||
(when-some [system' (component/start system)]
|
||||
(reset! *system system')
|
||||
(reset! *db {:datasource (get-in @*system [:hikari :datasource])}))))
|
||||
(migrate))
|
||||
|
||||
(defn reset []
|
||||
(stop)
|
||||
(refresh)
|
||||
(go))
|
||||
|
||||
(defn get-unix-timestamp []
|
||||
(tc/to-long (t/now)))
|
||||
|
||||
(def date-format
|
||||
"Format for DateTime"
|
||||
"yyyyMMddHHmmss")
|
||||
(def migrations-dir
|
||||
"Default migrations directory"
|
||||
"resources/migrations/")
|
||||
(def ragtime-format-edn
|
||||
"EDN template for SQL migrations"
|
||||
"{:up [\"\"]\n :down [\"\"]}")
|
||||
|
||||
(defn migrations-dir-exist?
|
||||
"Checks if 'resources/migrations' directory exists"
|
||||
[]
|
||||
(.isDirectory (io/file migrations-dir)))
|
||||
|
||||
(defn now
|
||||
"Gets the current DateTime" []
|
||||
(.format (java.text.SimpleDateFormat. date-format) (new java.util.Date)))
|
||||
|
||||
(defn migration-file-path
|
||||
"Complete migration file path"
|
||||
[name]
|
||||
(str migrations-dir (now) "_" (string/replace name #"\s+|-+|_+" "_") ".edn"))
|
||||
|
||||
(defn create-migration
|
||||
"Creates a migration file with the current DateTime"
|
||||
[name]
|
||||
(let [migration-file (migration-file-path name)]
|
||||
(if-not (migrations-dir-exist?)
|
||||
(io/make-parents migration-file))
|
||||
(spit migration-file ragtime-format-edn)))
|
||||
|
||||
(defn reset-db
|
||||
[]
|
||||
(dotimes [i 100]
|
||||
(rollback))
|
||||
(migrate))
|
||||
@@ -6,10 +6,9 @@
|
||||
"shadow-cljs": "2.8.81"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "npx shadow-cljs watch app",
|
||||
"release": "npx shadow-cljs release app",
|
||||
"server": "npx shadow-cljs server;",
|
||||
"clean": "rm -rf target; rm -rf public/js/compiled; rm -rf public/js/cljs-runtime"
|
||||
"watch": "clj -A:cljs watch app",
|
||||
"release": "clj -A:cljs release app",
|
||||
"clean": "rm -rf target; rm -rf resources/public/js/compiled; rm -rf resources/public/js/cljs-runtime"
|
||||
},
|
||||
"dependencies": {
|
||||
"browserfs": "^1.4.3",
|
||||
2
procfile
2
procfile
@@ -1 +1 @@
|
||||
web: java -Dclojure.main.report=stderr -cp target/uberjar/logseq.jar clojure.main -m backend.core
|
||||
web: java -Dclojure.main.report=stderr -cp target/logseq.jar clojure.main -m app.core
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
;; shadow-cljs configuration
|
||||
{:deps true
|
||||
|
||||
;; :dependencies
|
||||
;; [[binaryage/devtools "0.9.10"]
|
||||
;; [cider/cider-nrepl "0.23.0-SNAPSHOT"]
|
||||
|
||||
;; [rum "0.11.4"]
|
||||
;; [datascript "0.18.9"]
|
||||
;; [funcool/promesa "4.0.2"]
|
||||
;; [medley "1.2.0"]
|
||||
;; [metosin/reitit "0.3.10"]
|
||||
;; [metosin/reitit-spec "0.3.10"]
|
||||
;; [metosin/reitit-frontend "0.3.10"]]
|
||||
|
||||
:nrepl {:port 8701}
|
||||
|
||||
:builds
|
||||
@@ -20,7 +7,7 @@
|
||||
{:target :browser
|
||||
:modules {:main {:init-fn frontend.core/init}}
|
||||
|
||||
:output-dir "public/js"
|
||||
:output-dir "resources/public/js"
|
||||
:asset-path "/js"
|
||||
|
||||
:compiler-options {:infer-externs :auto
|
||||
@@ -35,11 +22,4 @@
|
||||
{:before-load frontend.core/stop
|
||||
;; after live-reloading finishes call this function
|
||||
:after-load frontend.core/start
|
||||
;; serve the public directory over http at port 8700
|
||||
;:http-root "public"
|
||||
;:http-port 8700
|
||||
;; :http-root "public"
|
||||
;; :http-port 8080
|
||||
:preloads [devtools.preload]}
|
||||
|
||||
}}}
|
||||
:preloads [devtools.preload]}}}}
|
||||
23
web/.gitignore
vendored
23
web/.gitignore
vendored
@@ -1,23 +0,0 @@
|
||||
node_modules/
|
||||
public/js/cljs-runtime
|
||||
public/js/main.js
|
||||
public/js/manifest.edn
|
||||
|
||||
/.cpcache
|
||||
/target
|
||||
/checkouts
|
||||
/src/gen
|
||||
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.iml
|
||||
*.jar
|
||||
*.log
|
||||
.shadow-cljs
|
||||
.idea
|
||||
.lein-*
|
||||
.nrepl-*
|
||||
.DS_Store
|
||||
|
||||
.hgignore
|
||||
.hg/
|
||||
16
web/deps.edn
16
web/deps.edn
@@ -1,16 +0,0 @@
|
||||
{:deps
|
||||
{;; dev
|
||||
thheller/shadow-cljs {:mvn/version "RELEASE"}
|
||||
cider/cider-nrepl {:mvn/version "0.23.0-SNAPSHOT"}
|
||||
binaryage/devtools {:mvn/version "0.9.10"}
|
||||
|
||||
rum {:mvn/version "0.11.4"}
|
||||
datascript-transit {:mvn/version "0.3.0"}
|
||||
funcool/promesa {:mvn/version "4.0.2"}
|
||||
medley {:mvn/version "1.2.0"}
|
||||
metosin/reitit-frontend {:mvn/version "0.3.10"}
|
||||
cljs-bean {:mvn/version "1.5.0"}}
|
||||
|
||||
:paths
|
||||
["src"
|
||||
"dev"]}
|
||||
@@ -1,31 +0,0 @@
|
||||
(ns shadow.hooks
|
||||
(:require [clojure.java.shell :refer [sh]]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; copied from https://gist.github.com/mhuebert/ba885b5e4f07923e21d1dc4642e2f182
|
||||
(defn exec [& cmd]
|
||||
(let [cmd (str/split (str/join " " (flatten cmd)) #"\s+")
|
||||
_ (println (str/join " " cmd))
|
||||
{:keys [exit out err]} (apply sh cmd)]
|
||||
(if (zero? exit)
|
||||
(when-not (str/blank? out)
|
||||
(println out))
|
||||
(println err))))
|
||||
|
||||
(defn purge-css
|
||||
{:shadow.build/stage :flush}
|
||||
[state {:keys [css-source
|
||||
js-globs
|
||||
public-dir]}]
|
||||
(case (:shadow.build/mode state)
|
||||
:release
|
||||
(exec "purgecss --css " css-source
|
||||
(for [content (if (string? js-globs) [js-globs] js-globs)]
|
||||
(str "--content " content))
|
||||
"-o" public-dir)
|
||||
|
||||
:dev
|
||||
(do
|
||||
(exec "mkdir -p" public-dir)
|
||||
(exec "cp" css-source (str public-dir "/" (last (str/split css-source #"/"))))))
|
||||
state)
|
||||
@@ -1,7 +0,0 @@
|
||||
(ns shadow.user
|
||||
(:require [shadow.cljs.devtools.api :as api]))
|
||||
|
||||
(defn cljs-repl
|
||||
[]
|
||||
(api/watch :app)
|
||||
(api/repl :app))
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
|
||||
Original highlight.js style (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
background: #F0F0F0;
|
||||
}
|
||||
|
||||
|
||||
/* Base color: saturation 0; */
|
||||
|
||||
.hljs,
|
||||
.hljs-subst {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.hljs-comment {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-attribute,
|
||||
.hljs-selector-tag,
|
||||
.hljs-meta-keyword,
|
||||
.hljs-doctag,
|
||||
.hljs-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/* User color: hue: 0 */
|
||||
|
||||
.hljs-type,
|
||||
.hljs-string,
|
||||
.hljs-number,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-quote,
|
||||
.hljs-template-tag,
|
||||
.hljs-deletion {
|
||||
color: #880000;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #880000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-symbol,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-link,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #BC6060;
|
||||
}
|
||||
|
||||
|
||||
/* Language color: hue: 90; */
|
||||
|
||||
.hljs-literal {
|
||||
color: #78A960;
|
||||
}
|
||||
|
||||
.hljs-builtin-name,
|
||||
.hljs-built_in,
|
||||
.hljs-bullet,
|
||||
.hljs-code,
|
||||
.hljs-addition {
|
||||
color: #397300;
|
||||
}
|
||||
|
||||
|
||||
/* Meta color: hue: 200 */
|
||||
|
||||
.hljs-meta {
|
||||
color: #1f7199;
|
||||
}
|
||||
|
||||
.hljs-meta-string {
|
||||
color: #4d99bf;
|
||||
}
|
||||
|
||||
|
||||
/* Misc effects */
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,278 +0,0 @@
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.space-between {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* copied from emacs org html exporter */
|
||||
.title { text-align: center;
|
||||
margin-bottom: .2em; }
|
||||
.subtitle { text-align: center;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
margin-top:0; }
|
||||
.timestamp { color: #bebebe; margin-left: 6px; }
|
||||
.timestamp-kwd { color: #5f9ea0; margin-left: 6px; }
|
||||
.org-right { margin-left: auto; margin-right: 0px; text-align: right; }
|
||||
.org-left { margin-left: 0px; margin-right: auto; text-align: left; }
|
||||
.org-center { margin-left: auto; margin-right: auto; text-align: center; }
|
||||
.underline { text-decoration: underline; }
|
||||
#postamble p, #preamble p { font-size: 90%; margin: .2em; }
|
||||
p.verse { margin-left: 3%; }
|
||||
pre {
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 3px 3px 3px #eee;
|
||||
padding: 8px;
|
||||
font-family: monospace;
|
||||
overflow: auto;
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
pre.src {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
padding-top: 1.2em;
|
||||
}
|
||||
pre.src:before {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
top: -10px;
|
||||
right: 10px;
|
||||
padding: 3px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
pre.src:hover:before { display: inline;}
|
||||
/* Languages per Org manual */
|
||||
pre.src-asymptote:before { content: 'Asymptote'; }
|
||||
pre.src-awk:before { content: 'Awk'; }
|
||||
pre.src-C:before { content: 'C'; }
|
||||
/* pre.src-C++ doesn't work in CSS */
|
||||
pre.src-clojure:before { content: 'Clojure'; }
|
||||
pre.src-css:before { content: 'CSS'; }
|
||||
pre.src-D:before { content: 'D'; }
|
||||
pre.src-ditaa:before { content: 'ditaa'; }
|
||||
pre.src-dot:before { content: 'Graphviz'; }
|
||||
pre.src-calc:before { content: 'Emacs Calc'; }
|
||||
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
|
||||
pre.src-fortran:before { content: 'Fortran'; }
|
||||
pre.src-gnuplot:before { content: 'gnuplot'; }
|
||||
pre.src-haskell:before { content: 'Haskell'; }
|
||||
pre.src-hledger:before { content: 'hledger'; }
|
||||
pre.src-java:before { content: 'Java'; }
|
||||
pre.src-js:before { content: 'Javascript'; }
|
||||
pre.src-latex:before { content: 'LaTeX'; }
|
||||
pre.src-ledger:before { content: 'Ledger'; }
|
||||
pre.src-lisp:before { content: 'Lisp'; }
|
||||
pre.src-lilypond:before { content: 'Lilypond'; }
|
||||
pre.src-lua:before { content: 'Lua'; }
|
||||
pre.src-matlab:before { content: 'MATLAB'; }
|
||||
pre.src-mscgen:before { content: 'Mscgen'; }
|
||||
pre.src-ocaml:before { content: 'Objective Caml'; }
|
||||
pre.src-octave:before { content: 'Octave'; }
|
||||
pre.src-org:before { content: 'Org mode'; }
|
||||
pre.src-oz:before { content: 'OZ'; }
|
||||
pre.src-plantuml:before { content: 'Plantuml'; }
|
||||
pre.src-processing:before { content: 'Processing.js'; }
|
||||
pre.src-python:before { content: 'Python'; }
|
||||
pre.src-R:before { content: 'R'; }
|
||||
pre.src-ruby:before { content: 'Ruby'; }
|
||||
pre.src-sass:before { content: 'Sass'; }
|
||||
pre.src-scheme:before { content: 'Scheme'; }
|
||||
pre.src-screen:before { content: 'Gnu Screen'; }
|
||||
pre.src-sed:before { content: 'Sed'; }
|
||||
pre.src-sh:before { content: 'shell'; }
|
||||
pre.src-sql:before { content: 'SQL'; }
|
||||
pre.src-sqlite:before { content: 'SQLite'; }
|
||||
/* additional languages in org.el's org-babel-load-languages alist */
|
||||
pre.src-forth:before { content: 'Forth'; }
|
||||
pre.src-io:before { content: 'IO'; }
|
||||
pre.src-J:before { content: 'J'; }
|
||||
pre.src-makefile:before { content: 'Makefile'; }
|
||||
pre.src-maxima:before { content: 'Maxima'; }
|
||||
pre.src-perl:before { content: 'Perl'; }
|
||||
pre.src-picolisp:before { content: 'Pico Lisp'; }
|
||||
pre.src-scala:before { content: 'Scala'; }
|
||||
pre.src-shell:before { content: 'Shell Script'; }
|
||||
pre.src-ebnf2ps:before { content: 'ebfn2ps'; }
|
||||
/* additional language identifiers per "defun org-babel-execute"
|
||||
in ob-*.el */
|
||||
pre.src-cpp:before { content: 'C++'; }
|
||||
pre.src-abc:before { content: 'ABC'; }
|
||||
pre.src-coq:before { content: 'Coq'; }
|
||||
pre.src-groovy:before { content: 'Groovy'; }
|
||||
/* additional language identifiers from org-babel-shell-names in
|
||||
ob-shell.el: ob-shell is the only babel language using a lambda to put
|
||||
the execution function name together. */
|
||||
pre.src-bash:before { content: 'bash'; }
|
||||
pre.src-csh:before { content: 'csh'; }
|
||||
pre.src-ash:before { content: 'ash'; }
|
||||
pre.src-dash:before { content: 'dash'; }
|
||||
pre.src-ksh:before { content: 'ksh'; }
|
||||
pre.src-mksh:before { content: 'mksh'; }
|
||||
pre.src-posh:before { content: 'posh'; }
|
||||
/* Additional Emacs modes also supported by the LaTeX listings package */
|
||||
pre.src-ada:before { content: 'Ada'; }
|
||||
pre.src-asm:before { content: 'Assembler'; }
|
||||
pre.src-caml:before { content: 'Caml'; }
|
||||
pre.src-delphi:before { content: 'Delphi'; }
|
||||
pre.src-html:before { content: 'HTML'; }
|
||||
pre.src-idl:before { content: 'IDL'; }
|
||||
pre.src-mercury:before { content: 'Mercury'; }
|
||||
pre.src-metapost:before { content: 'MetaPost'; }
|
||||
pre.src-modula-2:before { content: 'Modula-2'; }
|
||||
pre.src-pascal:before { content: 'Pascal'; }
|
||||
pre.src-ps:before { content: 'PostScript'; }
|
||||
pre.src-prolog:before { content: 'Prolog'; }
|
||||
pre.src-simula:before { content: 'Simula'; }
|
||||
pre.src-tcl:before { content: 'tcl'; }
|
||||
pre.src-tex:before { content: 'TeX'; }
|
||||
pre.src-plain-tex:before { content: 'Plain TeX'; }
|
||||
pre.src-verilog:before { content: 'Verilog'; }
|
||||
pre.src-vhdl:before { content: 'VHDL'; }
|
||||
pre.src-xml:before { content: 'XML'; }
|
||||
pre.src-nxml:before { content: 'XML'; }
|
||||
/* add a generic configuration mode; LaTeX export needs an additional
|
||||
(add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
|
||||
pre.src-conf:before { content: 'Configuration File'; }
|
||||
|
||||
table { border-collapse:collapse; }
|
||||
caption.t-above { caption-side: top; }
|
||||
caption.t-bottom { caption-side: bottom; }
|
||||
td, th { vertical-align:top; }
|
||||
th.org-right { text-align: center; }
|
||||
th.org-left { text-align: center; }
|
||||
th.org-center { text-align: center; }
|
||||
td.org-right { text-align: right; }
|
||||
td.org-left { text-align: left; }
|
||||
td.org-center { text-align: center; }
|
||||
dt { font-weight: bold; }
|
||||
.footpara { display: inline; }
|
||||
.footdef { margin-bottom: 1em; }
|
||||
.figure { padding: 1em; }
|
||||
.figure p { text-align: center; }
|
||||
.inlinetask {
|
||||
padding: 10px;
|
||||
border: 2px solid gray;
|
||||
margin: 10px;
|
||||
background: #ffffcc;
|
||||
}
|
||||
#org-div-home-and-up
|
||||
{ text-align: right; font-size: 70%; white-space: nowrap; }
|
||||
.linenr { font-size: smaller }
|
||||
.code-highlighted { background-color: #ffff00; }
|
||||
.org-info-js_info-navigation { border-style: none; }
|
||||
#org-info-js_console-label
|
||||
{ font-size: 10px; font-weight: bold; white-space: nowrap; }
|
||||
.org-info-js_search-highlight
|
||||
{ background-color: #ffff00; color: #000000; font-weight: bold; }
|
||||
.org-svg { width: 90%; }
|
||||
|
||||
.-mr-14 {
|
||||
margin-right: -3.5rem;
|
||||
}
|
||||
|
||||
/* loader */
|
||||
.loader {
|
||||
border-top-color: #3498db;
|
||||
-webkit-animation: spinner 1.5s linear infinite;
|
||||
animation: spinner 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spinner {
|
||||
0% { -webkit-transform: rotate(0deg); }
|
||||
100% { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* scroll-bg */
|
||||
.scroll-background {
|
||||
height: 400%; width: 400%; top: -25%; left: -100%; background-size: 800px auto; background-image: url('/img/hero-pattern-lg.png');
|
||||
}
|
||||
|
||||
.angled-background {
|
||||
background-image: url('/img/angled-background.svg'); background-size: 100% auto; background-position: -5px -5px;
|
||||
}
|
||||
|
||||
.scroll-background-2 {
|
||||
height: 800%; width: 400%; top: -100%; left: -100%; background-size: 400px auto; background-image: url('/img/hero-pattern-lg.png');
|
||||
}
|
||||
|
||||
@-webkit-keyframes scrollSmall {0%{transform:rotate(-13deg) translateY(0)}to{transform:rotate(-13deg) translateY(-639px)}}
|
||||
@keyframes scrollSmall{0%{transform:rotate(-13deg) translateY(0)}to{transform:rotate(-13deg) translateY(-639px)}}
|
||||
@-webkit-keyframes scrollLarge{0%{transform:rotate(-13deg) translateY(0)}to{transform:rotate(-13deg) translateY(-1278px)}}
|
||||
@keyframes scrollLarge{0%{transform:rotate(-13deg) translateY(0)}to{transform:rotate(-13deg) translateY(-1278px)}}
|
||||
@-webkit-keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(359deg)}}
|
||||
@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(359deg)}}
|
||||
@-webkit-keyframes pulse{0%{opacity:1}50%{opacity:.5}to{opacity:1}}
|
||||
@keyframes pulse{0%{opacity:1}50%{opacity:.5}to{opacity:1}}
|
||||
.spin{-webkit-animation:spin .5s linear infinite;animation:spin .5s linear infinite}
|
||||
.pulse{-webkit-animation:pulse 2s ease infinite;animation:pulse 2s ease infinite}
|
||||
.scroll-bg{-webkit-animation-name:scrollSmall;animation-name:scrollSmall;-webkit-animation-duration:15s;animation-duration:15s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}
|
||||
@media (min-width:1024px){.scroll-bg{-webkit-animation-name:scrollLarge;animation-name:scrollLarge;-webkit-animation-duration:35s;animation-duration:35s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(39,41,43,.15);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content ol, .content ui {
|
||||
list-style: disc;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.content p, .content div {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#journals .journal:first-child {
|
||||
border-top: none;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
#journals .journal {
|
||||
border-top: 1px solid #738694;
|
||||
padding: 48px 0;
|
||||
margin: 24px 0 48px 0;
|
||||
}
|
||||
|
||||
#journals {
|
||||
margin-bottom: 300px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
}
|
||||
1
web/public/css/tailwind.min.css
vendored
1
web/public/css/tailwind.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 100 1200" fill="#161e2e" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="0,0 100,0 0,1200"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 125 B |
Binary file not shown.
|
Before Width: | Height: | Size: 396 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
File diff suppressed because one or more lines are too long
@@ -1,29 +0,0 @@
|
||||
(ns frontend.blob)
|
||||
|
||||
(defn- decode
|
||||
"Decodes the data portion of a data url from base64"
|
||||
[[media-type data]]
|
||||
[media-type (js/atob data)])
|
||||
|
||||
(defn- uint8
|
||||
"Converts a base64 decoded data string to a Uint8Array"
|
||||
[[media-type data]]
|
||||
(->> (map #(.charCodeAt %1) data)
|
||||
js/Uint8Array.
|
||||
(vector media-type)))
|
||||
|
||||
(defn- make-blob
|
||||
"Creates a JS Blob object from a media type and a Uint8Array"
|
||||
[[media-type uint8]]
|
||||
(js/Blob. (array uint8) (js-obj "type" media-type)))
|
||||
|
||||
(defn blob
|
||||
"Converts a data-url into a JS Blob. This is useful for uploading
|
||||
image data from JavaScript."
|
||||
[data-url]
|
||||
{:pre [(string? data-url)]}
|
||||
(-> (re-find #"^data:([^;]+);base64,(.*)$" data-url)
|
||||
rest
|
||||
decode
|
||||
uint8
|
||||
make-blob))
|
||||
@@ -1,104 +0,0 @@
|
||||
(ns frontend.components.agenda
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.format.org.block :as block]
|
||||
[frontend.state :as state]
|
||||
[clojure.string :as string]
|
||||
[frontend.format.org-mode :as org]
|
||||
[frontend.components.sidebar :as sidebar]
|
||||
[frontend.db :as db]
|
||||
[frontend.ui :as ui]))
|
||||
|
||||
(rum/defc timestamps-cp
|
||||
[timestamps]
|
||||
[:ul
|
||||
(for [[type {:keys [date time]}] timestamps]
|
||||
(let [{:keys [year month day]} date
|
||||
{:keys [hour min]} time]
|
||||
[:li {:key type}
|
||||
[:span {:style {:margin-right 6}} type]
|
||||
[:span (if time
|
||||
(str year "-" month "-" day " " hour ":" min)
|
||||
(str year "-" month "-" day))]]))])
|
||||
|
||||
(rum/defc title-cp
|
||||
[title]
|
||||
(let [title-json (js/JSON.stringify (clj->js title))
|
||||
html (org/inline-list->html title-json)]
|
||||
(util/raw-html html)))
|
||||
|
||||
(rum/defc children-cp
|
||||
[children]
|
||||
(let [children-json (js/JSON.stringify (clj->js children))
|
||||
html (org/json->html children-json)]
|
||||
(util/raw-html html)))
|
||||
|
||||
(rum/defc marker-cp
|
||||
[marker]
|
||||
(if marker
|
||||
[:span {:class (str "marker-" (string/lower-case marker))
|
||||
:style {:margin-left 8}}
|
||||
(if (contains? #{"DOING" "IN-PROGRESS"} marker)
|
||||
(str " (" marker ")"))]))
|
||||
|
||||
(rum/defc tags-cp
|
||||
[tags]
|
||||
[:span
|
||||
(for [{:keys [tag/name]} tags]
|
||||
[:span.tag {:key name}
|
||||
[:span
|
||||
name]])])
|
||||
|
||||
(rum/defc agenda
|
||||
[]
|
||||
(let [tasks (db/get-agenda)]
|
||||
(sidebar/sidebar
|
||||
[:div#agenda
|
||||
[:h2.mb-3 "Agenda"]
|
||||
(if (seq tasks)
|
||||
[:div.ml-1
|
||||
(let [tasks (block/sort-tasks tasks)]
|
||||
(for [{:heading/keys [uuid marker title priority level tags children timestamps meta repo file] :as task} tasks]
|
||||
[:div.mb-2
|
||||
{:key (str "task-" uuid)
|
||||
:style {:padding-left 8
|
||||
:padding-right 8}}
|
||||
[:div.column
|
||||
[:div.row {:style {:align-items "center"}}
|
||||
(case marker
|
||||
(list "DOING" "IN-PROGRESS" "TODO")
|
||||
(ui/checkbox {:on-change (fn [_]
|
||||
;; FIXME: Log timestamp
|
||||
(handler/check task))})
|
||||
|
||||
"WAIT"
|
||||
[:span {:style {:font-weight "bold"}}
|
||||
"WAIT"]
|
||||
|
||||
"DONE"
|
||||
(ui/checkbox {:checked true
|
||||
:on-change (fn [_]
|
||||
;; FIXME: Log timestamp
|
||||
(handler/uncheck task)
|
||||
)})
|
||||
|
||||
nil)
|
||||
[:div.row.ml-2
|
||||
(if priority
|
||||
[:span.priority.mr-1
|
||||
(str "#[" priority "]")])
|
||||
(title-cp title)
|
||||
(marker-cp marker)
|
||||
(when (seq tags)
|
||||
(tags-cp tags))]]
|
||||
(when (seq timestamps)
|
||||
(timestamps-cp timestamps))
|
||||
|
||||
;; FIXME: parse error
|
||||
;; (when (seq children)
|
||||
;; (children-cp children))
|
||||
|
||||
]]
|
||||
))]
|
||||
"Empty")])))
|
||||
@@ -1,27 +0,0 @@
|
||||
(ns frontend.components.content
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.format :as format]
|
||||
[frontend.format.org-mode :as org]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.util :as util]))
|
||||
|
||||
(defn- highlight!
|
||||
[]
|
||||
(doseq [block (-> (js/document.querySelectorAll "pre code")
|
||||
(array-seq))]
|
||||
(js/hljs.highlightBlock block)))
|
||||
|
||||
(rum/defc html <
|
||||
{:did-mount (fn [state]
|
||||
(highlight!)
|
||||
(handler/render-local-images!)
|
||||
state)
|
||||
:did-update (fn [state]
|
||||
(highlight!)
|
||||
state)}
|
||||
[content format config]
|
||||
(case format
|
||||
(list :png :jpg :jpeg)
|
||||
content
|
||||
(util/raw-html (format/to-html content format
|
||||
config))))
|
||||
@@ -1,86 +0,0 @@
|
||||
(ns frontend.components.file
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[clojure.string :as string]
|
||||
[frontend.db :as db]
|
||||
[frontend.components.sidebar :as sidebar]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.format :as format]
|
||||
[frontend.format.org-mode :as org]
|
||||
[frontend.components.content :as content]
|
||||
[goog.crypt.base64 :as b64]))
|
||||
|
||||
(defn- get-path
|
||||
[state]
|
||||
(let [route-match (first (:rum/args state))
|
||||
encoded-path (get-in route-match [:parameters :path :path])
|
||||
decoded-path (b64/decodeString encoded-path)]
|
||||
[encoded-path decoded-path]))
|
||||
|
||||
(rum/defcs file <
|
||||
[state]
|
||||
(let [[encoded-path path] (get-path state)
|
||||
suffix (keyword (string/lower-case (last (string/split path #"\."))))]
|
||||
(sidebar/sidebar
|
||||
(cond
|
||||
(and suffix (contains? #{:md :markdown :org} suffix))
|
||||
[:div.content
|
||||
[:a {:href (str "/file/" encoded-path "/edit")}
|
||||
"edit"]
|
||||
(let [content (db/get-file (last (get-path state)))]
|
||||
(cond
|
||||
(string/blank? content)
|
||||
[:span]
|
||||
|
||||
content
|
||||
(content/html content suffix org/default-config)
|
||||
|
||||
:else
|
||||
"Loading ..."))]
|
||||
|
||||
;; image type
|
||||
(and suffix (contains? #{:png :jpg :jpeg} suffix))
|
||||
(content/html [:img {:src path}] suffix org/default-config)
|
||||
|
||||
:else
|
||||
[:div "Format ." (name suffix) " is not supported."]))))
|
||||
|
||||
(defn- count-newlines
|
||||
[s]
|
||||
(count (re-seq #"\n" (or s ""))))
|
||||
|
||||
(rum/defcs edit <
|
||||
(rum/local nil ::content)
|
||||
(rum/local "" ::commit-message)
|
||||
{:will-mount (fn [state]
|
||||
(assoc state ::initial-content (db/get-file (last (get-path state)))))}
|
||||
[state]
|
||||
(let [initial-content (get state ::initial-content)
|
||||
initial-rows (+ 3 (count-newlines initial-content))
|
||||
content (get state ::content)
|
||||
commit-message (get state ::commit-message)
|
||||
rows (if (nil? @content) initial-rows (+ 3 (count-newlines @content)))
|
||||
[_encoded-path path] (get-path state)]
|
||||
(prn {:rows rows})
|
||||
(sidebar/sidebar
|
||||
[:div.content
|
||||
[:h3.mb-2 (str "Update " path)]
|
||||
[:textarea
|
||||
{:rows rows
|
||||
:default-value initial-content
|
||||
:on-change #(reset! content (.. % -target -value))
|
||||
:auto-focus true}]
|
||||
[:div.mt-1.mb-1.relative.rounded-md.shadow-sm
|
||||
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5
|
||||
{:placeholder "Commit message"
|
||||
:on-change (fn [e]
|
||||
(reset! commit-message (util/evalue e)))}]]
|
||||
(ui/button "Save" (fn []
|
||||
(when (and (not (string/blank? @content))
|
||||
(not (= initial-content
|
||||
@content)))
|
||||
(let [commit-message (if (string/blank? @commit-message)
|
||||
(str "Update " path)
|
||||
@commit-message)]
|
||||
(handler/alter-file path commit-message @content)))))])))
|
||||
@@ -1,81 +0,0 @@
|
||||
(ns frontend.components.home
|
||||
(:require [frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.mixins :as mixins]
|
||||
[frontend.config :as config]
|
||||
[rum.core :as rum]
|
||||
[frontend.format :as format]
|
||||
[clojure.string :as string]
|
||||
[frontend.db :as db]
|
||||
[frontend.components.sidebar :as sidebar]))
|
||||
|
||||
(rum/defc front-page
|
||||
[]
|
||||
[:div.relative.min-h-screen.overflow-hidden.bg-gray-900.lg:bg-gray-300
|
||||
[:div.hidden.lg:block.absolute.scroll-bg.scroll-background]
|
||||
[:div.angled-background
|
||||
{:class (util/hiccup->class ".relative.min-h-screen.lg:min-w-3xl.xl:min-w-4xl.lg:flex.lg:items-center.lg:justify-center.lg:w-3/5.lg:py-20.lg:pl-8.lg:pr-8.bg-no-repeat")}
|
||||
[:div
|
||||
[:div.px-6.pt-8.pb-12.md:max-w-3xl.md:mx-auto.lg:mx-0.lg:max-w-none.lg:pt-0.lg:pb-16
|
||||
[:div.flex.items-center.justify-between
|
||||
[:div
|
||||
[:img.h-6.lg:h-8.xl:h-9
|
||||
{:alt "Logseq",
|
||||
:src "/img/logo.png"}]]
|
||||
[:div
|
||||
[:a.text-sm.font-semibold.text-white.focus:outline-none.focus:underline
|
||||
{:href "/login/github"}
|
||||
"Login →"]]]]
|
||||
[:div.px-6.md:max-w-3xl.md:mx-auto.lg:mx-0.lg:max-w-none
|
||||
[:p.text-sm.font-semibold.text-gray-300.uppercase.tracking-wider
|
||||
"\n Now in early access\n "]
|
||||
[:h1.mt-3.text-3xl.leading-9.font-semibold.font-display.text-white.sm:mt-6.sm:text-4xl.sm:leading-10.xl:text-5xl.xl:leading-none
|
||||
"\n Beautiful UI components, crafted\n "
|
||||
[:br.hidden.sm:inline]
|
||||
[:span.text-teal-400
|
||||
"\n by the creators of Tailwind CSS.\n "]]
|
||||
[:p.mt-2.text-lg.leading-7.text-gray-300.sm:mt-3.sm:text-xl.sm:max-w-xl.xl:mt-4.xl:text-2xl.xl:max-w-2xl
|
||||
"\n Fully responsive HTML components, designed and developed by Adam Wathan and Steve Schoger.\n "]
|
||||
[:div.mt-6.sm:flex.sm:mt-8.xl:mt-12
|
||||
[:a.w-full.sm:w-auto.inline-flex.items-center.justify-center.px-6.py-3.border.border-transparent.text-base.leading-6.font-semibold.rounded-md.text-gray-900.bg-white.shadow-sm.hover:text-gray-600.focus:outline-none.focus:text-gray-600.transition.ease-in-out.duration-150.xl:text-lg.xl:py-4
|
||||
{:href (str config/api "login/github")}
|
||||
"Login with Github"]
|
||||
[:a.mt-4.sm:ml-4.sm:mt-0.w-full.sm:w-auto.inline-flex.items-center.justify-center.px-6.py-3.border.border-transparent.text-base.leading-6.font-semibold.rounded-md.text-white.bg-gray-800.shadow-sm.hover:bg-gray-700.focus:outline-none.focus:bg-gray-700.transition.ease-in-out.duration-150.xl:text-lg.xl:py-4
|
||||
{:href "/demo"}
|
||||
"Live Demo"]]]
|
||||
[:div.mt-8.sm:mt-12.relative.h-64.overflow-hidden.bg-gray-300.lg:hidden
|
||||
[:div.absolute.scroll-bg.scroll-background-2]]
|
||||
[:div.px-6.py-8.sm:pt-12.md:max-w-3xl.md:mx-auto.lg:mx-0.lg:max-w-full.lg:py-0.lg:pt-24
|
||||
[:p.text-sm.font-semibold.text-gray-300.uppercase.tracking-wider
|
||||
"Designed and developed by"]
|
||||
[:div.mt-4.sm:flex
|
||||
[:a.flex.items-center.no-underline
|
||||
{:href "https://twitter.com/adamwathan"}]
|
||||
[:div.flex-shrink-0
|
||||
[:img.h-12.w-12.rounded-full.border-2.border-white
|
||||
{:alt "", :src "/img/adam.jpg"}]]
|
||||
[:div.ml-3
|
||||
[:p.font-semibold.text-white.leading-tight "Adam Wathan"]
|
||||
[:p.text-sm.text-gray-500.leading-tight
|
||||
"Creator of Tailwind CSS"]]
|
||||
[:a.mt-6.sm:mt-0.sm:ml-12.flex.items-center.no-underline
|
||||
{:href "https://twitter.com/steveschoger"}]
|
||||
[:div.flex-shrink-0
|
||||
[:img.h-12.w-12.rounded-full.border-2.border-white
|
||||
{:alt "", :src "/img/steve.jpg"}]]
|
||||
[:div.ml-3
|
||||
[:p.font-semibold.text-white.leading-tight "Steve Schoger"]
|
||||
[:p.text-sm.text-gray-500.leading-tight
|
||||
"Author of Refactoring UI"]]]]]]])
|
||||
|
||||
(rum/defc home <
|
||||
{:will-mount (fn [state]
|
||||
(when-not (db/get-github-token)
|
||||
(handler/get-github-access-token))
|
||||
state)}
|
||||
[state]
|
||||
(if (db/get-github-token)
|
||||
(sidebar/sidebar (sidebar/main-content))
|
||||
(front-page)))
|
||||
@@ -1,105 +0,0 @@
|
||||
(ns frontend.components.journal
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[clojure.string :as string]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.format :as format]
|
||||
[frontend.mixins :as mixins]
|
||||
[frontend.db :as db]
|
||||
[frontend.state :as state]
|
||||
[frontend.format.org-mode :as org]
|
||||
[goog.object :as gobj]
|
||||
[frontend.image :as image]
|
||||
[frontend.components.content :as content]))
|
||||
|
||||
(def edit-content (atom ""))
|
||||
(rum/defc editor-box <
|
||||
(mixins/event-mixin
|
||||
(fn [state]
|
||||
(let [heading (first (:rum/args state))]
|
||||
(mixins/hide-when-esc-or-outside
|
||||
state
|
||||
nil
|
||||
:show-fn (fn []
|
||||
(:edit? @state/state))
|
||||
:on-hide (fn []
|
||||
(handler/save-current-edit-journal! (str heading "\n" @edit-content)))))))
|
||||
[heading content]
|
||||
[:div.flex-1
|
||||
(ui/textarea-autosize
|
||||
{:on-change (fn [e]
|
||||
(reset! edit-content (util/evalue e)))
|
||||
:default-value content
|
||||
:auto-focus true
|
||||
:style {:border "none"
|
||||
:border-radius 0
|
||||
:background "transparent"
|
||||
:margin-top 12.5}})
|
||||
[:input
|
||||
{:id "files"
|
||||
:type "file"
|
||||
:on-change (fn [e]
|
||||
(let [files (.-files (.-target e))]
|
||||
(image/upload
|
||||
files
|
||||
(fn [file file-form-data file-name file-type]
|
||||
;; TODO: set uploading
|
||||
(.append file-form-data "name" file-name)
|
||||
(.append file-form-data file-type true)
|
||||
|
||||
;; (citrus/dispatch!
|
||||
;; :image/upload
|
||||
;; file-form-data
|
||||
;; (fn [url]
|
||||
;; (reset! uploading? false)
|
||||
;; (swap! form assoc name url)
|
||||
;; (if on-uploaded
|
||||
;; (on-uploaded form name url))))
|
||||
))))
|
||||
;; :hidden true
|
||||
}]])
|
||||
|
||||
(defn split-first [re s]
|
||||
(clojure.string/split s re 2))
|
||||
|
||||
(defn- split-heading-body
|
||||
[content]
|
||||
(let [result (split-first #"\n" content)]
|
||||
(if (= 1 (count result))
|
||||
[result ""]
|
||||
result)))
|
||||
|
||||
(rum/defc journal-cp < rum/reactive
|
||||
[{:keys [uuid title content] :as journal}]
|
||||
(let [{:keys [edit? edit-journal]} (rum/react state/state)
|
||||
[heading content] (split-heading-body content)]
|
||||
[:div.flex-1
|
||||
[:h1.text-gray-600 {:style {:font-weight "450"}}
|
||||
title]
|
||||
|
||||
(if (and edit? (= uuid (:uuid edit-journal)))
|
||||
(editor-box heading content)
|
||||
[:div {:on-click (fn []
|
||||
(handler/edit-journal! content journal)
|
||||
(reset! edit-content content))
|
||||
:style {:padding 8
|
||||
:min-height 200}}
|
||||
(if (or (not content)
|
||||
(string/blank? content))
|
||||
[:div]
|
||||
(content/html content "org" org/config-with-line-break))])]))
|
||||
|
||||
(rum/defcs journals < rum/reactive
|
||||
{:will-mount (fn [state]
|
||||
(handler/set-latest-journals!)
|
||||
state)}
|
||||
[state]
|
||||
(let [{:keys [latest-journals]} (rum/react state/state)]
|
||||
[:div#journals
|
||||
(ui/infinite-list
|
||||
(for [journal latest-journals]
|
||||
[:div.journal.content {:key (cljs.core/random-uuid)}
|
||||
(journal-cp journal)])
|
||||
{:on-load (fn []
|
||||
(handler/load-more-journals!))})]))
|
||||
@@ -1,42 +0,0 @@
|
||||
(ns frontend.components.repo
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler :as handler]
|
||||
[clojure.string :as string]
|
||||
[frontend.ui :as ui]))
|
||||
|
||||
(defn repos
|
||||
[repos]
|
||||
(when (seq repos)
|
||||
[:div#repos
|
||||
[:ul
|
||||
(for [url repos]
|
||||
[:li {:key url}
|
||||
[:button {:on-click (fn []
|
||||
;; (handler/set-current-repo url)
|
||||
)}
|
||||
(string/replace url "https://github.com/" "")]])]]))
|
||||
|
||||
(rum/defcs add-repo < (rum/local "https://github.com/" ::repo-url)
|
||||
[state]
|
||||
(let [prefix "https://github.com/"
|
||||
repo-url (get state ::repo-url)]
|
||||
[:div.p-8.flex.items-center.justify-center.bg-white
|
||||
[:div.w-full.max-w-xs.mx-auto
|
||||
[:div
|
||||
[:div
|
||||
[:h2 "Specify your repo:"]
|
||||
[:div.mt-2.mb-2.relative.rounded-md.shadow-sm
|
||||
[:div.absolute.inset-y-0.left-0.pl-3.flex.items-center.pointer-events-none
|
||||
[:span.text-gray-500.sm:text-sm.sm:leading-5
|
||||
prefix]]
|
||||
[:input#repo.form-input.block.w-full.pl-16.sm:pl-14.sm:text-sm.sm:leading-5
|
||||
{:autoFocus true
|
||||
:placeholder "username/repo"
|
||||
:on-change (fn [e]
|
||||
(reset! repo-url (util/evalue e)))
|
||||
:style {:padding-left "9.1em"}}]]]]
|
||||
(ui/button
|
||||
"Clone"
|
||||
(fn []
|
||||
(handler/clone-and-pull (str prefix @repo-url))))]]))
|
||||
@@ -1,51 +0,0 @@
|
||||
(ns frontend.components.settings
|
||||
;; (:require [rum.core :as rum]
|
||||
;; [frontend.mui :as mui]
|
||||
;; [frontend.util :as util]
|
||||
;; [frontend.state :as state]
|
||||
;; [frontend.handler :as handler]
|
||||
;; [clojure.string :as string])
|
||||
)
|
||||
|
||||
;; (defn settings-form
|
||||
;; [github-token github-repo]
|
||||
;; [:form {:style {:min-width 300}}
|
||||
;; (mui/grid
|
||||
;; {:container true
|
||||
;; :direction "column"}
|
||||
;; (mui/text-field {:id "standard-basic"
|
||||
;; :style {:margin-bottom 12}
|
||||
;; :label "Github repo"
|
||||
;; :on-change (fn [event]
|
||||
;; (let [v (util/evalue event)]
|
||||
;; (swap! state/state assoc :github-repo v)))
|
||||
;; :value github-repo
|
||||
;; })
|
||||
;; (mui/button {:variant "contained"
|
||||
;; :color "primary"
|
||||
;; :on-click (fn []
|
||||
;; (when (and github-token github-repo)
|
||||
;; (handler/clone github-token github-repo)))}
|
||||
;; "Sync"))])
|
||||
|
||||
;; (rum/defc settings < rum/reactive
|
||||
;; []
|
||||
;; ;; Change repo and basic token
|
||||
;; (let [state (rum/react state/state)
|
||||
;; {:keys [github-token github-repo]} state]
|
||||
;; (mui/container
|
||||
;; {:id "root-container"
|
||||
;; :style {:display "flex"
|
||||
;; :justify-content "center"
|
||||
;; :margin-top 64}}
|
||||
|
||||
;; [:div
|
||||
|
||||
;; (settings-form github-token github-repo)
|
||||
|
||||
;; (mui/divider {:style {:margin "24px 0"}})
|
||||
|
||||
;; ;; clear storage
|
||||
;; (mui/button {:on-click handler/clear-storage
|
||||
;; :color "primary"}
|
||||
;; "Clear storage and clone")])))
|
||||
@@ -1,157 +0,0 @@
|
||||
(ns frontend.components.sidebar
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.mixins :as mixins]
|
||||
[frontend.db :as db]
|
||||
[frontend.components.repo :as repo]
|
||||
[frontend.components.journal :as journal]
|
||||
[goog.crypt.base64 :as b64]
|
||||
[frontend.util :as util]
|
||||
[frontend.state :as state]))
|
||||
|
||||
(defonce active-button :a.group.flex.items-center.px-2.py-2.text-base.leading-6.font-medium.rounded-md.text-white.bg-gray-900.focus:outline-none.focus:bg-gray-700.transition.ease-in-out.duration-150)
|
||||
(defonce inactive-button :a.mt-1.group.flex.items-center.px-2.py-2.text-base.leading-6.font-medium.rounded-md.text-gray-300.hover:text-white.hover:bg-gray-700.focus:outline-none.focus:text-white.focus:bg-gray-700.transition.ease-in-out.duration-150)
|
||||
|
||||
(defn nav-item
|
||||
([title href svg-d]
|
||||
(nav-item title href svg-d false))
|
||||
([title href svg-d active?]
|
||||
(let [a (if active? active-button inactive-button)]
|
||||
[a {:href href}
|
||||
[:svg.mr-4.h-6.w-6.text-gray-400.group-hover:text-gray-300.group-focus:text-gray-300.transition.ease-in-out.duration-150
|
||||
{:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
|
||||
[:path
|
||||
{:d svg-d
|
||||
:stroke-width "2",
|
||||
:stroke-linejoin "round",
|
||||
:stroke-linecap "round"}]]
|
||||
title])))
|
||||
|
||||
(rum/defc files-list
|
||||
[file-active?]
|
||||
(let [files (db/get-files)]
|
||||
[:div.cursor-pointer.my-1.flex.flex-col.ml-2
|
||||
(if (seq files)
|
||||
(for [file files]
|
||||
(let [encoded-path (b64/encodeString file)]
|
||||
[:a {:key file
|
||||
:class (util/hiccup->class "mt-1.group.flex.items-center.px-2.py-1.text-base.leading-6.font-medium.rounded-md.text-gray-500.hover:text-white.hover:bg-gray-700.focus:outline-none.focus:text-white.focus:bg-gray-700.transition.ease-in-out.duration-150")
|
||||
:style {:color (if (file-active? encoded-path) "#FFF")}
|
||||
:href (str "/file/" encoded-path)}
|
||||
file])))]))
|
||||
|
||||
(rum/defc sidebar-nav < rum/reactive
|
||||
[]
|
||||
(let [{:keys [:route-match]} (rum/react state/state)
|
||||
active? (fn [route] (= route (get-in route-match [:data :name])))
|
||||
file-active? (fn [path]
|
||||
(= path (get-in route-match [:parameters :path :path])))]
|
||||
[:nav.flex-1.px-2.py-4.bg-gray-800
|
||||
(nav-item "Journals" "/"
|
||||
"M3 12l9-9 9 9M5 10v10a1 1 0 001 1h3a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1h3a1 1 0 001-1V10M9 21h6"
|
||||
(active? :home))
|
||||
(nav-item "Agenda" "/agenda"
|
||||
"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
(active? :agenda))
|
||||
(files-list file-active?)]))
|
||||
|
||||
(rum/defc main-content
|
||||
[]
|
||||
(let [repos (db/get-repos)]
|
||||
[:div.max-w-7xl.mx-auto.px-4.sm:px-6.md:px-8
|
||||
(if (seq repos)
|
||||
(journal/journals)
|
||||
(repo/add-repo))]))
|
||||
|
||||
(rum/defcs sidebar < (mixins/modal)
|
||||
[state main-content]
|
||||
(let [{:keys [open? close-fn open-fn]} state]
|
||||
[:div.h-screen.flex.overflow-hidden.bg-gray-100
|
||||
[:div.md:hidden
|
||||
[:div.fixed.inset-0.z-30.bg-gray-600.opacity-0.pointer-events-none.transition-opacity.ease-linear.duration-300
|
||||
{:class (if @open?
|
||||
"opacity-75 pointer-events-auto"
|
||||
"opacity-0 pointer-events-none")
|
||||
:on-click close-fn}]
|
||||
[:div.fixed.inset-y-0.left-0.flex.flex-col.z-40.max-w-xs.w-full.bg-gray-800.transform.ease-in-out.duration-300
|
||||
{:class (if @open?
|
||||
"translate-x-0"
|
||||
"-translate-x-full")}
|
||||
(if @open?
|
||||
[:div.absolute.top-0.right-0.-mr-14.p-1
|
||||
[:button.flex.items-center.justify-center.h-12.w-12.rounded-full.focus:outline-none.focus:bg-gray-600
|
||||
{:on-click close-fn}
|
||||
[:svg.h-6.w-6.text-white
|
||||
{:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
|
||||
[:path
|
||||
{:d "M6 18L18 6M6 6l12 12",
|
||||
:stroke-width "2",
|
||||
:stroke-linejoin "round",
|
||||
:stroke-linecap "round"}]]]])
|
||||
[:div.flex-shrink-0.flex.items-center.h-16.px-4.bg-gray-900
|
||||
[:img.h-8.w-auto
|
||||
{:alt "Logseq",
|
||||
:src "/img/logo.png"}]]
|
||||
[:div.flex-1.h-0.overflow-y-auto
|
||||
(sidebar-nav)]
|
||||
]]
|
||||
[:div.hidden.md:flex.md:flex-shrink-0
|
||||
[:div.flex.flex-col.w-64
|
||||
[:div.flex.items-center.h-16.flex-shrink-0.px-4.bg-gray-900
|
||||
[:img.h-8.w-auto
|
||||
{:alt "Logseq",
|
||||
:src "/img/logo.png"}]]
|
||||
[:div.h-0.flex-1.flex.flex-col.overflow-y-auto
|
||||
(sidebar-nav)]]]
|
||||
[:div.flex.flex-col.w-0.flex-1.overflow-hidden
|
||||
[:div.relative.z-10.flex-shrink-0.flex.h-16.bg-white.shadow
|
||||
[:button.px-4.border-r.border-gray-200.text-gray-500.focus:outline-none.focus:bg-gray-100.focus:text-gray-600.md:hidden
|
||||
{:on-click open-fn}
|
||||
[:svg.h-6.w-6
|
||||
{:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
|
||||
[:path
|
||||
{:d "M4 6h16M4 12h16M4 18h7",
|
||||
:stroke-width "2",
|
||||
:stroke-linejoin "round",
|
||||
:stroke-linecap "round"}]]]
|
||||
[:div.flex-1.px-4.flex.justify-between
|
||||
[:div.flex-1.flex
|
||||
[:div.w-full.flex.md:ml-0
|
||||
[:label.sr-only {:for "search_field"} "Search"]
|
||||
[:div.relative.w-full.text-gray-400.focus-within:text-gray-600
|
||||
[:div.absolute.inset-y-0.left-0.flex.items-center.pointer-events-none
|
||||
[:svg.h-5.w-5
|
||||
{:viewBox "0 0 20 20", :fill "currentColor"}
|
||||
[:path
|
||||
{:d
|
||||
"M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z",
|
||||
:clip-rule "evenodd",
|
||||
:fill-rule "evenodd"}]]]
|
||||
[:input#search_field.block.w-full.h-full.pl-8.pr-3.py-2.rounded-md.text-gray-900.placeholder-gray-500.focus:outline-none.focus:placeholder-gray-400.sm:text-sm
|
||||
{:placeholder "Search"}]]]]
|
||||
[:div.ml-4.flex.items-center.md:ml-6
|
||||
[:button.p-1.text-gray-400.rounded-full.hover:bg-gray-100.hover:text-gray-500.focus:outline-none.focus:shadow-outline.focus:text-gray-500
|
||||
[:svg.h-6.w-6
|
||||
{:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
|
||||
[:path
|
||||
{:d
|
||||
"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9",
|
||||
:stroke-width "2",
|
||||
:stroke-linejoin "round",
|
||||
:stroke-linecap "round"}]]]
|
||||
(ui/dropdown-with-links
|
||||
[{:title "Your Profile"
|
||||
:options {:href "#"}}
|
||||
{:title "Settings"
|
||||
:options {:href "#"}}
|
||||
{:title "Sign out"
|
||||
:options {:href "#"}}])]]]
|
||||
[:main.flex-1.relative.z-0.overflow-y-auto.py-6.focus:outline-none
|
||||
;; {:x-init "$el.focus()", :x-data "x-data", :tabindex "0"}
|
||||
{:tabIndex "0"}
|
||||
[:div.flex.justify-center
|
||||
[:div.flex-1.m-6 {:style {:position "relative"
|
||||
:max-width 800}}
|
||||
main-content]]]
|
||||
|
||||
(ui/notification)]]))
|
||||
@@ -1,14 +0,0 @@
|
||||
(ns frontend.config)
|
||||
|
||||
(defonce tasks-org "tasks.org")
|
||||
(defonce hidden-file ".hidden")
|
||||
(defonce dev? ^boolean goog.DEBUG)
|
||||
(def website
|
||||
(if dev?
|
||||
"http://localhost:3000"
|
||||
"https://logseq.com"))
|
||||
|
||||
(def api
|
||||
(if dev?
|
||||
"http://localhost:3000/api/v1/"
|
||||
(str website "/api/v1/")))
|
||||
@@ -1,40 +0,0 @@
|
||||
(ns frontend.core
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.page :as page]
|
||||
[frontend.routes :as routes]
|
||||
[reitit.frontend :as rf]
|
||||
[reitit.frontend.easy :as rfe]))
|
||||
|
||||
(defn set-router!
|
||||
[]
|
||||
(rfe/start!
|
||||
(rf/router routes/routes {})
|
||||
handler/set-route-match!
|
||||
;; set to false to enable HistoryAPI
|
||||
{:use-fragment false}))
|
||||
|
||||
(defn start []
|
||||
(rum/mount
|
||||
(page/current-page)
|
||||
(.getElementById js/document "root"))
|
||||
(set-router!))
|
||||
|
||||
(defn ^:export init []
|
||||
;; init is called ONCE when the page loads
|
||||
;; this is called in the index.html and must be exported
|
||||
;; so it is available even in :advanced release builds
|
||||
|
||||
(handler/start!)
|
||||
|
||||
;; popup to notify user, could be toggled in settings
|
||||
;; (handler/request-notifications-if-not-asked)
|
||||
|
||||
;; (handler/run-notify-worker!)
|
||||
|
||||
(start))
|
||||
|
||||
(defn stop []
|
||||
;; stop is called before any code is reloaded
|
||||
;; this is controlled by :before-load in the config
|
||||
(js/console.log "stop"))
|
||||
@@ -1,483 +0,0 @@
|
||||
(ns frontend.db
|
||||
(:require [datascript.core :as d]
|
||||
[frontend.util :as util]
|
||||
[medley.core :as medley]
|
||||
[datascript.transit :as dt]
|
||||
[frontend.format.org-mode :as org]
|
||||
[frontend.format.org.block :as block]
|
||||
[clojure.string :as string]
|
||||
[frontend.utf8 :as utf8]))
|
||||
|
||||
;; TODO: don't persistent :github/token
|
||||
|
||||
(def datascript-db "logseq/DB")
|
||||
(def schema
|
||||
{:db/ident {:db/unique :db.unique/identity}
|
||||
:github/token {}
|
||||
;; repo
|
||||
:repo/url {:db/unique :db.unique/identity}
|
||||
:repo/cloning? {}
|
||||
:repo/cloned? {}
|
||||
:repo/current {:db/valueType :db.type/ref}
|
||||
|
||||
;; file
|
||||
:file/path {:db/unique :db.unique/identity}
|
||||
:file/repo {:db/valueType :db.type/ref}
|
||||
:file/raw {}
|
||||
:file/html {}
|
||||
;; TODO: calculate memory/disk usage
|
||||
;; :file/size {}
|
||||
|
||||
;; heading
|
||||
:heading/uuid {:db/unique :db.unique/identity}
|
||||
:heading/repo {:db/valueType :db.type/ref}
|
||||
:heading/file {:db/valueType :db.type/ref}
|
||||
:heading/anchor {}
|
||||
:heading/marker {}
|
||||
:heading/priority {}
|
||||
:heading/level {}
|
||||
:heading/tags {:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/many
|
||||
:db/isComponent true}
|
||||
|
||||
;; tag
|
||||
:tag/name {:db/unique :db.unique/identity}
|
||||
|
||||
;; task
|
||||
:task/scheduled {:db/index true}
|
||||
:task/deadline {:db/index true}
|
||||
})
|
||||
|
||||
(defonce conn
|
||||
(d/create-conn schema))
|
||||
|
||||
;; transit serialization
|
||||
|
||||
(defn db->string [db]
|
||||
(dt/write-transit-str db))
|
||||
|
||||
(defn string->db [s]
|
||||
(dt/read-transit-str s))
|
||||
|
||||
;; persisting DB between page reloads
|
||||
(defn persist [db]
|
||||
(js/localStorage.setItem datascript-db (db->string db)))
|
||||
|
||||
(defn reset-conn! [db]
|
||||
(reset! conn db))
|
||||
|
||||
;; (new TextEncoder().encode('foo')).length
|
||||
(defn db-size
|
||||
[]
|
||||
(when-let [store (js/localStorage.getItem datascript-db)]
|
||||
(let [bytes (.-length (.encode (js/TextEncoder.) store))]
|
||||
(/ bytes 1000))))
|
||||
|
||||
(defn restore! []
|
||||
(when-let [stored (js/localStorage.getItem datascript-db)]
|
||||
(let [stored-db (string->db stored)]
|
||||
(when (= (:schema stored-db) schema) ;; check for code update
|
||||
(reset-conn! stored-db)))))
|
||||
|
||||
;; TODO: added_at, started_at, schedule, deadline
|
||||
(def qualified-map
|
||||
{:file :heading/file
|
||||
:anchor :heading/anchor
|
||||
:title :heading/title
|
||||
:marker :heading/marker
|
||||
:priority :heading/priority
|
||||
:level :heading/level
|
||||
:timestamps :heading/timestamps
|
||||
:children :heading/children
|
||||
:tags :heading/tags
|
||||
:meta :heading/meta
|
||||
})
|
||||
|
||||
;; (def schema
|
||||
;; [{:db/ident {:db/unique :db.unique/identity}}
|
||||
|
||||
;; ;; {:db/ident :heading/title
|
||||
;; ;; :db/valueType :db.type/string
|
||||
;; ;; :db/cardinality :db.cardinality/one}
|
||||
|
||||
;; ;; {:db/ident :heading/parent-title
|
||||
;; ;; :db/valueType :db.type/string
|
||||
;; ;; :db/cardinality :db.cardinality/one}
|
||||
|
||||
;; ;; TODO: timestamps, meta
|
||||
;; ;; scheduled, deadline
|
||||
;; ])
|
||||
|
||||
(defn ->tags
|
||||
[tags]
|
||||
(map (fn [tag]
|
||||
{:db/id tag
|
||||
:tag/name tag})
|
||||
tags))
|
||||
|
||||
(defn extract-timestamps
|
||||
[{:keys [meta] :as heading}]
|
||||
(let [{:keys [pos timestamps]} meta]
|
||||
))
|
||||
|
||||
(defn- safe-headings
|
||||
[headings]
|
||||
(mapv (fn [heading]
|
||||
(let [heading (-> (util/remove-nils heading)
|
||||
(assoc :heading/uuid (d/squuid)))
|
||||
heading (assoc heading :tags
|
||||
(->tags (:tags heading)))]
|
||||
(medley/map-keys
|
||||
(fn [k] (get qualified-map k k))
|
||||
heading)))
|
||||
headings))
|
||||
|
||||
;; queries
|
||||
|
||||
(defn- distinct-result
|
||||
[query-result]
|
||||
(-> query-result
|
||||
seq
|
||||
flatten
|
||||
distinct))
|
||||
|
||||
(def seq-flatten (comp flatten seq))
|
||||
|
||||
(defn get-all-tags
|
||||
[]
|
||||
(distinct-result
|
||||
(d/q '[:find ?tags
|
||||
:where
|
||||
[?h :heading/tags ?tags]]
|
||||
@conn)))
|
||||
|
||||
(defn get-repo-headings
|
||||
[repo-url]
|
||||
(-> (d/q '[:find ?heading
|
||||
:in $ ?repo-url
|
||||
:where
|
||||
[?repo :repo/url ?repo-url]
|
||||
[?heading :heading/repo ?repo]]
|
||||
@conn repo-url)
|
||||
seq-flatten))
|
||||
|
||||
(defn delete-headings!
|
||||
[repo-url]
|
||||
(let [headings (get-repo-headings repo-url)
|
||||
headings (mapv (fn [eid] [:db.fn/retractEntity eid]) headings)]
|
||||
(d/transact! conn headings)))
|
||||
|
||||
(defn get-file-headings
|
||||
[repo-url path]
|
||||
(-> (d/q '[:find ?heading
|
||||
:in $ ?repo-url ?path
|
||||
:where
|
||||
[?repo :repo/url ?repo-url]
|
||||
[?file :file/path ?path]
|
||||
[?heading :heading/file ?file]
|
||||
[?heading :heading/repo ?repo]]
|
||||
@conn repo-url path)
|
||||
seq-flatten))
|
||||
|
||||
(defn delete-file-headings!
|
||||
[repo-url path]
|
||||
(let [headings (get-file-headings repo-url path)]
|
||||
(mapv (fn [eid] [:db.fn/retractEntity eid]) headings)))
|
||||
|
||||
;; transactions
|
||||
(defn reset-headings!
|
||||
[repo-url headings]
|
||||
(delete-headings! repo-url)
|
||||
(let [headings (safe-headings headings)]
|
||||
(d/transact! conn headings)))
|
||||
|
||||
(defn get-all-headings
|
||||
[]
|
||||
(seq-flatten
|
||||
(d/q '[:find (pull ?h [*])
|
||||
:where
|
||||
[?h :heading/title]]
|
||||
@conn)))
|
||||
|
||||
(defn search-headings-by-title
|
||||
[title])
|
||||
|
||||
(defn get-headings-by-tag
|
||||
[tag]
|
||||
(let [pred (fn [db tags]
|
||||
(some #(= tag %) tags))]
|
||||
(d/q '[:find (flatten (pull ?h [*]))
|
||||
:in $ ?pred
|
||||
:where
|
||||
[?h :heading/tags ?tags]
|
||||
[(?pred $ ?tags)]]
|
||||
@conn pred)))
|
||||
|
||||
(defn transact!
|
||||
[tx-data]
|
||||
(d/transact! conn tx-data))
|
||||
|
||||
(defn set-key-value
|
||||
[key value]
|
||||
(transact! [{:db/id -1
|
||||
:db/ident key
|
||||
key value}]))
|
||||
|
||||
(defn transact-github-token!
|
||||
[token]
|
||||
(set-key-value :github/token token))
|
||||
|
||||
(defn get-key-value
|
||||
[key]
|
||||
(some-> (d/entity (d/db conn) key)
|
||||
key))
|
||||
|
||||
(defn get-github-token
|
||||
[]
|
||||
(get-key-value :github/token))
|
||||
|
||||
(defn set-current-repo!
|
||||
[repo]
|
||||
(set-key-value :repo/current [:repo/url repo]))
|
||||
|
||||
(defn get-current-repo
|
||||
[]
|
||||
(:repo/url (get-key-value :repo/current)))
|
||||
|
||||
(defn get-repos
|
||||
[]
|
||||
(->> (d/q '[:find ?url
|
||||
:where [_ :repo/url ?url]]
|
||||
@conn)
|
||||
(map first)
|
||||
distinct))
|
||||
|
||||
(defn get-files
|
||||
[]
|
||||
(->> (d/q '[:find ?path
|
||||
:where
|
||||
[_ :repo/current ?repo]
|
||||
[?file :file/repo ?repo]
|
||||
[?file :file/path ?path]]
|
||||
@conn)
|
||||
(map first)
|
||||
distinct))
|
||||
|
||||
(defn set-repo-cloning
|
||||
[repo-url value]
|
||||
(d/transact! conn
|
||||
[{:repo/url repo-url
|
||||
:repo/cloning? value}]))
|
||||
|
||||
(defn mark-repo-as-cloned
|
||||
[repo-url]
|
||||
(d/transact! conn
|
||||
[{:repo/url repo-url
|
||||
:repo/cloned? true}]))
|
||||
|
||||
;; file
|
||||
(defn transact-files!
|
||||
[repo-url files]
|
||||
(d/transact! conn
|
||||
(for [file files]
|
||||
{:file/repo [:repo/url repo-url]
|
||||
:file/path file})))
|
||||
|
||||
(defn get-repo-files
|
||||
[repo-url]
|
||||
(->> (d/q '[:find ?path
|
||||
:in $ ?repo-url
|
||||
:where
|
||||
[?repo :repo/url ?repo-url]
|
||||
[?file :file/repo ?repo]
|
||||
[?file :file/path ?path]]
|
||||
@conn repo-url)
|
||||
(map first)
|
||||
distinct))
|
||||
|
||||
(defn set-file-content!
|
||||
[repo-url file content]
|
||||
(d/transact! conn
|
||||
[{:file/repo [:repo/url repo-url]
|
||||
:file/path file
|
||||
:file/content content}]))
|
||||
|
||||
(defn extract-headings
|
||||
[repo-url file content]
|
||||
(if (string/blank? content)
|
||||
[]
|
||||
(let [headings (org/->clj content)
|
||||
headings (block/extract-headings headings)]
|
||||
(map (fn [heading]
|
||||
(assoc heading
|
||||
:heading/repo [:repo/url repo-url]
|
||||
:heading/file [:file/path file]))
|
||||
headings))))
|
||||
|
||||
(defn get-all-files-content
|
||||
[repo-url]
|
||||
(d/q '[:find ?path ?content
|
||||
:in $ ?repo-url
|
||||
:where
|
||||
[?repo :repo/url ?repo-url]
|
||||
[?file :file/repo ?repo]
|
||||
[?file :file/content ?content]
|
||||
[?file :file/path ?path]]
|
||||
@conn repo-url))
|
||||
|
||||
(defn extract-all-headings
|
||||
[repo-url]
|
||||
(let [contents (get-all-files-content repo-url)]
|
||||
(vec
|
||||
(mapcat
|
||||
(fn [[file content] contents]
|
||||
(extract-headings repo-url file content))
|
||||
contents))))
|
||||
|
||||
(defn reset-file!
|
||||
[repo-url file content]
|
||||
(let [file-content [{:file/repo [:repo/url repo-url]
|
||||
:file/path file
|
||||
:file/content content}]
|
||||
delete-headings (delete-file-headings! repo-url file)
|
||||
headings (extract-headings repo-url file content)
|
||||
headings (safe-headings headings)]
|
||||
(d/transact! conn (concat file-content delete-headings headings))))
|
||||
|
||||
(defn get-file-content
|
||||
[repo-url path]
|
||||
(->> (d/q '[:find ?content
|
||||
:in $ ?repo-url ?path
|
||||
:where
|
||||
[?repo :repo/url ?repo-url]
|
||||
[?file :file/repo ?repo]
|
||||
[?file :file/path ?path]
|
||||
[?file :file/content ?content]]
|
||||
@conn repo-url path)
|
||||
(map first)
|
||||
first))
|
||||
|
||||
(defn get-file
|
||||
[path]
|
||||
(->
|
||||
(d/q '[:find ?content
|
||||
:in $ ?path
|
||||
:where
|
||||
[_ :repo/current ?repo]
|
||||
[?file :file/repo ?repo]
|
||||
[?file :file/path ?path]
|
||||
[?file :file/content ?content]]
|
||||
@conn
|
||||
path)
|
||||
ffirst))
|
||||
|
||||
;; marker should be one of: TODO, DOING, IN-PROGRESS
|
||||
;; time duration
|
||||
(defn get-agenda
|
||||
([]
|
||||
(get-agenda :week))
|
||||
([time]
|
||||
(let [duration (case time
|
||||
:today []
|
||||
:week []
|
||||
:month [])]
|
||||
(->
|
||||
(d/q '[:find (pull ?h [*])
|
||||
:where
|
||||
(or [?h :heading/marker "TODO"]
|
||||
[?h :heading/marker "DOING"]
|
||||
[?h :heading/marker "IN-PROGRESS"]
|
||||
[?h :heading/marker "DONE"])]
|
||||
@conn)
|
||||
seq-flatten))))
|
||||
|
||||
(defn entity
|
||||
[id-or-lookup-ref]
|
||||
(d/entity (d/db conn) id-or-lookup-ref))
|
||||
|
||||
(defn get-current-journal
|
||||
[]
|
||||
(get-file (util/current-journal-path)))
|
||||
|
||||
(defn get-month-journals
|
||||
[journal-path content before-date days]
|
||||
(let [[month day year] (string/split before-date #"/")
|
||||
day' (util/zero-pad (inc (util/parse-int day)))
|
||||
before-date (string/join "/" [month day' year])
|
||||
content-arr (utf8/encode content)
|
||||
end-pos (utf8/length content-arr)
|
||||
blocks (reverse (org/->clj content))
|
||||
headings (some->>
|
||||
blocks
|
||||
(filter (fn [block]
|
||||
(and
|
||||
(block/heading-block? block)
|
||||
|
||||
(= 1 (:level (second block)))
|
||||
|
||||
(let [[_ {:keys [title meta]}] block]
|
||||
(when-let [title (last (first title))]
|
||||
(let [date (last (string/split title #", "))]
|
||||
(<= (compare date before-date) 0)))))))
|
||||
(map (fn [[_ {:keys [title meta]}]]
|
||||
{:title (last (first title))
|
||||
:file-path journal-path
|
||||
:start-pos (:pos meta)}))
|
||||
(take (inc days)))
|
||||
[_ journals] (reduce (fn [[last-end-pos acc] heading]
|
||||
(let [end-pos last-end-pos
|
||||
acc (conj acc (assoc heading
|
||||
:uuid (cljs.core/random-uuid)
|
||||
:end-pos end-pos
|
||||
:content (utf8/substring content-arr
|
||||
(:start-pos heading)
|
||||
end-pos)))]
|
||||
[(:start-pos heading) acc])) [end-pos []] headings)]
|
||||
(if (> (count journals) days)
|
||||
(drop 1 journals)
|
||||
journals)))
|
||||
|
||||
(defn- compute-journal-path
|
||||
[before-date]
|
||||
(let [[month day year] (->> (string/split before-date #"/")
|
||||
(mapv util/parse-int))
|
||||
[year month] (cond
|
||||
(and (= month 1)
|
||||
(= day 1))
|
||||
[(dec year) 12]
|
||||
|
||||
(= day 1)
|
||||
[year (dec month)]
|
||||
|
||||
:else
|
||||
[year month])]
|
||||
(util/journals-path year month)))
|
||||
|
||||
;; before-date should be a string joined with "/", like "month/day/year"
|
||||
(defn get-latest-journals
|
||||
([]
|
||||
(get-latest-journals {}))
|
||||
([{:keys [content before-date days]
|
||||
:or {days 3}}]
|
||||
(let [before-date (if before-date
|
||||
before-date
|
||||
(let [{:keys [year month day]} (util/year-month-day-padded)]
|
||||
(string/join "/" [month day year])))
|
||||
|
||||
journal-path (compute-journal-path before-date)]
|
||||
(when-let [content (or content (get-file journal-path))]
|
||||
(get-month-journals journal-path content before-date days)))))
|
||||
|
||||
(comment
|
||||
(d/transact! conn [{:db/id -1
|
||||
:repo/url "https://github.com/tiensonqin/notes"
|
||||
:repo/cloned? false}])
|
||||
(d/entity (d/db conn) [:repo/url "https://github.com/tiensonqin/notes"])
|
||||
(d/transact! conn
|
||||
(safe-headings [{:heading/repo [:repo/url "https://github.com/tiensonqin/notes"]
|
||||
:heading/file "test.org"
|
||||
:heading/anchor "hello"
|
||||
:heading/marker "TODO"
|
||||
:heading/priority "A"
|
||||
:heading/level "10"
|
||||
:heading/title "hello world"}])))
|
||||
@@ -1,54 +0,0 @@
|
||||
// copied from https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side
|
||||
|
||||
function objectURLToBlob(url, callback) {
|
||||
var http = new XMLHttpRequest();
|
||||
http.open("GET", url, true);
|
||||
http.responseType = "blob";
|
||||
http.onload = function(e) {
|
||||
if (this.status == 200 || this.status === 0) {
|
||||
callback(this.response);
|
||||
}
|
||||
};
|
||||
http.send();
|
||||
}
|
||||
|
||||
export var getEXIFOrientation = function (img, callback) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
var view = new DataView(e.target.result)
|
||||
|
||||
if (view.getUint16(0, false) !== 0xFFD8) {
|
||||
return callback(-2)
|
||||
}
|
||||
var length = view.byteLength
|
||||
var offset = 2
|
||||
while (offset < length) {
|
||||
var marker = view.getUint16(offset, false)
|
||||
offset += 2
|
||||
if (marker === 0xFFE1) {
|
||||
if (view.getUint32(offset += 2, false) !== 0x45786966) {
|
||||
return callback(-1)
|
||||
}
|
||||
var little = view.getUint16(offset += 6, false) === 0x4949
|
||||
offset += view.getUint32(offset + 4, little)
|
||||
var tags = view.getUint16(offset, little)
|
||||
offset += 2
|
||||
for (var i = 0; i < tags; i++) {
|
||||
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
|
||||
var o = view.getUint16(offset + (i * 12) + 8, little);
|
||||
return callback(o)
|
||||
}
|
||||
}
|
||||
} else if ((marker & 0xFF00) !== 0xFF00) {
|
||||
break
|
||||
} else {
|
||||
offset += view.getUint16(offset, false)
|
||||
}
|
||||
}
|
||||
return callback(-1)
|
||||
};
|
||||
|
||||
objectURLToBlob(img.src, function (blob) {
|
||||
reader.readAsArrayBuffer(blob.slice(0, 65536));
|
||||
});
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
(ns frontend.format
|
||||
(:require [frontend.format.org-mode :as org :refer [->OrgMode]]
|
||||
[frontend.format.markdown :as markdown :refer [->Markdown]]
|
||||
[frontend.format.protocol :as protocol]))
|
||||
|
||||
(defn to-html
|
||||
([content suffix]
|
||||
(to-html content suffix nil))
|
||||
([content suffix config]
|
||||
(when-let [record (case (keyword suffix)
|
||||
:org
|
||||
(->OrgMode content)
|
||||
(list :md :markdown)
|
||||
(->Markdown content)
|
||||
nil)]
|
||||
(if config
|
||||
(protocol/toHtml record config)
|
||||
(protocol/toHtml record)))))
|
||||
@@ -1,13 +0,0 @@
|
||||
(ns frontend.format.markdown
|
||||
(:require ["showdown" :refer [Converter]]
|
||||
[frontend.format.protocol :as protocol]))
|
||||
|
||||
(defonce converter (Converter.))
|
||||
|
||||
(defrecord Markdown [content]
|
||||
protocol/Format
|
||||
(toHtml [this]
|
||||
(.makeHtml converter content))
|
||||
(toHtml [this config]
|
||||
;; TODO:
|
||||
(.makeHtml converter content)))
|
||||
@@ -1,99 +0,0 @@
|
||||
(ns frontend.format.org.block
|
||||
(:require [frontend.util :as util]))
|
||||
|
||||
(defn heading-block?
|
||||
[block]
|
||||
(and
|
||||
(vector? block)
|
||||
(= "Heading" (first block))))
|
||||
|
||||
(defn task-block?
|
||||
[block]
|
||||
(and
|
||||
(heading-block? block)
|
||||
(some? (:marker (second block)))))
|
||||
|
||||
;; FIXME:
|
||||
(defn extract-title
|
||||
[block]
|
||||
(-> (:title (second block))
|
||||
first
|
||||
second))
|
||||
|
||||
(defn- paragraph-block?
|
||||
[block]
|
||||
(and
|
||||
(vector? block)
|
||||
(= "Paragraph" (first block))))
|
||||
|
||||
(defn- timestamp-block?
|
||||
[block]
|
||||
(and
|
||||
(vector? block)
|
||||
(= "Timestamp" (first block))))
|
||||
|
||||
(defn- paragraph-timestamp-block?
|
||||
[block]
|
||||
(and (paragraph-block? block)
|
||||
(timestamp-block? (first (second block)))))
|
||||
|
||||
(defn extract-timestamp
|
||||
[block]
|
||||
(-> block
|
||||
second
|
||||
first
|
||||
second))
|
||||
|
||||
(defn extract-headings
|
||||
[blocks]
|
||||
(loop [headings []
|
||||
heading-children []
|
||||
blocks (reverse blocks)
|
||||
timestamps {}
|
||||
last-pos nil]
|
||||
(if (seq blocks)
|
||||
(let [block (first blocks)
|
||||
level (:level (second block))]
|
||||
(cond
|
||||
(paragraph-timestamp-block? block)
|
||||
(let [timestamp (extract-timestamp block)
|
||||
timestamps' (conj timestamps timestamp)]
|
||||
(recur headings heading-children (rest blocks) timestamps' last-pos))
|
||||
|
||||
(heading-block? block)
|
||||
(let [heading (-> (assoc (second block)
|
||||
:children (reverse heading-children)
|
||||
:timestamps timestamps)
|
||||
(assoc-in [:meta :end-pos] last-pos))
|
||||
last-pos' (get-in heading [:meta :pos])]
|
||||
(recur (conj headings heading) [] (rest blocks) {} last-pos'))
|
||||
|
||||
:else
|
||||
(let [heading-children' (conj heading-children block)]
|
||||
(recur headings heading-children' (rest blocks) timestamps last-pos))))
|
||||
(reverse headings))))
|
||||
|
||||
;; marker: DOING | IN-PROGRESS > TODO > WAITING | WAIT > DONE > CANCELED | CANCELLED
|
||||
;; priority: A > B > C
|
||||
(defn sort-tasks
|
||||
[headings]
|
||||
(let [markers ["DOING" "IN-PROGRESS" "TODO" "WAITING" "WAIT" "DONE" "CANCELED" "CANCELLED"]
|
||||
markers (zipmap markers (reverse (range 1 (count markers))))
|
||||
priorities ["A" "B" "C" "D" "E" "F" "G"]
|
||||
priorities (zipmap priorities (reverse (range 1 (count priorities))))]
|
||||
(sort (fn [t1 t2]
|
||||
(let [m1 (get markers (:heading/marker t1) 0)
|
||||
m2 (get markers (:heading/marker t2) 0)
|
||||
p1 (get priorities (:heading/priority t1) 0)
|
||||
p2 (get priorities (:heading/priority t2) 0)]
|
||||
(cond
|
||||
(and (= m1 m2)
|
||||
(= p1 p2))
|
||||
(compare (str (:heading/title t1))
|
||||
(str (:heading/title t2)))
|
||||
|
||||
(= m1 m2)
|
||||
(> p1 p2)
|
||||
:else
|
||||
(> m1 m2))))
|
||||
headings)))
|
||||
@@ -1,48 +0,0 @@
|
||||
(ns frontend.format.org-mode
|
||||
(:require ["mldoc_org" :as org]
|
||||
[frontend.format.protocol :as protocol]
|
||||
[frontend.util :as util]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(def default-config
|
||||
(js/JSON.stringify
|
||||
#js {:toc false
|
||||
:heading_number false
|
||||
:keep_line_break false}))
|
||||
|
||||
(def config-with-line-break
|
||||
(js/JSON.stringify
|
||||
#js {:toc false
|
||||
:heading_number false
|
||||
:keep_line_break true}))
|
||||
|
||||
(def Org (.-MldocOrg org))
|
||||
|
||||
(defrecord OrgMode [content]
|
||||
protocol/Format
|
||||
(toHtml [this]
|
||||
(.parseHtml Org content default-config))
|
||||
(toHtml [this config]
|
||||
(.parseHtml Org content config)))
|
||||
|
||||
(defn parse-json
|
||||
([content]
|
||||
(parse-json content default-config))
|
||||
([content config]
|
||||
(.parseJson Org content config)))
|
||||
|
||||
(defn ->clj
|
||||
[content]
|
||||
(if (string/blank? content)
|
||||
{}
|
||||
(-> content
|
||||
(parse-json)
|
||||
(util/json->clj))))
|
||||
|
||||
(defn inline-list->html
|
||||
[json]
|
||||
(.inlineListToHtmlStr Org json))
|
||||
|
||||
(defn json->html
|
||||
[json]
|
||||
(.jsonToHtmlStr Org json default-config))
|
||||
@@ -1,4 +0,0 @@
|
||||
(ns frontend.format.protocol)
|
||||
|
||||
(defprotocol Format
|
||||
(toHtml [this] [this config]))
|
||||
@@ -1,44 +0,0 @@
|
||||
(ns frontend.fs
|
||||
(:require [frontend.util :as util]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn mkdir
|
||||
[dir]
|
||||
(js/pfs.mkdir dir))
|
||||
|
||||
(defn readdir
|
||||
[dir]
|
||||
(js/pfs.readdir dir))
|
||||
|
||||
(defn read-file
|
||||
[dir path]
|
||||
(js/pfs.readFile (str dir "/" path)
|
||||
(clj->js {:encoding "utf8"})))
|
||||
|
||||
(defn read-file-2
|
||||
[dir path]
|
||||
(js/pfs.readFile (str dir "/" path)
|
||||
(clj->js {})))
|
||||
|
||||
(defn write-file
|
||||
[dir path content]
|
||||
(js/pfs.writeFile (str dir "/" path) content))
|
||||
|
||||
(defn stat
|
||||
[dir path]
|
||||
(js/pfs.stat (str dir "/" path)))
|
||||
|
||||
(defn create-if-not-exists
|
||||
([dir path]
|
||||
(create-if-not-exists dir path ""))
|
||||
([dir path initial-content]
|
||||
(util/p-handle
|
||||
(stat dir path)
|
||||
(fn [_stat] true)
|
||||
(fn [error]
|
||||
(write-file dir path initial-content)
|
||||
false))))
|
||||
|
||||
(comment
|
||||
(def dir "/notes")
|
||||
)
|
||||
@@ -1,133 +0,0 @@
|
||||
(ns frontend.git
|
||||
(:refer-clojure :exclude [clone])
|
||||
(:require [promesa.core :as p]
|
||||
[frontend.util :as util]
|
||||
[clojure.string :as string]))
|
||||
|
||||
;; only support Github now
|
||||
(defn auth
|
||||
[token]
|
||||
{:username token
|
||||
:password "x-oauth-basic"})
|
||||
|
||||
(defn set-username-email
|
||||
[dir username email]
|
||||
(prn {:dir dir
|
||||
:username username
|
||||
:email email})
|
||||
(util/p-handle (js/git.config (clj->js
|
||||
{:dir dir
|
||||
:path "user.name"
|
||||
:value username}))
|
||||
(fn [result]
|
||||
(js/git.config (clj->js
|
||||
{:dir dir
|
||||
:path "user.email"
|
||||
:value email})))
|
||||
(fn [error]
|
||||
(prn "error:" error))))
|
||||
|
||||
(defn with-auth
|
||||
[token m]
|
||||
(clj->js
|
||||
(merge (auth token)
|
||||
m)))
|
||||
|
||||
(defn get-repo-dir
|
||||
[repo-url]
|
||||
(str "/" (last (string/split repo-url #"/"))))
|
||||
|
||||
(defn clone
|
||||
[repo-url token]
|
||||
(js/git.clone (with-auth token
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:url repo-url
|
||||
:corsProxy "https://cors.isomorphic-git.org"
|
||||
:singleBranch true
|
||||
:depth 1})))
|
||||
|
||||
(defn list-files
|
||||
[repo-url]
|
||||
(js/git.listFiles (clj->js
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:ref "HEAD"})))
|
||||
|
||||
(defn fetch
|
||||
[repo-url token]
|
||||
(js/git.fetch (with-auth token
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:ref "master"
|
||||
:singleBranch true})))
|
||||
|
||||
(defn log
|
||||
[repo-url token depth]
|
||||
(js/git.log (with-auth token
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:ref "master"
|
||||
:depth depth
|
||||
:singleBranch true})))
|
||||
|
||||
(defn pull
|
||||
[repo-url token]
|
||||
(js/git.pull (with-auth token
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:ref "master"
|
||||
:singleBranch true})))
|
||||
(defn add
|
||||
[repo-url file]
|
||||
(js/git.add (clj->js
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:filepath file})))
|
||||
|
||||
;; TODO: cache email and name
|
||||
(defn commit
|
||||
[repo-url message]
|
||||
(js/git.commit (clj->js
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:author {:name "Orgnote"
|
||||
:email "orgnote@hello.world"}
|
||||
:message message})))
|
||||
|
||||
(defn push
|
||||
[repo-url token]
|
||||
(js/git.push (with-auth token
|
||||
{:dir (get-repo-dir repo-url)
|
||||
:remote "origin"
|
||||
:ref "master"
|
||||
})))
|
||||
|
||||
(defn add-commit-push
|
||||
[repo-url file message token push-ok-handler push-error-handler]
|
||||
(util/p-handle
|
||||
(let [files (if (coll? file) file [file])]
|
||||
(doseq [file files]
|
||||
(add repo-url file)))
|
||||
(fn [_]
|
||||
(util/p-handle
|
||||
(commit repo-url message)
|
||||
(fn [_]
|
||||
(push repo-url token)
|
||||
(push-ok-handler))
|
||||
push-error-handler))))
|
||||
|
||||
(defn add-commit
|
||||
[repo-url file message commit-ok-handler commit-error-handler]
|
||||
(let [get-seconds (fn []
|
||||
(/ (.getTime (js/Date.)) 1000))]
|
||||
(let [t1 (get-seconds)]
|
||||
(util/p-handle
|
||||
(add repo-url file)
|
||||
(fn [_]
|
||||
(let [t2 (get-seconds)]
|
||||
(prn "Add time: " (- t2 t1))
|
||||
(util/p-handle
|
||||
(commit repo-url message)
|
||||
(fn []
|
||||
(let [t3 (get-seconds)]
|
||||
(prn "Commit time: " (- t3 t2)))
|
||||
(prn "Commited")
|
||||
(commit-ok-handler))
|
||||
(fn [error]
|
||||
(commit-error-handler error))))
|
||||
)))
|
||||
))
|
||||
@@ -1,529 +0,0 @@
|
||||
(ns frontend.handler
|
||||
(:refer-clojure :exclude [clone load-file])
|
||||
(:require [frontend.git :as git]
|
||||
[frontend.fs :as fs]
|
||||
[frontend.state :as state]
|
||||
[frontend.db :as db]
|
||||
[frontend.storage :as storage]
|
||||
[frontend.util :as util]
|
||||
[frontend.config :as config]
|
||||
[clojure.walk :as walk]
|
||||
[clojure.string :as string]
|
||||
[promesa.core :as p]
|
||||
[cljs-bean.core :as bean]
|
||||
[reitit.frontend.easy :as rfe]
|
||||
[goog.crypt.base64 :as b64]
|
||||
[goog.object :as gobj]
|
||||
[goog.dom :as gdom]
|
||||
[rum.core :as rum]
|
||||
[datascript.core :as d]
|
||||
[frontend.utf8 :as utf8]
|
||||
[frontend.image :as image])
|
||||
(:import [goog.events EventHandler]))
|
||||
|
||||
;; We only support Github token now
|
||||
(defn load-file
|
||||
[repo-url path state-handler]
|
||||
(util/p-handle (fs/read-file (git/get-repo-dir repo-url) path)
|
||||
(fn [content]
|
||||
(state-handler content))))
|
||||
|
||||
(defn- hidden?
|
||||
[path patterns]
|
||||
(some (fn [pattern]
|
||||
(or
|
||||
(= path pattern)
|
||||
(and (string/starts-with? pattern "/")
|
||||
(= (str "/" (first (string/split path #"/")))
|
||||
pattern)))) patterns))
|
||||
|
||||
(defn load-files
|
||||
[repo-url]
|
||||
(util/p-handle (git/list-files repo-url)
|
||||
(fn [files]
|
||||
(when (> (count files) 0)
|
||||
(let [files (js->clj files)]
|
||||
;; FIXME: don't load blobs
|
||||
(if (contains? (set files) config/hidden-file)
|
||||
(load-file repo-url config/hidden-file
|
||||
(fn [patterns-content]
|
||||
(when patterns-content
|
||||
(let [patterns (string/split patterns-content #"\n")
|
||||
files (remove (fn [path] (hidden? path patterns)) files)]
|
||||
(db/transact-files! repo-url files)))))
|
||||
(p/promise (db/transact-files! repo-url files))))))))
|
||||
|
||||
|
||||
;; TODO: remove this
|
||||
(declare load-repo-to-db!)
|
||||
|
||||
(defn get-latest-commit
|
||||
[handler]
|
||||
(-> (git/log (db/get-current-repo)
|
||||
(db/get-github-token)
|
||||
1)
|
||||
(.then (fn [commits]
|
||||
(handler (first commits))))
|
||||
(.catch (fn [error]
|
||||
(prn "get latest commit failed: " error)))))
|
||||
|
||||
(defonce latest-commit (atom nil))
|
||||
|
||||
;; TODO: Maybe replace with fetch?
|
||||
;; TODO: callback hell
|
||||
(defn pull
|
||||
[repo-url token]
|
||||
(when (and (nil? (:git-error @state/state))
|
||||
(nil? (:git-status @state/state)))
|
||||
(util/p-handle
|
||||
(git/pull repo-url token)
|
||||
(fn [result]
|
||||
(prn "pull successfully!")
|
||||
(get-latest-commit
|
||||
(fn [commit]
|
||||
(when (or (nil? @latest-commit)
|
||||
(and @latest-commit
|
||||
commit
|
||||
(not= (gobj/get commit "oid")
|
||||
(gobj/get @latest-commit "oid"))))
|
||||
(prn "New commit oid: " (gobj/get commit "oid"))
|
||||
(-> (load-files repo-url)
|
||||
(p/then
|
||||
(fn []
|
||||
(load-repo-to-db! repo-url)))))
|
||||
(reset! latest-commit commit)))))))
|
||||
|
||||
(defn periodically-pull
|
||||
[repo-url]
|
||||
(when-let [token (db/get-github-token)]
|
||||
(pull repo-url token)
|
||||
(js/setInterval #(pull repo-url token)
|
||||
(* 60 1000))))
|
||||
|
||||
(defn git-add-commit
|
||||
[repo-url file message content]
|
||||
(swap! state/state assoc :git-status :commit)
|
||||
(db/reset-file! repo-url file content)
|
||||
(git/add-commit repo-url file message
|
||||
(fn []
|
||||
(swap! state/state assoc
|
||||
:git-status :should-push))
|
||||
(fn [error]
|
||||
(prn "Commit failed, "
|
||||
{:repo repo-url
|
||||
:file file
|
||||
:message message})
|
||||
(swap! state/state assoc
|
||||
:git-status :commit-failed
|
||||
:git-error error))))
|
||||
|
||||
;; TODO: update latest commit
|
||||
(defn push
|
||||
[repo-url file]
|
||||
(when (and (= :should-push (:git-status @state/state))
|
||||
(nil? (:git-error @state/state)))
|
||||
(swap! state/state assoc :git-status :push)
|
||||
(let [token (db/get-github-token)]
|
||||
(util/p-handle
|
||||
(git/push repo-url token)
|
||||
(fn []
|
||||
(prn "Push successfully!")
|
||||
(swap! state/state assoc
|
||||
:git-status nil
|
||||
:git-error nil)
|
||||
;; TODO: update latest-commit
|
||||
(get-latest-commit
|
||||
(fn [commit]
|
||||
(reset! latest-commit commit))))
|
||||
(fn [error]
|
||||
(prn "Failed to push, error: " error)
|
||||
(swap! state/state assoc
|
||||
:git-status :push-failed
|
||||
:git-error error))))))
|
||||
|
||||
(defn clone
|
||||
[repo]
|
||||
(let [token (db/get-github-token)]
|
||||
(util/p-handle
|
||||
(do
|
||||
(db/set-repo-cloning repo true)
|
||||
(git/clone repo token))
|
||||
(fn []
|
||||
(db/set-repo-cloning repo false)
|
||||
(db/mark-repo-as-cloned repo)
|
||||
(db/set-current-repo! repo)
|
||||
;; load contents
|
||||
(load-files repo))
|
||||
(fn [e]
|
||||
(db/set-repo-cloning repo false)
|
||||
(prn "Clone failed, reason: " e)))))
|
||||
|
||||
(defn new-notification
|
||||
[text]
|
||||
(js/Notification. "Logseq" #js {:body text
|
||||
;; :icon logo
|
||||
}))
|
||||
|
||||
(defn request-notifications
|
||||
[]
|
||||
(util/p-handle (.requestPermission js/Notification)
|
||||
(fn [result]
|
||||
(storage/set :notification-permission-asked? true)
|
||||
|
||||
(when (= "granted" result)
|
||||
(storage/set :notification-permission? true)))))
|
||||
|
||||
(defn request-notifications-if-not-asked
|
||||
[]
|
||||
(when-not (storage/get :notification-permission-asked?)
|
||||
(request-notifications)))
|
||||
|
||||
;; notify deadline or scheduled tasks
|
||||
(defn run-notify-worker!
|
||||
[]
|
||||
(when (storage/get :notification-permission?)
|
||||
(let [notify-fn (fn []
|
||||
(let [tasks (:tasks @state/state)
|
||||
tasks (flatten (vals tasks))]
|
||||
(doseq [{:keys [marker title] :as task} tasks]
|
||||
(when-not (contains? #{"DONE" "CANCElED" "CANCELLED"} marker)
|
||||
(doseq [[type {:keys [date time] :as timestamp}] (:timestamps task)]
|
||||
(let [{:keys [year month day]} date
|
||||
{:keys [hour min]
|
||||
:or {hour 9
|
||||
min 0}} time
|
||||
now (util/get-local-date)]
|
||||
(when (and (contains? #{"Scheduled" "Deadline"} type)
|
||||
(= (assoc date :hour hour :minute min) now))
|
||||
(let [notification-text (str type ": " (second (first title)))]
|
||||
(new-notification notification-text)))))))))]
|
||||
(notify-fn)
|
||||
(js/setInterval notify-fn (* 1000 60)))))
|
||||
|
||||
(defn show-notification!
|
||||
[text]
|
||||
(swap! state/state assoc
|
||||
:notification/show? true
|
||||
:notification/text text)
|
||||
(js/setTimeout #(swap! state/state assoc
|
||||
:notification/show? false
|
||||
:notification/text nil)
|
||||
3000))
|
||||
|
||||
(defn alter-file
|
||||
([path commit-message content]
|
||||
(alter-file path commit-message content true))
|
||||
([path commit-message content redirect?]
|
||||
(let [token (db/get-github-token)
|
||||
repo-url (db/get-current-repo)]
|
||||
(util/p-handle
|
||||
(fs/write-file (git/get-repo-dir repo-url) path content)
|
||||
(fn [_]
|
||||
(when redirect?
|
||||
(rfe/push-state :file {:path (b64/encodeString path)}))
|
||||
(git-add-commit repo-url path commit-message content))))))
|
||||
|
||||
(defn clear-storage
|
||||
[repo-url]
|
||||
(js/window.pfs._idb.wipe)
|
||||
(clone repo-url))
|
||||
|
||||
;; TODO: utf8 encode performance
|
||||
(defn check
|
||||
[heading]
|
||||
(let [{:heading/keys [repo file marker meta uuid]} heading
|
||||
pos (:pos meta)
|
||||
repo (db/entity (:db/id repo))
|
||||
file (db/entity (:db/id file))
|
||||
repo-url (:repo/url repo)
|
||||
file (:file/path file)
|
||||
token (db/get-github-token)]
|
||||
(when-let [content (db/get-file-content repo-url file)]
|
||||
(let [encoded-content (utf8/encode content)
|
||||
content' (str (utf8/substring encoded-content 0 pos)
|
||||
(-> (utf8/substring encoded-content pos)
|
||||
(string/replace-first marker "DONE")))]
|
||||
(util/p-handle
|
||||
(fs/write-file (git/get-repo-dir repo-url) file content')
|
||||
(fn [_]
|
||||
(prn "check successfully, " file)
|
||||
(git-add-commit repo-url file
|
||||
(util/format "`%s` marked as DONE." marker)
|
||||
content')))))))
|
||||
|
||||
(defn uncheck
|
||||
[heading]
|
||||
(let [{:heading/keys [repo file marker meta]} heading
|
||||
pos (:pos meta)
|
||||
repo (db/entity (:db/id repo))
|
||||
file (db/entity (:db/id file))
|
||||
repo-url (:repo/url repo)
|
||||
file (:file/path file)
|
||||
token (db/get-github-token)]
|
||||
(when-let [content (db/get-file-content repo-url file)]
|
||||
(let [encoded-content (utf8/encode content)
|
||||
content' (str (utf8/substring encoded-content 0 pos)
|
||||
(-> (utf8/substring encoded-content pos)
|
||||
(string/replace-first "DONE" "TODO")))]
|
||||
(util/p-handle
|
||||
(fs/write-file (git/get-repo-dir repo-url) file content')
|
||||
(fn [_]
|
||||
(prn "uncheck successfully, " file)
|
||||
(git-add-commit repo-url file
|
||||
"DONE rollbacks to TODO."
|
||||
content')))))))
|
||||
|
||||
(defn remove-non-text-files
|
||||
[files]
|
||||
(remove
|
||||
(fn [file]
|
||||
(not (contains?
|
||||
#{"org"
|
||||
"md"
|
||||
"markdown"
|
||||
"txt"}
|
||||
(string/lower-case (last (string/split file #"\."))))))
|
||||
files))
|
||||
|
||||
(defn load-all-contents!
|
||||
[repo-url ok-handler]
|
||||
(let [files (db/get-repo-files repo-url)
|
||||
files (remove-non-text-files files)]
|
||||
(-> (p/all (for [file files]
|
||||
(load-file repo-url file
|
||||
(fn [content]
|
||||
(db/set-file-content! repo-url file content)))))
|
||||
(p/then
|
||||
(fn [_]
|
||||
(ok-handler))))))
|
||||
|
||||
(defonce headings-atom (atom nil))
|
||||
|
||||
(defn load-repo-to-db!
|
||||
[repo-url]
|
||||
(load-all-contents!
|
||||
repo-url
|
||||
(fn []
|
||||
(let [headings (db/extract-all-headings repo-url)]
|
||||
(reset! headings-atom headings)
|
||||
(db/reset-headings! repo-url headings)))))
|
||||
|
||||
|
||||
;; (defn sync
|
||||
;; []
|
||||
;; (let [[_user token repos] (get-user-token-repos)]
|
||||
;; (doseq [repo repos]
|
||||
;; (pull repo token))))
|
||||
|
||||
;; {:resp {:user {:name "tiensonqin", :email "tiensonqin@gmail.com", :id "2e3a6ebf-9ee6-40a8-8734-47f9f2697a1a", :created_at "2020-03-01T04:27:08Z"}, :tokens [{:user_id "2e3a6ebf-9ee6-40a8-8734-47f9f2697a1a", :oauth_token "", :id "203ca06a-0721-4322-b184-931c4c5f3dc3", :created_at "2020-03-01T04:27:08Z", :oauth_id "479169", :oauth_type "github"}], :repos [{:created_at "2020-03-01T04:27:43Z", :user_id "2e3a6ebf-9ee6-40a8-8734-47f9f2697a1a", :url "https://github.com/tiensonqin/notes", :id "0fcd3cfb-ce1f-4f9c-9d88-eecd8776777d"}]}}
|
||||
|
||||
(defn- extract-github-token
|
||||
[{:keys [user tokens repos]}]
|
||||
(:oauth_token (first tokens)))
|
||||
|
||||
(defn get-github-access-token
|
||||
[]
|
||||
(util/fetch (str config/api "me")
|
||||
(fn [resp]
|
||||
(when-let [github-token (extract-github-token resp)]
|
||||
(prn {:github-token github-token})
|
||||
(db/transact-github-token! github-token)))
|
||||
(fn [_error]
|
||||
;; (prn "Get token failed, error: " error)
|
||||
)))
|
||||
|
||||
;; org-journal format, something like `* Tuesday, 06/04/13`
|
||||
(defn default-month-journal-content
|
||||
[]
|
||||
(let [{:keys [year month day]} (util/get-date)
|
||||
last-day (util/get-month-last-day)
|
||||
month-pad (if (< month 10) (str "0" month) month)]
|
||||
(->> (map
|
||||
(fn [day]
|
||||
(let [day-pad (if (< day 10) (str "0" day) day)
|
||||
weekday (util/get-weekday (js/Date. year (dec month) (dec day)))]
|
||||
(util/format "* %s, %s/%s/%d\n\n" weekday month-pad day-pad year)))
|
||||
(range 1 (inc last-day)))
|
||||
(apply str))))
|
||||
|
||||
;; journals
|
||||
(defn create-month-journal-if-not-exists
|
||||
[repo-url]
|
||||
(let [repo-dir (git/get-repo-dir repo-url)
|
||||
path (util/current-journal-path)
|
||||
file-path (str "/" path)
|
||||
default-content (default-month-journal-content)]
|
||||
(->
|
||||
(util/p-handle
|
||||
(fs/mkdir (str repo-dir "/journals"))
|
||||
(fn [result]
|
||||
(fs/create-if-not-exists repo-dir file-path default-content))
|
||||
(fn [error]
|
||||
(fs/create-if-not-exists repo-dir file-path default-content)))
|
||||
(util/p-handle
|
||||
(fn [file-exists?]
|
||||
(if file-exists?
|
||||
(prn "Month journal already exists!")
|
||||
(do
|
||||
(prn "create a month journal")
|
||||
(git-add-commit repo-url path "create a month journal" default-content))))
|
||||
(fn [error]
|
||||
(prn error))))))
|
||||
|
||||
(defn clone-and-pull
|
||||
[repo]
|
||||
(p/then (clone repo)
|
||||
(fn []
|
||||
(create-month-journal-if-not-exists repo)
|
||||
(periodically-pull repo))))
|
||||
|
||||
(defn set-route-match!
|
||||
[route]
|
||||
(swap! state/state assoc :route-match route))
|
||||
|
||||
(defn set-ref-component!
|
||||
[k ref]
|
||||
(swap! state/state assoc :ref-components k ref))
|
||||
|
||||
(defn set-root-component!
|
||||
[comp]
|
||||
(swap! state/state assoc :root-component comp))
|
||||
|
||||
(defn re-render!
|
||||
[]
|
||||
(when-let [comp (get @state/state :root-component)]
|
||||
(when-not (:edit? @state/state)
|
||||
(rum/request-render comp))))
|
||||
|
||||
(defn db-listen-to-tx!
|
||||
[]
|
||||
(d/listen! db/conn :persistence
|
||||
(fn [tx-report] ;; FIXME do not notify with nil as db-report
|
||||
;; FIXME do not notify if tx-data is empty
|
||||
(when-let [db (:db-after tx-report)]
|
||||
(prn "DB changed, re-rendered!")
|
||||
(re-render!)
|
||||
(js/setTimeout (fn []
|
||||
(db/persist db)) 0)))))
|
||||
|
||||
(defn periodically-push-tasks
|
||||
[repo-url]
|
||||
(let [token (db/get-github-token)
|
||||
push (fn []
|
||||
(push repo-url token))]
|
||||
(js/setInterval push
|
||||
(* 10 1000))))
|
||||
|
||||
(defn periodically-pull-and-push
|
||||
[repo-url]
|
||||
(periodically-pull repo-url)
|
||||
;; (periodically-push-tasks repo-url)
|
||||
)
|
||||
|
||||
(defn set-state-kv!
|
||||
[key value]
|
||||
(swap! state/state assoc key value))
|
||||
|
||||
(defn edit-journal!
|
||||
[content journal]
|
||||
(swap! state/state assoc
|
||||
:edit? true
|
||||
:edit-journal journal))
|
||||
|
||||
(defn set-latest-journals!
|
||||
[]
|
||||
(set-state-kv! :latest-journals (db/get-latest-journals {})))
|
||||
|
||||
(defn set-journal-content!
|
||||
[uuid content]
|
||||
(swap! state/state update :latest-journals
|
||||
(fn [journals]
|
||||
(mapv
|
||||
(fn [journal]
|
||||
(if (= (:uuid journal) uuid)
|
||||
(assoc journal :content content)
|
||||
journal))
|
||||
journals))))
|
||||
|
||||
(defn save-current-edit-journal!
|
||||
[edit-content]
|
||||
(let [{:keys [edit-journal]} @state/state
|
||||
{:keys [start-pos end-pos]} edit-journal]
|
||||
(swap! state/state assoc
|
||||
:edit? false
|
||||
:edit-journal nil)
|
||||
(when-not (= edit-content (:content edit-journal)) ; if new changes
|
||||
(let [path (:file-path edit-journal)
|
||||
current-journals (db/get-file path)
|
||||
new-content (utf8/insert! current-journals start-pos end-pos edit-content)]
|
||||
(set-state-kv! :latest-journals (db/get-latest-journals {:content new-content}))
|
||||
(alter-file path "Auto save" new-content false)))))
|
||||
|
||||
(defn render-local-images!
|
||||
[]
|
||||
(let [images (array-seq (gdom/getElementsByTagName "img"))
|
||||
get-src (fn [image] (.getAttribute image "src"))
|
||||
local-images (filter
|
||||
(fn [image]
|
||||
(let [src (get-src image)]
|
||||
(and src
|
||||
(not (or (string/starts-with? src "http://")
|
||||
(string/starts-with? src "https://"))))))
|
||||
images)]
|
||||
(doseq [img local-images]
|
||||
(gobj/set img
|
||||
"onerror"
|
||||
(fn []
|
||||
(gobj/set (gobj/get img "style")
|
||||
"display" "none")))
|
||||
(let [path (get-src img)
|
||||
path (if (= (first path) \.)
|
||||
(subs path 1)
|
||||
path)]
|
||||
(util/p-handle
|
||||
(fs/read-file-2 (git/get-repo-dir (db/get-current-repo))
|
||||
path)
|
||||
(fn [blob]
|
||||
(let [blob (js/Blob. (array blob) (clj->js {:type "image"}))
|
||||
img-url (image/create-object-url blob)]
|
||||
(gobj/set img "src" img-url)
|
||||
(gobj/set (gobj/get img "style")
|
||||
"display" "initial"))))))))
|
||||
|
||||
;; FIXME:
|
||||
(defn set-username-email
|
||||
[]
|
||||
(git/set-username-email
|
||||
(git/get-repo-dir (db/get-current-repo))
|
||||
"Tienson Qin"
|
||||
"tiensonqin@gmail.com"))
|
||||
|
||||
(defn load-more-journals!
|
||||
[]
|
||||
(let [journals (:latest-journals @state/state)]
|
||||
(when-let [title (:title (last journals))]
|
||||
(let [before-date (last (string/split title #", "))
|
||||
more-journals (->> (db/get-latest-journals {:before-date before-date
|
||||
:days 4})
|
||||
(drop 1))
|
||||
journals (concat journals more-journals)]
|
||||
(set-state-kv! :latest-journals journals)))))
|
||||
|
||||
(defn start!
|
||||
[]
|
||||
(db/restore!)
|
||||
(db-listen-to-tx!)
|
||||
(when-let [first-repo (first (db/get-repos))]
|
||||
(db/set-current-repo! first-repo))
|
||||
(let [repos (db/get-repos)]
|
||||
(doseq [repo repos]
|
||||
(create-month-journal-if-not-exists repo)
|
||||
(periodically-pull-and-push repo))))
|
||||
|
||||
(comment
|
||||
(util/p-handle (fs/read-file (git/get-repo-dir (db/get-current-repo)) "test.org")
|
||||
(fn [content]
|
||||
(prn content)))
|
||||
|
||||
(pull (db/get-current-repo) (db/get-github-token))
|
||||
)
|
||||
@@ -1,103 +0,0 @@
|
||||
(ns frontend.image
|
||||
(:require [goog.object :as gobj]
|
||||
[frontend.blob :as blob]
|
||||
["/frontend/exif" :as exif]
|
||||
[frontend.util :as util]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(defn reverse?
|
||||
[exif-orientation]
|
||||
(contains? #{5 6 7 8} exif-orientation))
|
||||
|
||||
(defn re-scale
|
||||
[exif-orientation width height max-width max-height]
|
||||
(let [[width height]
|
||||
(if (reverse? exif-orientation)
|
||||
[height width]
|
||||
[width height])]
|
||||
(let [ratio (/ width height)
|
||||
to-width (if (> width max-width) max-width width)
|
||||
to-height (if (> height max-height) max-height height)
|
||||
new-ratio (/ to-width to-height)]
|
||||
(let [[w h] (cond
|
||||
(> new-ratio ratio)
|
||||
[(* ratio to-height) to-height]
|
||||
|
||||
(< new-ratio ratio)
|
||||
[to-width (/ to-width ratio)]
|
||||
|
||||
:else
|
||||
[to-width to-height])]
|
||||
[(int w) (int h)]))))
|
||||
|
||||
|
||||
(defn fix-orientation
|
||||
"Given image and exif orientation, ensure the photo is displayed
|
||||
rightside up"
|
||||
[img exif-orientation cb max-width max-height]
|
||||
(let [off-canvas (js/document.createElement "canvas")
|
||||
ctx ^js (.getContext off-canvas "2d")
|
||||
width (gobj/get img "width")
|
||||
height (gobj/get img "height")
|
||||
[to-width to-height] (re-scale exif-orientation width height max-width max-height)]
|
||||
(gobj/set ctx "imageSmoothingEnabled" false)
|
||||
(set! (.-width off-canvas) to-width)
|
||||
(set! (.-height off-canvas) to-height)
|
||||
;; rotate
|
||||
(let [[width height] (if (reverse? exif-orientation)
|
||||
[to-height to-width]
|
||||
[to-width to-height])]
|
||||
(case exif-orientation
|
||||
2 (.transform ctx -1 0 0 1 width 0)
|
||||
3 (.transform ctx -1 0 0 -1 width height)
|
||||
4 (.transform ctx 1 0 0 -1 0 height)
|
||||
5 (.transform ctx 0 1 1 0 0 0)
|
||||
6 (.transform ctx 0 1 -1 0 height 0)
|
||||
7 (.transform ctx 0 -1 -1 0 height width)
|
||||
8 (.transform ctx 0 -1 1 0 0 width)
|
||||
(.transform ctx 1 0 0 1 0 0))
|
||||
(.drawImage ctx img 0 0 width height))
|
||||
(cb off-canvas)))
|
||||
|
||||
(defn get-orientation
|
||||
[img cb max-width max-height]
|
||||
(exif/getEXIFOrientation
|
||||
img
|
||||
(fn [orientation]
|
||||
(fix-orientation img orientation cb max-width max-height))))
|
||||
|
||||
(defn create-object-url
|
||||
[file]
|
||||
(.createObjectURL (or (.-URL js/window)
|
||||
(.-webkitURL js/window))
|
||||
file))
|
||||
|
||||
;; (defn build-image
|
||||
;; []
|
||||
;; (let [img (js/Image.)]
|
||||
;; ))
|
||||
|
||||
(defn upload
|
||||
[files file-cb & {:keys [max-width max-height]
|
||||
:or {max-width 1920
|
||||
max-height 1080}}]
|
||||
(doseq [file (array-seq files)]
|
||||
(let [file-type (gobj/get file "type")
|
||||
ymd (->> (vals (util/year-month-day-padded))
|
||||
(string/join "_"))
|
||||
file-name (str ymd "_" (gobj/get file "name"))]
|
||||
(if (= 0 (.indexOf type "image/"))
|
||||
(let [img (js/Image.)]
|
||||
(set! (.-onload img)
|
||||
(fn []
|
||||
(get-orientation img
|
||||
(fn [^js off-canvas]
|
||||
(let [file-form-data ^js (js/FormData.)
|
||||
data-url (.toDataURL off-canvas)
|
||||
blob (blob/blob data-url)]
|
||||
(.append file-form-data "file" blob)
|
||||
(file-cb file file-form-data file-name file-type)))
|
||||
max-width
|
||||
max-height)))
|
||||
(set! (.-src img)
|
||||
(create-object-url file)))))))
|
||||
@@ -1,117 +0,0 @@
|
||||
(ns frontend.mixins
|
||||
(:require [rum.core :as rum]
|
||||
[goog.dom :as dom])
|
||||
(:import [goog.events EventHandler]))
|
||||
|
||||
(defn detach
|
||||
"Detach all event listeners."
|
||||
[state]
|
||||
(some-> state ::event-handler .removeAll))
|
||||
|
||||
(defn listen
|
||||
"Register an event `handler` for events of `type` on `target`."
|
||||
[state target type handler & [opts]]
|
||||
(when-let [event-handler (::event-handler state)]
|
||||
(.listen event-handler target (name type) handler (clj->js opts))))
|
||||
|
||||
(def event-handler-mixin
|
||||
"The event handler mixin."
|
||||
{:will-mount
|
||||
(fn [state]
|
||||
(assoc state ::event-handler (EventHandler.)))
|
||||
:will-unmount
|
||||
(fn [state]
|
||||
(detach state)
|
||||
(dissoc state ::event-handler))})
|
||||
|
||||
;; (defn timeout-mixin
|
||||
;; "The setTimeout mixin."
|
||||
;; [name t f]
|
||||
;; {:will-mount
|
||||
;; (fn [state]
|
||||
;; (assoc state name (util/set-timeout t f)))
|
||||
;; :will-unmount
|
||||
;; (fn [state]
|
||||
;; (let [timeout (get state name)]
|
||||
;; (util/clear-timeout timeout)
|
||||
;; (dissoc state name)))})
|
||||
|
||||
;; (defn interval-mixin
|
||||
;; "The setInterval mixin."
|
||||
;; [name t f]
|
||||
;; {:will-mount
|
||||
;; (fn [state]
|
||||
;; (assoc state name (util/set-interval t f)))
|
||||
;; :will-unmount
|
||||
;; (fn [state]
|
||||
;; (when-let [interval (get state name)]
|
||||
;; (util/clear-interval interval))
|
||||
;; (dissoc state name))})
|
||||
|
||||
(defn hide-when-esc-or-outside
|
||||
[state show? & {:keys [on-hide node show-fn]}]
|
||||
(let [node (or node (rum/dom-node state))
|
||||
show? (if (and show-fn (fn? show-fn))
|
||||
(show-fn)
|
||||
@show?)]
|
||||
(when show?
|
||||
(listen state js/window "click"
|
||||
(fn [e]
|
||||
;; If the click target is outside of current node
|
||||
(when-not (dom/contains node (.. e -target))
|
||||
(on-hide e))))
|
||||
|
||||
(listen state js/window "keydown"
|
||||
(fn [e]
|
||||
(case (.-keyCode e)
|
||||
;; Esc
|
||||
27 (on-hide e)
|
||||
nil))))))
|
||||
|
||||
(defn event-mixin
|
||||
([attach-listeners]
|
||||
(event-mixin attach-listeners identity))
|
||||
([attach-listeners init-callback]
|
||||
(merge
|
||||
event-handler-mixin
|
||||
{:init (fn [state props]
|
||||
(init-callback state))
|
||||
:did-mount (fn [state]
|
||||
(attach-listeners state)
|
||||
state)
|
||||
:did-remount (fn [old-state new-state]
|
||||
(detach old-state)
|
||||
(attach-listeners new-state)
|
||||
new-state)})))
|
||||
|
||||
;; TODO: is it possible that multiple nested components using the same key `:open?`?
|
||||
(defn modal
|
||||
[]
|
||||
(let [k :open?]
|
||||
(event-mixin
|
||||
(fn [state]
|
||||
(let [open? (get state k)]
|
||||
(hide-when-esc-or-outside state
|
||||
open?
|
||||
:on-hide (fn []
|
||||
(reset! open? false)))))
|
||||
(fn [state]
|
||||
(let [open? (atom false)
|
||||
component (:rum/react-component state)]
|
||||
(add-watch open? ::open
|
||||
(fn [_ _ _ _]
|
||||
(rum/request-render component)))
|
||||
(assoc state
|
||||
k open?
|
||||
:close-fn (fn []
|
||||
(reset! open? false))
|
||||
:open-fn (fn []
|
||||
(reset! open? true))
|
||||
:toggle-fn (fn []
|
||||
(swap! open? not))))))))
|
||||
|
||||
(defn will-mount-effect
|
||||
[handler]
|
||||
{:will-mount (fn [state]
|
||||
(handler (:rum/args state))
|
||||
state)})
|
||||
@@ -1,16 +0,0 @@
|
||||
(ns frontend.page
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.state :as state]
|
||||
[frontend.handler :as handler]))
|
||||
|
||||
(rum/defc current-page < rum/reactive
|
||||
{:did-mount (fn [state]
|
||||
(handler/set-root-component! (:rum/react-component state))
|
||||
state)}
|
||||
[]
|
||||
(let [state (rum/react state/state)
|
||||
route-match (:route-match state)]
|
||||
(if route-match
|
||||
(when-let [view (:view (:data route-match))]
|
||||
(view route-match))
|
||||
[:div "404 Page"])))
|
||||
@@ -1,37 +0,0 @@
|
||||
(ns frontend.routes
|
||||
(:require [frontend.components.home :as home]
|
||||
[frontend.components.sidebar :as sidebar]
|
||||
[frontend.components.repo :as repo]
|
||||
[frontend.components.file :as file]
|
||||
[frontend.components.agenda :as agenda]
|
||||
))
|
||||
|
||||
(def routes
|
||||
[["/"
|
||||
{:name :home
|
||||
:view home/home}]
|
||||
|
||||
["/repo/add"
|
||||
{:name :repo-add
|
||||
:view repo/add-repo}]
|
||||
|
||||
["/file/:path"
|
||||
{:name :file
|
||||
:view file/file}]
|
||||
|
||||
["/file/:path/edit"
|
||||
{:name :file-edit
|
||||
:view file/edit}]
|
||||
|
||||
["/agenda"
|
||||
{:name :agenda
|
||||
:view agenda/agenda}]
|
||||
|
||||
;; TODO: edit file
|
||||
;; Settings
|
||||
;; ["/item/:id"
|
||||
;; {:name ::item
|
||||
;; :view item-page
|
||||
;; :parameters {:path {:id int?}
|
||||
;; :query {(ds/opt :foo) keyword?}}}]
|
||||
])
|
||||
@@ -1,59 +0,0 @@
|
||||
(ns frontend.rum
|
||||
(:require [clojure.string :as s]
|
||||
[clojure.set :as set]
|
||||
[clojure.walk :as w]))
|
||||
|
||||
;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs
|
||||
|
||||
(defn kebab-case->camel-case
|
||||
"Converts from kebab case to camel case, eg: on-click to onClick"
|
||||
[input]
|
||||
(let [words (s/split input #"-")
|
||||
capitalize (->> (rest words)
|
||||
(map #(apply str (s/upper-case (first %)) (rest %))))]
|
||||
(apply str (first words) capitalize)))
|
||||
|
||||
(defn map-keys->camel-case
|
||||
"Stringifys all the keys of a cljs hashmap and converts them
|
||||
from kebab case to camel case. If :html-props option is specified,
|
||||
then rename the html properties values to their dom equivalent
|
||||
before conversion"
|
||||
[data & {:keys [html-props]}]
|
||||
(let [convert-to-camel (fn [[key value]]
|
||||
[(kebab-case->camel-case (name key)) value])]
|
||||
(w/postwalk (fn [x]
|
||||
(if (map? x)
|
||||
(let [new-map (if html-props
|
||||
(set/rename-keys x {:class :className :for :htmlFor})
|
||||
x)]
|
||||
(into {} (map convert-to-camel new-map)))
|
||||
x))
|
||||
data)))
|
||||
|
||||
;; adapted from https://github.com/tonsky/rum/issues/20
|
||||
(defn adapt-class [react-class]
|
||||
(fn [& args]
|
||||
(let [[opts children] (if (map? (first args))
|
||||
[(first args) (rest args)]
|
||||
[{} args])
|
||||
type# (first children)
|
||||
;; we have to make sure to check if the children is sequential
|
||||
;; as a list can be returned, eg: from a (for)
|
||||
new-children (if (sequential? type#)
|
||||
(let [result (sablono.interpreter/interpret children)]
|
||||
(if (sequential? result)
|
||||
result
|
||||
[result]))
|
||||
children)
|
||||
;; convert any options key value to a react element, if
|
||||
;; a valid html element tag is used, using sablono
|
||||
vector->react-elems (fn [[key val]]
|
||||
(if (sequential? val)
|
||||
[key (sablono.interpreter/interpret val)]
|
||||
[key val]))
|
||||
new-options (into {} (map vector->react-elems opts))]
|
||||
;; (.dir js/console new-children)
|
||||
(apply js/React.createElement react-class
|
||||
;; sablono html-to-dom-attrs does not work for nested hashmaps
|
||||
(clj->js (map-keys->camel-case new-options :html-props true))
|
||||
new-children))))
|
||||
@@ -1,12 +0,0 @@
|
||||
(ns frontend.state)
|
||||
|
||||
;; TODO: replace this with datascript
|
||||
(def state (atom
|
||||
{:route-match nil
|
||||
:notification/show? false
|
||||
:notification/text nil
|
||||
:root-component nil
|
||||
:git-status nil
|
||||
:git-error nil
|
||||
:edit? false
|
||||
:latest-journals []}))
|
||||
@@ -1,20 +0,0 @@
|
||||
(ns frontend.storage
|
||||
(:refer-clojure :exclude [get set remove])
|
||||
(:require [cljs.reader :as reader]))
|
||||
|
||||
;; TODO: deprecate this, will persistent datascript
|
||||
(defn get
|
||||
[key]
|
||||
(reader/read-string ^js (.getItem js/localStorage (name key))))
|
||||
|
||||
(defn set
|
||||
[key value]
|
||||
(.setItem ^js js/localStorage (name key) (pr-str value)))
|
||||
|
||||
(defn remove
|
||||
[key]
|
||||
(.removeItem ^js js/localStorage (name key)))
|
||||
|
||||
(defn clear
|
||||
[]
|
||||
(.clear ^js js/localStorage))
|
||||
@@ -1,142 +0,0 @@
|
||||
(ns frontend.ui
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.rum :as r]
|
||||
["react-transition-group" :refer [TransitionGroup CSSTransition]]
|
||||
["react-textarea-autosize" :as Textarea]
|
||||
[frontend.util :as util]
|
||||
[frontend.mixins :as mixins]
|
||||
[frontend.state :as state]
|
||||
[goog.object :as gobj]
|
||||
[goog.dom :as gdom]))
|
||||
|
||||
(defonce transition-group (r/adapt-class TransitionGroup))
|
||||
(defonce css-transition (r/adapt-class CSSTransition))
|
||||
|
||||
(defonce textarea-autosize (r/adapt-class (gobj/get Textarea "default")))
|
||||
|
||||
(rum/defc dropdown-content-wrapper [state content]
|
||||
[:div.origin-top-right.absolute.right-0.mt-2.w-48.rounded-md.shadow-lg
|
||||
{:class (case state
|
||||
"entering" "transition ease-out duration-100 transform opacity-0 scale-95"
|
||||
"entered" "transition ease-out duration-100 transform opacity-100 scale-100"
|
||||
"exiting" "transition ease-in duration-75 transform opacity-100 scale-100"
|
||||
"exited" "transition ease-in duration-75 transform opacity-0 scale-95")}
|
||||
content])
|
||||
|
||||
;; public exports
|
||||
(rum/defcs dropdown < (mixins/modal)
|
||||
[state content]
|
||||
(let [{:keys [open? toggle-fn]} state]
|
||||
[:div.ml-3.relative
|
||||
[:div
|
||||
[:button.max-w-xs.flex.items-center.text-sm.rounded-full.focus:outline-none.focus:shadow-outline
|
||||
{:on-click toggle-fn}
|
||||
[:img.h-8.w-8.rounded-full
|
||||
{:src
|
||||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"}]]]
|
||||
(css-transition
|
||||
{:in @open? :timeout 0}
|
||||
(fn [state]
|
||||
(dropdown-content-wrapper state content)))]))
|
||||
|
||||
(defn dropdown-with-links
|
||||
[links]
|
||||
(dropdown
|
||||
[:div.py-1.rounded-md.bg-white.shadow-xs
|
||||
(for [{:keys [options title]} links]
|
||||
[:a.block.px-4.py-2.text-sm.text-gray-700.hover:bg-gray-100.transition.ease-in-out.duration-150
|
||||
(merge {:key (cljs.core/random-uuid)}
|
||||
options)
|
||||
title])]))
|
||||
|
||||
(rum/defc button
|
||||
[text on-click]
|
||||
[:button.inline-flex.items-center.px-3.py-2.border.border-transparent.text-sm.leading-4.font-medium.rounded-md.text-white.bg-indigo-600.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.active:bg-indigo-700.transition.ease-in-out.duration-150.mt-1
|
||||
{:type "button"
|
||||
:on-click on-click}
|
||||
text])
|
||||
|
||||
(rum/defc notification-content
|
||||
[state text]
|
||||
[:div.fixed.inset-0.flex.items-end.justify-center.px-4.py-6.pointer-events-none.sm:p-6.sm:items-start.sm:justify-end
|
||||
[:div.max-w-sm.w-full.bg-white.shadow-lg.rounded-lg.pointer-events-auto
|
||||
{:class (case state
|
||||
"entering" "transition ease-out duration-300 transform opacity-0 translate-y-2 sm:translate-x-0"
|
||||
"entered" "transition ease-out duration-300 transform translate-y-0 opacity-100 sm:translate-x-0"
|
||||
"exiting" "transition ease-in duration-100 opacity-100"
|
||||
"exited" "transition ease-in duration-100 opacity-0")}
|
||||
[:div.rounded-lg.shadow-xs.overflow-hidden
|
||||
[:div.p-4
|
||||
[:div.flex.items-start
|
||||
[:div.flex-shrink-0
|
||||
[:svg.h-6.w-6.text-green-400
|
||||
{:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
|
||||
[:path
|
||||
{:d "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
|
||||
:stroke-width "2",
|
||||
:stroke-linejoin "round",
|
||||
:stroke-linecap "round"}]]]
|
||||
[:div.ml-3.w-0.flex-1.pt-0.5
|
||||
[:p.text-sm.leading-5.font-medium.text-gray-900
|
||||
text]]
|
||||
[:div.ml-4.flex-shrink-0.flex
|
||||
[:button.inline-flex.text-gray-400.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150
|
||||
{:on-click (fn []
|
||||
(swap! state/state assoc :notification/show? false))}
|
||||
[:svg.h-5.w-5
|
||||
{:fill "currentColor", :viewBox "0 0 20 20"}
|
||||
[:path
|
||||
{:clip-rule "evenodd",
|
||||
:d
|
||||
"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z",
|
||||
:fill-rule "evenodd"}]]]]]]]]])
|
||||
|
||||
(rum/defc notification < rum/reactive
|
||||
[]
|
||||
(let [{:keys [:notification/show? :notification/text]} (rum/react state/state)]
|
||||
(css-transition
|
||||
{:in show? :timeout 100}
|
||||
(fn [state]
|
||||
(notification-content state text)))))
|
||||
|
||||
(rum/defc checkbox
|
||||
[option]
|
||||
[:input.form-checkbox.h-4.w-4.text-indigo-600.transition.duration-150.ease-in-out
|
||||
(merge {:type "checkbox"} option)])
|
||||
|
||||
(rum/defc badge
|
||||
[text option]
|
||||
[:span.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-medium.leading-4.bg-purple-100.text-purple-800
|
||||
option
|
||||
text])
|
||||
|
||||
;; scroll
|
||||
(defn main-node
|
||||
[]
|
||||
(first (array-seq (js/document.querySelectorAll "main"))))
|
||||
|
||||
(defn get-scroll-top []
|
||||
(.-scrollTop (main-node)))
|
||||
|
||||
(defn on-scroll
|
||||
[on-load]
|
||||
(let [node (main-node)
|
||||
full-height (gobj/get node "scrollHeight")
|
||||
scroll-top (gobj/get node "scrollTop")
|
||||
bottom-reached? (>= (- full-height scroll-top 300) 0)]
|
||||
(when bottom-reached?
|
||||
(on-load))))
|
||||
|
||||
(defn attach-listeners
|
||||
"Attach scroll and resize listeners."
|
||||
[state]
|
||||
(let [opts (-> state :rum/args second)
|
||||
debounced-on-scroll (util/debounce 500 #(on-scroll (:on-load opts)))]
|
||||
(mixins/listen state (main-node) :scroll debounced-on-scroll)))
|
||||
|
||||
(rum/defcs infinite-list <
|
||||
(mixins/event-mixin attach-listeners)
|
||||
"Render an infinite list."
|
||||
[state body {:keys [on-load]
|
||||
:as opts}]
|
||||
body)
|
||||
@@ -1,31 +0,0 @@
|
||||
(ns frontend.utf8
|
||||
(:require [goog.object :as gobj]))
|
||||
|
||||
(defonce encoder
|
||||
(js/TextEncoder. "utf-8"))
|
||||
|
||||
(defonce decoder
|
||||
(js/TextDecoder. "utf-8"))
|
||||
|
||||
(defn encode
|
||||
[s]
|
||||
(.encode encoder s))
|
||||
|
||||
(defn substring
|
||||
([arr start]
|
||||
(->> (.subarray arr start)
|
||||
(.decode decoder)))
|
||||
([arr start end]
|
||||
(->> (.subarray arr start end)
|
||||
(.decode decoder))))
|
||||
|
||||
(defn length
|
||||
[arr]
|
||||
(gobj/get arr "length"))
|
||||
|
||||
(defn insert!
|
||||
[s start-pos end-pos content]
|
||||
(let [arr (encode s)]
|
||||
(str (substring arr 0 start-pos)
|
||||
content
|
||||
(substring arr end-pos))))
|
||||
@@ -1,184 +0,0 @@
|
||||
(ns frontend.util
|
||||
(:require [goog.object :as gobj]
|
||||
[promesa.core :as p]
|
||||
[clojure.walk :as walk]
|
||||
[clojure.string :as string]
|
||||
[cljs-bean.core :as bean]))
|
||||
|
||||
(defn evalue
|
||||
[event]
|
||||
(gobj/getValueByKeys event "target" "value"))
|
||||
|
||||
(defn p-handle
|
||||
([p ok-handler]
|
||||
(p-handle p ok-handler (fn [error] (prn "p-handle error: " error))))
|
||||
([p ok-handler error-handler]
|
||||
(-> p
|
||||
(p/then (fn [result]
|
||||
(ok-handler result)))
|
||||
(p/catch (fn [error]
|
||||
(error-handler error))))))
|
||||
|
||||
(defn get-width
|
||||
[]
|
||||
(gobj/get js/window "innerWidth"))
|
||||
|
||||
(defn listen
|
||||
"Register an event `handler` for events of `type` on `target`."
|
||||
[event-handler target type handler & [opts]]
|
||||
(.listen event-handler target (name type) handler (clj->js opts)))
|
||||
|
||||
(defn indexed
|
||||
[coll]
|
||||
(map-indexed vector coll))
|
||||
|
||||
(defn find-first
|
||||
[pred coll]
|
||||
(first (filter pred coll)))
|
||||
|
||||
(defn get-local-date
|
||||
[]
|
||||
(let [date (js/Date.)
|
||||
year (.getFullYear date)
|
||||
month (inc (.getMonth date))
|
||||
day (.getDate date)
|
||||
hour (.getHours date)
|
||||
minute (.getMinutes date)]
|
||||
{:year year
|
||||
:month month
|
||||
:day day
|
||||
:hour hour
|
||||
:minute minute}))
|
||||
|
||||
(defn dissoc-in
|
||||
"Dissociates an entry from a nested associative structure returning a new
|
||||
nested structure. keys is a sequence of keys. Any empty maps that result
|
||||
will not be present in the new structure."
|
||||
[m [k & ks :as keys]]
|
||||
(if ks
|
||||
(if-let [nextmap (get m k)]
|
||||
(let [newmap (dissoc-in nextmap ks)]
|
||||
(if (seq newmap)
|
||||
(assoc m k newmap)
|
||||
(dissoc m k)))
|
||||
m)
|
||||
(dissoc m k)))
|
||||
|
||||
(defn format
|
||||
[fmt & args]
|
||||
(apply goog.string/format fmt args))
|
||||
|
||||
(defn raw-html
|
||||
[content]
|
||||
[:div {:dangerouslySetInnerHTML
|
||||
{:__html content}}])
|
||||
|
||||
(defn json->clj
|
||||
[json-string]
|
||||
(-> json-string
|
||||
(js/JSON.parse)
|
||||
(js->clj :keywordize-keys true)))
|
||||
|
||||
(defn remove-nils
|
||||
"remove pairs of key-value that has nil value from a (possibly nested) map. also transform map to nil if all of its value are nil"
|
||||
[nm]
|
||||
(walk/postwalk
|
||||
(fn [el]
|
||||
(if (map? el)
|
||||
(not-empty (into {} (remove (comp nil? second)) el))
|
||||
el))
|
||||
nm))
|
||||
|
||||
(defn index-by
|
||||
[col k]
|
||||
(->> (map (fn [entry] [(get entry k) entry])
|
||||
col)
|
||||
(into {})))
|
||||
|
||||
;; ".lg:absolute.lg:inset-y-0.lg:right-0.lg:w-1/2"
|
||||
(defn hiccup->class
|
||||
[class]
|
||||
(some->> (string/split class #"\.")
|
||||
(string/join " ")
|
||||
(string/trim)))
|
||||
|
||||
(defn fetch
|
||||
([url on-ok on-failed]
|
||||
(fetch url #js {} on-ok on-failed))
|
||||
([url opts on-ok on-failed]
|
||||
(-> (js/fetch url opts)
|
||||
(.then #(if (.-ok %)
|
||||
(.json %)
|
||||
(on-failed %)))
|
||||
(.then bean/->clj)
|
||||
(.then #(on-ok %)))))
|
||||
|
||||
(defn get-weekday
|
||||
[date]
|
||||
(.toLocaleString date "en-us" (clj->js {:weekday "long"})))
|
||||
|
||||
(defn get-date
|
||||
[]
|
||||
(let [date (js/Date.)]
|
||||
{:year (.getFullYear date)
|
||||
:month (inc (.getMonth date))
|
||||
:day (.getDate date)
|
||||
:weekday (get-weekday date)}))
|
||||
|
||||
(defn journals-path
|
||||
[year month]
|
||||
(let [month (if (< month 10) (str "0" month) month)]
|
||||
(str "journals/" year "_" month ".org")))
|
||||
|
||||
(defn current-journal-path
|
||||
[]
|
||||
(let [{:keys [year month]} (get-date)]
|
||||
(journals-path year month)))
|
||||
|
||||
(defn today
|
||||
[]
|
||||
(.toLocaleDateString (js/Date.) "default"
|
||||
(clj->js {:month "long"
|
||||
:year "numeric"
|
||||
:day "numeric"
|
||||
:weekday "long"})))
|
||||
|
||||
(defn zero-pad
|
||||
[n]
|
||||
(if (< n 10)
|
||||
(str "0" n)
|
||||
(str n)))
|
||||
|
||||
(defn year-month-day-padded
|
||||
[]
|
||||
(let [{:keys [year month day]} (get-date)]
|
||||
{:year year
|
||||
:month (zero-pad month)
|
||||
:day (zero-pad day)}))
|
||||
|
||||
(defn get-month-last-day
|
||||
[]
|
||||
(let [today (js/Date.)
|
||||
date (js/Date. (.getFullYear today) (inc (.getMonth today)) 0)]
|
||||
(.getDate date)))
|
||||
|
||||
(defn parse-int
|
||||
[x]
|
||||
(if (string? x)
|
||||
(js/parseInt x)
|
||||
x))
|
||||
|
||||
(defn debounce
|
||||
"Returns a function that will call f only after threshold has passed without new calls
|
||||
to the function. Calls prep-fn on the args in a sync way, which can be used for things like
|
||||
calling .persist on the event object to be able to access the event attributes in f"
|
||||
([threshold f] (debounce threshold f (constantly nil)))
|
||||
([threshold f prep-fn]
|
||||
(let [t (atom nil)]
|
||||
(fn [& args]
|
||||
(when @t (js/clearTimeout @t))
|
||||
(apply prep-fn args)
|
||||
(reset! t (js/setTimeout #(do
|
||||
(reset! t nil)
|
||||
(apply f args))
|
||||
threshold))))))
|
||||
Reference in New Issue
Block a user