Skip to content

Commit a6b89c2

Browse files
committed
Feat: text pagination.
- Add[dx]: seed fn for article on empty db (in dev). - Add[db]: "current_page" to article schema (default to 0) - Add[db]: pagination when attaching words to an article. - Add[ui]: pagination buttons in article. - Add[ui]: settings for "words-per-page" - Add[db]: "hook" to run whenever settings are updated.
1 parent e08fce9 commit a6b89c2

File tree

11 files changed

+517
-84
lines changed

11 files changed

+517
-84
lines changed

src/app/main/db.cljs

Lines changed: 110 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[app.shared.specs :as specs]
55
["better-sqlite3" :as sqlite]
66
[clojure.string :as str]
7+
[clojure.data :as data]
78
["electron" :refer [app]]
89
["fs" :as fs]
910
["path" :as path]))
@@ -12,15 +13,6 @@
1213
(def db-path (.join path (.getPath app "userData") db-name))
1314
(def db (sqlite. db-path))
1415

15-
(defn wipe!
16-
"Wipes the database and relaunches the application."
17-
[]
18-
(.exec db "DROP TABLE words;
19-
DROP TABLE articles;
20-
DROP TABLE phrases;
21-
DROP TABLE settings;" #(println %))
22-
(.relaunch app)
23-
(.quit app))
2416

2517
;; (defn remove-db-file! []
2618
;; (.unlink fs db-path #(when % (prn "Failed to delete db file") %)))
@@ -73,7 +65,8 @@
7365
word_ids TEXT,
7466
language TEXT,
7567
date_created INTEGER,
76-
last_opened INTEGER
68+
last_opened INTEGER,
69+
current_page INTEGER DEFAULT 0
7770
);
7871
7972
CREATE TABLE IF NOT EXISTS settings (
@@ -110,7 +103,65 @@
110103
js->clj
111104
(get "version")))
112105

113-
;; -- DB calls -----------------------------------------------------------------
106+
107+
(defn read-sample-file
108+
[name]
109+
(-> (.readFileSync ^:export fs (.join path js/__dirname ".." "test/sample_texts/" name) "utf8")))
110+
111+
112+
113+
;; -- DB: Settings -------------------------------------------------------------
114+
;; Settings are handled in JSON. For better or worse: ¯\_(ツ)_/¯
115+
;; All settings updates/inserts need to be jsonified.
116+
117+
118+
(defn- settings->json
119+
[s]
120+
(-> s clj->js js/JSON.stringify))
121+
122+
(defn- settings->edn
123+
[json-from-db]
124+
(-> json-from-db js/JSON.parse js->clj))
125+
126+
(defn settings-get
127+
[]
128+
(let [res (sql {:op :get :stmt "SELECT user FROM settings"})]
129+
(-> res :user settings->edn)))
130+
131+
(defn settings-hook!
132+
"since we batch update settings as json (for better or worse)) we need to
133+
sometimes handle side-effectful things, so we diff the new settings
134+
against the old, and run 'hooks' based on what changed."
135+
[new-settings-from-fe]
136+
(let [old-settings (settings-get)
137+
;; normalize new-settings to look like it came from the db before we diff it.
138+
new-settings (-> new-settings-from-fe settings->json settings->edn)
139+
[old-diff new-diff both] (data/diff new-settings old-settings)]
140+
(cond
141+
(contains? new-diff "page-size")
142+
(sql {:op :run :params [] :stmt "UPDATE articles SET current_page = 0"})
143+
144+
:else nil
145+
)))
146+
147+
(defn settings-update
148+
[settings]
149+
(settings-hook! settings)
150+
(sql {:op :run
151+
:stmt "UPDATE settings SET user = ? WHERE settings_id = 1"
152+
:params [(settings->json settings)]}))
153+
154+
(defn settings-init
155+
"If there are no rows in the settings table, initialize it."
156+
[]
157+
(let [default-settings (settings->json (specs/make-default-settings trunk-version))
158+
existing-settings (settings-get)]
159+
(when-not existing-settings
160+
(sql {:op :run
161+
:stmt "INSERT INTO settings(user) VALUES (?)"
162+
:params [default-settings]}))))
163+
164+
;; -- DB: Articles -------------------------------------------------------------
114165

115166
(defn article-get-by-id
116167
[{:keys [article_id language]}]
@@ -131,24 +182,31 @@
131182
:op :all}))
132183

