Refactor to use deps.edn

This commit is contained in:
Tienson Qin
2020-04-10 13:39:45 +08:00
parent 88c14f70fa
commit db73251f82
53 changed files with 21 additions and 3470 deletions

6
.gitignore vendored
View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
yarn install
yarn release
clj -A:uberdeps

View File

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

View File

@@ -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",

View File

@@ -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

View File

@@ -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
View File

@@ -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/

View File

@@ -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"]}

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -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

View File

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

View File

@@ -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")])))

View File

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

View File

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

View File

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

View File

@@ -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!))})]))

View File

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

View File

@@ -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")])))

View File

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

View File

@@ -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/")))

View File

@@ -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"))

View File

@@ -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"}])))

View File

@@ -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));
});
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
(ns frontend.format.protocol)
(defprotocol Format
(toHtml [this] [this config]))

View File

@@ -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")
)

View File

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

View File

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

View File

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

View 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)})

View File

@@ -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"])))

View File

@@ -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?}}}]
])

View File

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

View File

@@ -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 []}))

View File

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

View File

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

View File

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

View File

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