133184
(defn article-update-last-opened
134-
[{:keys [article_id]}]
135-
(sql {:stmt "UPDATE articles SET last_opened = ? WHERE article_ID = ?"
136-
:op :run
137-
:params [(js/Date.now) article_id]}))
185+
[{:keys [article_id current_page]}]
186+
(let [current_page (if (< current_page 0) 0 current_page)]
187+
(sql {:stmt "UPDATE articles SET last_opened = ?, current_page = ? WHERE article_ID = ?"
188+
:op :run
189+
:params [(js/Date.now) current_page article_id]})))
138190

139191
(defn article-attach-words
140192
"When given an article, it fetches the word data for each word from the DB and
141193
attaches it back to the article."
142194
[article]
143-
(let [word-ids (get article :word_ids)
195+
(let [page-size (get (settings-get) "page-size" 1000) ; aka 'limit'
196+
curr-page (get article :current_page 0)
197+
word-ids (get article :word_ids)
144198
words-ids-vec (u/split-delimited-article word-ids)
199+
total-pages (/ (count words-ids-vec) page-size)
200+
;; paginate the words we return.
201+
words-ids-slice (u/paginate-vector words-ids-vec page-size curr-page)
145202
words-out (atom [])]
146-
(doseq [word-id words-ids-vec]
203+
(doseq [word-id words-ids-slice]
147204
(let [res (sql {:stmt "SELECT * FROM words WHERE id = ?"
148205
:params [word-id]
149206
:op :get})]
150207
(swap! words-out conj res)))
151-
(assoc article :word-data @words-out)))
208+
(assoc article :word-data @words-out :total-pages (js/Math.ceil total-pages)))
209+
)
152210

153211
(defn article-insert
154212
"Creates a new article. Requirements:
@@ -286,45 +344,53 @@
286344
(assoc article :word-data @new-word-data))))
287345

288346

289-
;; -- DB: Settings -------------------------------------------------------------
290-
;; All settings updates/inserts need to be jsonified.
291-
(defn- settings->json
292-
[s]
293-
(-> s clj->js js/JSON.stringify))
294347

295-
(defn- settings->edn
296-
[json-from-db]
297-
(-> json-from-db js/JSON.parse js->clj))
348+
;; ----------------------------------------------------------------------------------------
349+
;; Seed fns
298350

299-
(defn settings-get
351+
352+
(defn seed-article
300353
[]
301-
(let [res (sql {:op :get :stmt "SELECT user FROM settings"})]
302-
(-> res :user settings->edn)))
354+
(let [
355+
data {:article (read-sample-file "fr_compte2.txt") :title "Compte, Ch 2", :source "..", :language "fr"}
356+
_ (words-insert data)
357+
word-ids-str (words-get-ids-for-article data)
358+
inserted-article (article-insert (merge data {:word_ids word-ids-str}))])
359+
)
303360

304-
(defn settings-update
305-
[settings]
306-
(sql {:op :run
307-
:stmt "UPDATE settings SET user = ? WHERE settings_id = 1"
308-
:params [(settings->json settings)]}))
361+
(defn run-seeds
309362

310-
(defn settings-init
311-
"If there are no rows in the settings table, initialize it."
312363
[]
313-
(let [default-settings (settings->json (specs/make-default-settings trunk-version))
314-
existing-settings (settings-get)]
315-
(when-not existing-settings
316-
(sql {:op :run
317-
:stmt "INSERT INTO settings(user) VALUES (?)"
318-
:params [default-settings]}))))
364+
(let [articles (sql {:op :get :stmt "SELECT * FROM articles" :params []})
365+
words (sql {:op :get :stmt "SELECT * FROM words" :params []})
366+
no-content-yet (and (= (count articles) 0)
367+
(= (count words) 0))]
368+
(when no-content-yet
369+
(seed-article))))
370+
371+
372+
;; Wipe / Init -----------------------------------------------------------------------------
373+
374+
(defn wipe!
375+
"Wipes the database and relaunches the application."
376+
[]
377+
(.exec db "DROP TABLE words;
378+
DROP TABLE articles;
379+
DROP TABLE phrases;
380+
DROP TABLE settings;" #(println %))
381+
(.relaunch app)
382+
(.quit app)
383+
)
319384

320-
;; -----------------------------------------------------------------------------
321385

322386
(defn init
323387
[]
324388
(.exec db db-seed
325389
(fn [err]
326390
(when err
327391
(throw (js/Error. (str "Failed db" err))))))
328-
(settings-init))
392+
(settings-init)
393+
(when u/debug? (run-seeds))
394+
)
329395

330396
;; FIXME: when do I run "db.close()"?

src/app/main/ipc.cljs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@
6464
(catch js/Error e
6565
(reply-err! event "Failed to get article." e))))
6666

67+
;; TODO dedupe with article-get
68+
(s-ev :article-change-page)
69+
(fn [event data]
70+
(try
71+
(db/article-update-last-opened data)
72+
(->> data
73+
db/article-get-by-id
74+
db/article-attach-words
75+
db/article-attach-phrases
76+
(reply! event (s-ev :article-received)))
77+
(catch js/Error e
78+
(reply-err! event "Failed to get article." e)))
79+
)
80+
6781
;; -- WORDS HANDLERS ---------------------------
6882

6983
(s-ev :words-get)

src/app/renderer/components.cljs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
(let [styles "mt-2 mb-2 flex border w-64 py-1 rounded dark:bg-gray-800 dark:text-white outline-none"]
2525
[:select (merge {:class styles} props) options]))
2626

27-
2827
(defn erase-db
2928
"Button for erasing the database"
3029
[]
@@ -96,15 +95,22 @@
9695
(defn ext-link
9796
[{:keys [link text]}]
9897
(let [handle-click (fn [e]
99-
(.preventDefault e )
98+
(.preventDefault e)
10099
(.openExternal (.-shell (js/require "electron")) link))]
101100
[:a.text-blue-600.dark:text-blue-400.cursor-pointer {:on-click handle-click} text]))
102101

103102
(defn button
104-
[{:keys [on-click text icon-name icon-size]}]
105-
(let [styles "dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 self-start text-xs bg-white hover:bg-gray-100 text-gray-800 py-1 px-2 border border-gray-400 rounded shadow"]
103+
[{:keys [on-click text icon-name icon-size disabled? style]
104+
:or {disabled? false style ""}}]
105+
(let [style (case style
106+
"primary" "bg-blue-400 hover:bg-blue-500 text-white border-none font-bold"
107+
"" "bg-white text-gray-800 hover:bg-gray-100 dark:text-white dark:bg-gray-800 dark:hover:bg-gray-700")
108+
styles (str style " self-start text-xs py-1 px-2 border border-gray-400 rounded shadow " (when disabled? "cursor-not-allowed"))]
109+
;; styles (str "dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 self-start text-xs bg-white hover:bg-gray-100 text-gray-800 py-1 px-2 border border-gray-400 rounded shadow " (when disabled? "cursor-not-allowed"))]
106110
[:button
107-
{:class styles :on-click on-click}
111+
{:class styles
112+
:on-click on-click
113+
:disabled disabled?}
108114
(if icon-name
109115
[:span [icon {:icon icon-name :size (or icon-size 18)}] [:span text]]
110116
text)]))
@@ -245,10 +251,10 @@
245251
is-phrase (or currently-selected-phrase
246252
(u/is-phrase word-or-phrase))
247253
handle-submit (fn [e]
248-
(.preventDefault e)
249-
(if is-phrase
250-
(|> [(s-ev :phrase-update) @form])
251-
(|> [(s-ev :word-update) @form])))]
254+
(.preventDefault e)
255+
(if is-phrase
256+
(|> [(s-ev :phrase-update) @form])
257+
(|> [(s-ev :word-update) @form])))]
252258
[:div {:class "bg-gray-50 w-full border-t md:border-t-0 md:flex md:w-2/5 md:relative border-l dark:border-gray-900 dark:bg-gray-800 dark:border-gray-700"}
253259
(when word-or-phrase
254260
[:div {:class "dark:bg-gray-800 w-full p-8 flex flex-col mx-auto"}

src/app/renderer/events.cljs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,22 @@
161161
{:db (assoc db :loading? true)
162162
::ipc-send! (with-lang db event)}))
163163

164+
(r-fx (s-ev :article-change-page)
165+
(fn [{:keys [db]} [ev-name dir]]
166+
(let [curr-a (-> db :current-article (dissoc :word-data))
167+
dir-fn (if (= dir :next) inc dec)
168+
success-out {:db (assoc db :loading? true)
169+
::ipc-send! [ev-name
170+
(update curr-a :current_page dir-fn)]}
171+
172+
{:keys [current_page total-pages]} (db :current-article)
173+
current_page (inc current_page)
174+
]
175+
(cond
176+
(and (= dir :next) (< current_page total-pages)) success-out
177+
(and (= dir :prev) (> current_page 1)) success-out
178+
:else {:db db}))))
179+
164180
(r-fx (s-ev :article-delete)
165181
(fn [{:keys [db]} event]
166182
{:db (-> db (assoc :loading? true))

src/app/renderer/views/article.cljs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@
77
[reagent.core :as r]
88
[app.shared.util :as u]))
99

10+
(defn pagination
11+
[{:keys [current_page total-pages]}]
12+
(let [curr-page (inc current_page)]
13+
[:div.text-sm.flex.items-center.justify-between.border-t.border-gray-300.dark:border-gray-700
14+
[:div.p-3
15+
[component/button {:text "← Previous page"
16+
:disabled? (= curr-page 1)
17+
:on-click #(|> [(s-ev :article-change-page) :prev])}]]
18+
[:div "Page " curr-page " / " total-pages]
19+
[:div.p-3
20+
[component/button {:text "Next page →"
21+
:disabled? (= curr-page total-pages)
22+
:on-click #(|> [(s-ev :article-change-page) :next])}]]]))
23+
1024
(defn view
1125
"Displays a single article."
1226
[]
@@ -24,9 +38,9 @@
2438
form (r/atom word-or-phrase)
2539
total-words (count (get current-article :word-data))
2640
words-known (count (filter (fn [word-data]
27-
(or (not= 0 (word-data :comfort))
28-
(not= nil (word-data :translation))))
29-
(-> current-article :word-data)))
41+
(or (not= 0 (word-data :comfort))
42+
(not= nil (word-data :translation))))
43+
(-> current-article :word-data)))
3044
;; -- handlers -----
3145

3246
handle-mark-all-known (fn []
@@ -54,15 +68,16 @@
5468
(case @sure-mark? 0 "Mark all known?" 1 "You sure?")])]
5569

5670
[:article {:key "view-article" :class "flex overflow-auto flex-col flex-1 bg-white dark:bg-gray-900"}
57-
[:div.leading-8.p-8.flex.flex-wrap.max-w-5xl.mx-auto
58-
{:style {:user-select (if shift-held? "none" "inherit")}}
59-
(map-indexed (fn [index word]
60-
^{:key (str word "-" index)}
61-
[component/article-word
62-
{:word word
63-
:current-word current-word
64-
:on-click #(handle-word-click word index)
65-
:index index
66-
:current-phrase-idxs current-phrase-idxs
67-
:current-word-idx current-word-idx}]) word-data)]]]
71+
[:div.leading-8.p-8.flex.flex-wrap.max-w-5xl.mx-auto
72+
{:style {:user-select (if shift-held? "none" "inherit")}}
73+
(map-indexed (fn [index word]
74+
^{:key (str word "-" index)}
75+
[component/article-word
76+
{:word word
77+
:current-word current-word
78+
:on-click #(handle-word-click word index)
79+
:index index
80+
:current-phrase-idxs current-phrase-idxs
81+
:current-word-idx current-word-idx}]) word-data)]]
82+
[pagination current-article]]
6883
[component/view-current-word {:current-word current-word :form form}]])))))

0 commit comments

Comments
 (0)