From f346ec36fb4b51964b63f84483a649ae19df6fc9 Mon Sep 17 00:00:00 2001 From: Tushar Chandra Date: Tue, 2 Jan 2024 21:39:28 -0600 Subject: [PATCH] merge: fix conflicts with rewritten version --- .gitignore | 49 +-- .hotreload | 0 biome.json | 23 +- bun.lockb | Bin 58990 -> 24400 bytes main.js | 147 ++++++++ manifest.json | 9 +- package.json | 42 +-- src/main.ts | 192 ++++++---- src/readwise.ts | 917 ++++++++++++++++++++++++++++++++++++++++++++++ src/vite-env.d.ts | 1 + tsconfig.json | 40 +- vite.config.ts | 23 ++ 12 files changed, 1270 insertions(+), 173 deletions(-) create mode 100644 .hotreload create mode 100644 main.js create mode 100644 src/readwise.ts create mode 100644 src/vite-env.d.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index 47231ae..3cb2fbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,26 @@ -# vscode -.vscode - -# Intellij -*.iml -.idea - -# npm -node_modules - -# Don't include the compiled main.js file in the repo. -# They should be uploaded to GitHub releases instead. -main.js - -# Exclude sourcemaps -*.map - -# obsidian -data.json - -# Exclude macOS Finder (System Explorer) View States -.DS_Store -tsconfig.tsbuildinfo +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +data.json diff --git a/.hotreload b/.hotreload new file mode 100644 index 0000000..e69de29 diff --git a/biome.json b/biome.json index fc0f903..ae8b4d8 100644 --- a/biome.json +++ b/biome.json @@ -1,27 +1,12 @@ { "$schema": "https://biomejs.dev/schemas/1.4.1/schema.json", - "organizeImports": { - "enabled": true - }, + "organizeImports": { "enabled": true }, "linter": { "enabled": true, - "rules": { - "recommended": true - } + "rules": { "recommended": true } }, + "formatter": { "indentStyle": "space" }, "javascript": { - "formatter": { - "enabled": true, - "indentWidth": 2, - "indentStyle": "space", - "quoteStyle": "single" - } - }, - "json": { - "formatter": { - "enabled": true, - "indentWidth": 2, - "indentStyle": "space" - } + "formatter": { "quoteStyle": "single" } } } diff --git a/bun.lockb b/bun.lockb index 6842c23180944d0b75c0a297b2b9c7e579db258f..16a35ab7f88cb1dfa20ce4e626f00691601a96a2 100755 GIT binary patch literal 24400 zcmeHv2|U!@_x}u&y_7^75!x_zQOFXdv?!&BY(rDFW`-!$votZnI$Oa@x|_t34#TekR-ZMHZOkJoOi<^eMlsBD4j(^5nm=PlorTh zGJ^d5LxM@9B~Yvq@Mi!W3RIsL9|Uv|#6OZrBo&~wK$U^!0UZuB0q8KGD|pn6$G70c zhXX};;y{N04Gv&^-g0{Ya#~kbVWIG|*z8Pz2ix zDEzSlf&!o!NF+yIToejJ`Rsrq`#_tKP`lYdp#y>H0!4Z$0!8hDltJ}sfc%JG1r+h6 zoEL`PA7sH8MWOedB-t5ucKcez$mZxrGyQ8P%cfbyMl<$}d>C!tTlQ(Cp4R89pOu!+ zAlFCSW={}z^kqa}@fkbvxQYI!n|3d(G$h(>CSKgM?MvbrwSKQnT35{I(>JQ}4-JZ& zZbM{$F;=eBL{+6A81%bOe|)@a^7$f#U$ zY5_B5a#v28dc?vR!__0c-t9chij)(cjPRW=x3pMmb~*?SqFPq#GyHH z=j&rL`YTR)A2+^9ZSj_b=xEBZMGO0l3ws&QdY3w(<4-Sz&yupkB471=*TH~-cvZUX$IeCBSu zhJcbskBX8q_0O3mYp}I6BWYgF=JivCZ;d`(9^0vS#%tE9rhBW-oP55v>XJ-W$ngDt z-rxA%R$lb3X4Kh+4~G{YPPU(DZxD6G;N?u^=9bkNUXk-G+(iniTO=MINc7KGdSS$g z3ED>_7S|}Z)PFQs@?1?+WjZm|h9P zOQ-gc{>Hmg-^F_mr zixBcpL4KYP^5dY=!j!)gINdyfw(<`3R@~8wSpo z5AtRL^8Y4>`BG3lG%kLJD|CKht z^6qdRFoRcqPvnn-Jeogn|HbwAw>seRKY+ZW5c2kL$b#mNo|YdE^1{@=9^`vce`FtI zZ*-wC57QtgAPP$Yr!S)f)TdGS|4GrjrGg59pu+k8Ek*qr_V}Fe-%OFMVXSfHX_#|3 z;YSqpsR?i)Y6%w#mT;lD$__3RewL!VzuWtNw;%oAws)6o7wS0wj{}*+#TA66-1{o| zhL*Co;^XrWnVl}JsU3-hMG-O*4gHhWti01|{b19u>AN+>HhrEwa?e3y#z|JpDw%X| zsT9*`5=x8W3QE!ZgK&|(@Vt!Fro0?1x^i{0&Y2prY0X;NwzQK0CBrIPa?^Foj}G<8 zf3Dq9pfphVY_AVW^Y$ivc;@P`ZExqJX~uVH6)A}Z@rsXac(}+8XpX^w(s;8NKZaYU;2Jo)f=u>%Eky`(r289{#vG?8T@9p~j ze)I55xj6B900Kw}*m`ls6d!lrZyK6{hQAkGI=(K?RKnm_*#`%W|MuCKEGy4g0BFs;5tkx4_tb??89D58L?OC zrSd*%8|~`n1>6?Z(r5}E_|^1T&i$%#z4q2kG7;vZRKGACr`JEWJFxHhqJ3nQSN$~y zX)WX7vVo$x0|$!zdUw{I!hPZir}N354&6$Omp)w~U74@7E~))8OX0NrbhX3o*X&YC z*BfS-W!Y909u>EIxPiV^?}VA_QZYX_)LzIAhzg!tP!8NG&3-gB{(aK8goh7DEp7!s!duVvOVmDBP^Y}j^;*9)rv~Gs{d=CCbM5VL-QD=12$ND1#l=PM zA->rUQCuQctIn45TWRy!?cHX7^0_@br|-^sm|)>Chke0ihL&{07s|HPu^e0xj=qBD z&y*G9yt17~J{e2(S}z^&(0XQ{+B;6ksx{`H(Djp}7DCK`CuB|9|*%$AB5H{sX@@B`p*43;iBOj@e8mVn%2kwP74^^F+DAqsxS*}r$ zT-oG!vg+{qYWZ^MWm1lJa%3H@R?j5Is1^P>sM4#zorjCY1+p;?l)?k65A{-gA3K%# zw&-!wjH6C#){dT$l(}oQr=lro?{x)x$>6(Zx-8#$ejRT#E@qMI12vx){jZ!DHacUM z?5dQAb0p==51b#LI%)Vhg6+THjFk!Im_@ zscXq{i|mVPi_0!oJ{cstD)Ujr+#44vrmcCjktu)+k0bm*nPl7Ilsq@THqp<)$K= zVtwH;fgdQhKc}2ne|6`I{6>@0uUz`~`KoUxnKPhNnQ>M7;4{^eNur5sp ze>m08nQiyT?$VjPFH!opoFW_)5prHW;&`A=gv+W$^NQ1;M!-s zUK^}xL-cfKHr-(-v|m`&wluJP*JGtq4I?KymaUyqT-saAHFCq6x8zYZ0=TfP<_F5q zW%Z;1AIi|#*s=KmF#F~8$!N}xL9&4XX83c z*YfHEk#y%0|GqcAv?_0RKIG|Ij)kTca@;(x z6?WD&RGMz-y>xBej;XTVR`u#N&W&!Hq z*B$i7e$LWwN~xj?M$F8}_t-0cGgFljXn@w4@BWy#~bmQ9zIiqyPMdt#+G= zIWA_;WOh8ZNu7Po7 zMI_}?z_H5IsKcdb>>^w=Hw@*Yg1&1{ztf*%U9dCxqLX~m1Jga4bN|?J{Z229;x3Us z)Zyw1x6^iy(RS+iYk|k$I{QId@5T?-mJgIpX?FflvFxS83v_+}xFQG_Kd&KpifE$S ztj+Q2OqpF5HwVAnrDJT6cG=AGi(~HQyDvMcp42|0(3XY{IpD1te7NA#)2IP&JRVf7 z^juuvn|^%1SwmSR7nd`aDI-8$psWtQ_o;E^(|V7)Tig!nHrJ`_jEyY3RdXjdEjj62 z?U82P=QZiHM~BU4`gW$bz449m88gN`we7|JwFXDC9v!K&2OKi=GNdp3CW9X+y36l- zoLM{M>fONE%DMi>Pfwa-XQ*Li_cf)AZ2bDRT$uNh$gNpx^5Q)f`s&@YTv}1p=TsjL zwv~K@o>hJ5r%JS@fgL!wDtuJ1gLvY~Z8j;JgR*W{hW4>~|J0?ciJn#EaJR9YMSmMs zJzrM0Ji(^$)v)Z$NWa)x-}PaPgz#Mfe#at97EB-2Zd)#Zt15__9zSbxF5RL)eCgGa z*Q^1wLs2_jbtGjD?*EXPG_Q8FYIFa%&?hkwiXpn5w*78f$u63`Z=m_{DeY4N|5}x2 zKJ5etSAwJOa6#Oqsqa439g{8g$fSi!MY)d9n<1xiBeTSQkD~jPWU={9GF$rob#i*x zJEmK4ThJII`eNUKPKAb~=k7bi%km1uQ#iQXbzV&nm%41zx`-qF8Xxsiqoy8LFD|zp zyg2YJOFrt=rne!jsT-E}dUI4PBp_JdbctbB)SpjNil3y!&cB{)=aM|xC4ST_4lZ}U zqb`X1K;0+nah9apZjH08>r8IswDhkW;j~wQwxnMAj+upf>U|5FI$y2$bJ;DsYhIHc z589m{qi9p~ex%H4qj62lm0vixBndR&Gz4)+*p5lwN?Q4L_r}|k8(W81)eICl-e2w& zZ9q+;-nJW(cFP*&R{HD=uu5;!mEE-||5MwWJRkO=?40XkLKJ(A%GKoHQaA&7gdlGG zV7bFP@`L1Jjc!-xEbq;XeG(92J@pbRX79QpjV!OMS@k~24r{9KTOABuvcmCseze`_ z_Lryj#DwqJ?ssd{iHjUu?*4bAAnqzFrxoIkwwkTgXt8!4 zo_I=au0yax&Y|3s;j2GN>V0y!S3-aP^6ruS?iJLo$s(_w^7N$u9VLj{Z(Kx^;oLmU zm7{Oo-f<{n%-Tyz>l@>=IzF9Pxb6O9_shE;{-NqH>W$Usu~!?@+pb=oc*slfOs2)z z)xJ-aw5hs90q{?8| z&|^!s+%=ap6Ll146;8dkZeRM%CtZUR=M^S>$$Baa=dS<>+5+wvLEOcfe`x|?NJ&3*>5g3Og1f3KA&-L z_|WSuvp;fh;fWA%Q5)hwd0y|5eq3Z%ddrx%j+sw^sGu zm(*#cFY#palSM91ZSt?3*!Vbc*3N9H3pFkq{pXq#0T7yZ0GXtPL4i^fBC2l|tbeS& zJuY`-PTbkf1H-LF#*bOIE@_WxsZrUnS+vm)x7EmqXNQ`+4}81rL7v&(n5jn6F_!TO zT~A$@F5LYu_xV^`5H~F(@%S-^swT^+jni(Dizse!mDtQM84-f8g3zPY_q~gjdk{ zMupYMpX@)3U7zT{Hjgpb-gJ4x)CFluVQvFpguC~-4Whx9*d z+V-DXu;Kh8#ny_Ux2sl;I7VAks%$FTFS{r}`mFp-0bBz?+~?WnCO3K8S41ghUcaYx z!ok3+VA7%+UOHtv?6#$SmPt}p9NBbjTHokNt75{}oU?g9FJjha$FBvONH=X*m1UZ3 z0=R~PxO4S9WEMUWuT$T3Ew_IBbcY;eeZz)ZdE-Y(yxx6b<8zbDtke0~@g_4|cTvX2 zIR(v;qqpZx9ku@Tyd>v3>8xj01#pc7amy1+^za}F?GiTzZ{!d7rV%$ z%=kNBKPKEZn42Avs8)J*V*RpaFY=wZ+AQbTt3D5_;-j)8q?Vzx0o>1w1#!FV`d#$6 z>Z*5LJioA{dZ)GHlH5|KrR58+)^~NNkq}X0j-xMk-Gp~5{6O*8usC9uO5MU_i{jY} z2Z}b?8uyQ5d*S;c39_sewcDtIfnsZGSYAZDVqV)pJ#W+yRcb&QyMOsK-g-t8>ohz)>%c;02(w62c;}zl7;-Z<98Ms$H#%rDD7=NZre&Lp@E0T7s zcn?5GUxbU!t#P27ihkc~kzaO&!@a%CNi!D2P<>%!*!Z;U+K?=9Bo+@Tepy>HnXBdVNBB;nb%od;a>-Gc)8L^lqBgRBY%xq3+~HOUJ3s{<}Z8 z-z49ev;F*I(dRbpVMZWDEBMe7IteKOitNRwbu`A$Ij@b8u{!aY0} zv%iz=chWCd0NE7(2D<|$7&sSVqcr?`Y!lc-{!@GWLNULy{s&nA%|j?)%8mW^{YU-O ziw-}e2f7sw#w7oX<^z~te}~@|_-%pT7Wi#}-xm07f!`MRZGqnw_-%pzNDD~teqK0~ zN2Sb|3~wqukje7&_ow2Ixv5SJ8qGpQUsr`mUrr11)lxB3@${$r1qS(Ye$LYMl^akM z(YeZG9-0~YaUujMvk(J)3lRFgI69NzoRy&8U|GUt1(!8k==WI@;j)Dbo!_CeHgqnA z&alukUN5-NyJ&QNh>Aq#FsOVLxK!a94i|c_r4AQ5TSI3s=sgTN*GBI*(7OmtxX>9t zI*&(Z=jfapok^qfV|3Pw&SlXVDmp_$XOl{BDZ_={N22$V=v^)PO$0oqp--35H`SbX z_=-T$yYWGA4TcN7A07%9GRY#IUD%cof$$^ypn4;{kbcNc2p^S+Y$XC0&-C9Vh>vtc zzq5ez5A^*t=l48t?t#AP=KRJ9jzKu~Lv}>=M0Q2>K=ncOLbgFRLbgISL$*UUM7BgW zMYcsYMz%&aSAh%J2H6PN3e_3a9oYcc0@(!FMgcBlS7cvgXJmIok=>E~vAt1yqIN~+ zi>TdE`=fr4Z3+J%T${wDQf99D#%>`d&`pi`{Y;POrl|gKvGMoF63955fWK_VUpz~Q z_h@H=^oOK}~$BhrdSWaCE7l7(QRZ-+_ye-F2yYrc?tG_7s4> zAP0^K)sSkS&p-Ra-?9V85Xu2FV6O)FYj+BHl|Iz~z(^sWJ37CL)p6M}BG=qhp6mrF z&}wXd8n`2XkYHBTP~Ka#Ri2F64h1$`GC)?H4Plm<+}4*TyX#T)^`Ic8Cs$_HOY4pO zVi$!GGE7#ue+b}B*zal3$0D*=fD_{9$pEpu3h(NoUS%f;4vWTM(ikMt+qEuV537-_ zpd2Hp=|9&8EQKya{c2<$U};9&0&*sB1+!Tu$%-vNSyJy2lJ1Ox~BsKCAp2oCmQfxR0L z9PH-;`#&H!`1BKdCV*ZAJ%?eRi=5%X4 zJred9fjtQj9PCp9`x+oP*y{xLMnG_|UkdE6fZ)`@RE7FC_GmzGux|_O^MK%B?-$rB z0>Q!lF|gkRf`dJ1V9yE!2m9E-z844%_QHX^GY}l?rvv+MAUN2Q2lntlaImir>;r<} zU~eGUO9a8e{z9-H34((?ieOI@1PA*Z!M-R64)#idy;Tq#?6(B_vmiJsFjb=V!5%LN z4)%S5ePR$C>?H(yj}RdnQH?qCDE1?Q{Y!|DVd6EV!jgeKjbINHeaY^|+}?|QlVG0} z1gE*V9yu80Tqx2*ry5hbs@r^VX>DJ?EQk^U_U0< zzXgS?2MVCo4tqMm9xwaInV}?1_Wm44e#VK$yp) z-|hmLw17|=gK8TF(|Vw%|7;e69_VMGqDL&VCd4vJVlllrSjC^@(-=Fq%OeLtC{6a! zL9=G1rTHu-EDeC?c1vZ=EZ?agZuaG_N$X9Fu93s~bVc)YIP1=VL3<-u)Ahc$q;oR( zc+&mDwHY)|pJj9=&60C$zL*JqiQxF06&_4udNb(3ETjcmv#E=ji&(5+<~SW42F;Jo zWHG|2fx!WbnbaVLA18;7Hiu{%L6_3grLz2%yX%>t?xhkDp<-dK4|h~uOAfM5V2}?D zw!w>7fnX7E0`voDypRJtzSZWec*fmy+e%+5k@y@o<+j`W)Chbr2jK2V*VYG zt1(9qYWssm>!!)KaztBxn@%)sPmn>|?*T({PjJ6A5dr*ddQXsvRv}P{w4NYxttikB z2;jf5dV=4r7ZT{*7(Kz}DhPv?7y|_UcbGjXqx&!<3h2)12|8a%G@M{m5a``G!lHkt zhjV5-WV)VUbDNI~fu|=r{QQ^P9LfdeYEGz(1JRh%%8%d<8hNoHZG( z=iKKTs`n3d;bsC9?u|C*2?&+Waa_pt&G!REcu(^n&%4v+J@s(bT+H>PhiL}~d&APg zPaziB@1=bwCt8@Wv|#&nPZZ4O3&(O^OuUq1X`CvUlRx_SaQqa3a=B?}h3%HjKPV{B z?{66c=)p{5Lz;iL%->pqi^-dhyX9mWnCgagOB2*Okj6sQ6HE#UrUiCSV}=9FIs+FA1O>ap&x0Tf^HDe(8i8K*^DvyrijTvcx_%LeZ#sbC z-~Vc*1lj~(1Y*AmM6@^H5RqSn;dVJ_9YEpV_Y8#7L-{bASkHiX1Aq^OHz>c@Ufk+% zMi?K86YH5)cm(sYa6-@Ec+&(QjTi437;lhpYuuAT!j;5>L?G5PeE^7GF}VL#5P?}4 zU<6`41;Q1>P@MR$f)RUdP>P835C*FxxPJXA6y6eXhp#}ar$AUK4Amn$2?&Fs1Y*Ce z72YZ!7*1^AE?#7Y4HYd)OhPJ?W2aCRh=U5jY5745$jc+4vV}?qMc~ zaBq;VaFqEkLcBvJ0qbk)5D{o;bWn;gcp1_0+z%`#kP8$+nW5W3E0_vEz*{faQvN## zHy^-oZ#esMBZPx`FX!m(M;PCn(E$i2&h3F5T`=FOL z_x#*N0j8R7vI{H~KT{2)kbnqOs9OqAwO@ndPJsj%#D5)1a4zPlCYaP?Ktegf=)Vpo auu%pI6^gk?;)VLr^^3%|NjS3{|FZV literal 58990 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 om(=v3B@oAZvX%Q diff --git a/main.js b/main.js new file mode 100644 index 0000000..045e7b0 --- /dev/null +++ b/main.js @@ -0,0 +1,147 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => { + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; +}; +Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } }); +const obsidian = require("obsidian"); +class DailyHighlightsPlugin extends obsidian.Plugin { + constructor() { + super(...arguments); + __publicField(this, "settings", {}); + } + getAuthHeaders() { + return { + AUTHORIZATION: `Token ${this.settings.readwiseAPIToken}` + }; + } + getOfficialPluginSettings() { + const plugins = this.app.plugins; + const settings = plugins.plugins["readwise-official"].settings; + return settings; + } + /** + * If there's no token set, add a command to read it from the _official_ Readwise plugin settings. + * (Assume that it's installed and enabled, I suppose.) + * + * Accessing the data from another plugin is questionable. I don't think it's explicitly + * forbidden, but I also don't think it's intended. (The type definition for `this.app` + * does not include `plugins`, so it's at minimum undocumented.) + * + * This is primarily for my own use, and it's gated behind a command the user has to choose, + * though, so I'm not too worried about it. + */ + async getTokenFromOfficialPlugin() { + const apiToken = this.getOfficialPluginSettings().token; + this.settings.readwiseAPIToken = apiToken; + await this.saveSettings(); + new obsidian.Notice("Successfully set Readwise API token"); + } + async getReview() { + const response = await fetch(`https://readwise.io/api/v2/review/`, { + method: "GET", + headers: this.getAuthHeaders() + }); + const responseJson = await response.json(); + console.log(responseJson); + return responseJson; + } + /** + * Find the note that contains the given highlight. Return the block-reference link. + */ + highlightToMarkdown(highlight) { + var _a; + const bookIdsMap = this.getOfficialPluginSettings().booksIDsMap; + const bookTitle = (_a = Object.entries(bookIdsMap).find(([_key, value]) => value === highlight.id.toString())) == null ? void 0 : _a[0]; + return bookTitle; + } + async onload() { + await this.loadSettings(); + this.addRibbonIcon( + "book-open", + "Review highlights", + async (_evt) => { + new obsidian.Notice("This is a notice! I hope this changed."); + await this.getTokenFromOfficialPlugin(); + } + ); + this.addCommand({ + id: "add-review-highlights", + name: "Add daily review highlights to current note", + callback: async () => { + await this.getTokenFromOfficialPlugin(); + const review = await this.getReview(); + const highlights = review.highlights; + console.log(highlights); + console.log(highlights.map(this.highlightToMarkdown.bind(this))); + } + }); + this.addCommand({ + id: "open-sample-modal-complex", + name: "Open sample modal (complex)", + checkCallback: (checking) => { + const markdownView = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView); + if (markdownView) { + if (!checking) { + new SampleModal(this.app).open(); + } + return true; + } + } + }); + this.addCommand({ + id: "find-readwise-token", + name: "Set the Readwise API token from the official plugin settings", + callback: this.getTokenFromOfficialPlugin.bind(this) + }); + this.addSettingTab(new SettingTab(this.app, this)); + this.registerDomEvent(document, "click", (evt) => { + console.log("click", evt); + }); + this.registerInterval( + window.setInterval(() => console.log("setInterval"), 5 * 60 * 1e3) + ); + } + onunload() { + } + async loadSettings() { + } + async saveSettings() { + await this.saveData(this.settings); + } +} +class SampleModal extends obsidian.Modal { + constructor(app) { + super(app); + } + onOpen() { + const { contentEl } = this; + contentEl.setText("Woah!"); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} +class SettingTab extends obsidian.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + __publicField(this, "plugin"); + this.plugin = plugin; + } + display() { + const { containerEl } = this; + containerEl.empty(); + new obsidian.Setting(containerEl).setName("Readwise API token").setDesc( + "API token from readwise.io. (Requires active Readwise subscription.)" + ).addText( + (text) => text.setPlaceholder("n/a").setValue(this.plugin.settings.readwiseAPIToken).onChange(async (value) => { + this.plugin.settings.readwiseAPIToken = value; + await this.plugin.saveSettings(); + }) + ); + } +} +exports.default = DailyHighlightsPlugin; diff --git a/manifest.json b/manifest.json index 37eb761..7f49836 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,10 @@ { "id": "daily-readwise", - "name": "Daily Readwise", + "name": "Readwise highlights", "version": "1.0.0", - "minAppVersion": "0.15.0", - "description": "Integrate your Readwise daily review into your Obsidian daily note.", + "minAppVersion": "1.0.0", + "description": "Integrate Readwise daily reviews into Obsidian daily notes. This plugin is not affiliated with Readwise.", "author": "Tushar Chandra", - "authorUrl": "", - "fundingUrl": "", + "authorUrl": "https://github.com/tuchandra", "isDesktopOnly": false } diff --git a/package.json b/package.json index af68a39..f630b6c 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,18 @@ { "name": "daily-readwise", - "version": "0.0.1", - "description": "Link my Readwise daily review with my Obsidian vault.", - "main": "main.js", - "scripts-unclear": { - "dev": "bun run esbuild.dev.config.mjs", - "build": "tsc -noEmit -skipLibCheck && bun run esbuild.config.mjs production", - "dev-publish": "bun run esbuild.publish.config.mjs", - "build-publish": "tsc -noEmit -skipLibCheck && node esbuild.publish.config.mjs production" - }, + "private": true, + "version": "0.0.0", + "type": "module", "scripts": { - "tsc": "tsc -noEmit", - "test": "bun test", - "test:log": "LOG_TESTS=true bun test", - "format": "bunx @biomejs/biome format .", - "format:fix": "bunx @biomejs/biome format . --write", - "lint": "biome check src/", - "lint:fix": "biome check --apply src/", - "check": "bun run format && bun run lint && bun run tsc && bun run test", - "check:fix": "bun run format:fix && bun run lint:fix && bun run tsc && bun run test", - "release": "bun run automation/release.ts" + "dev": "vite build --watch", + "build": "tsc && vite build", + "preview": "vite preview", + "format": "bunx @biomejs/biome format --write ." }, - "keywords": [], - "author": "tushar chandra", - "license": "MIT", "devDependencies": { - "@biomejs/biome": "1.4.1", - "@types/node": "^16.11.6", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", - "builtin-modules": "3.3.0", - "bun-types": "^1.0.18", + "@biomejs/biome": "^1.4.1", "obsidian": "latest", - "tslib": "2.4.0", - "typescript": "^5.3.3" + "typescript": "^5.2.2", + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/src/main.ts b/src/main.ts index 3eb5b59..565b6e6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,76 +1,136 @@ -/// -/// - import { App, Editor, - MarkdownFileInfo, MarkdownView, Modal, Notice, Plugin, - PluginManifest, PluginSettingTab, Setting, } from 'obsidian'; -// Remember to rename these classes and interfaces! +interface PluginSettings { + readwiseAPIToken?: string; +} + +/** + * Simplified version of the official Readwise plugin's settings; we need the API token + * and the book IDs/titles map so that we can integrate with the existing exports. + */ +interface ReadwiseOfficialPluginSettings { + booksIDsMap: { [key: string]: string }; + token: string; +} + +export interface ReadwiseReview { + review_id: number; + review_url: string; + review_completed: boolean; + highlights: ReadwiseHighlight[]; +} -interface MyPluginSettings { - mySetting: string; +export interface ReadwiseHighlight { + id: number; + text: string; + title: string; + author: string; + + // We don't care about any of the other fields + url: string | null; + source_url: string | null; + source_type: string; + category: string | null; + location_type: string; + location: number; + note: string; + highlighted_at: string; + highlight_url: string | null; + image_url: string; + api_source: string | null; } -const DEFAULT_SETTINGS: MyPluginSettings = { - mySetting: 'default', -}; -export default class MyPlugin extends Plugin { - settings: MyPluginSettings; +export default class DailyHighlightsPlugin extends Plugin { + settings: PluginSettings = {}; + + getAuthHeaders() { + return { + AUTHORIZATION: `Token ${this.settings.readwiseAPIToken}`, + }; + } + + getOfficialPluginSettings(): ReadwiseOfficialPluginSettings { + const plugins = this.app.plugins; // property 'plugins' does not exist + const settings = plugins.plugins['readwise-official'].settings; + return settings; + } + + /** + * If there's no token set, add a command to read it from the _official_ Readwise plugin settings. + * (Assume that it's installed and enabled, I suppose.) + * + * Accessing the data from another plugin is questionable. I don't think it's explicitly + * forbidden, but I also don't think it's intended. (The type definition for `this.app` + * does not include `plugins`, so it's at minimum undocumented.) + * + * This is primarily for my own use, and it's gated behind a command the user has to choose, + * though, so I'm not too worried about it. + */ + async getTokenFromOfficialPlugin(): Promise { + const apiToken = this.getOfficialPluginSettings().token; + this.settings.readwiseAPIToken = apiToken; + + await this.saveSettings(); + new Notice('Successfully set Readwise API token'); + } + + async getReview(): Promise { + const response = await fetch(`https://readwise.io/api/v2/review/`, { + method: 'GET', + headers: this.getAuthHeaders(), + }); + const responseJson = await response.json(); + console.log(responseJson); + return responseJson; + } + + /** + * Find the note that contains the given highlight. Return the block-reference link. + */ + highlightToMarkdown(highlight: ReadwiseHighlight): string | undefined { + const bookIdsMap = this.getOfficialPluginSettings().booksIDsMap; - constructor(app: App, manifest: PluginManifest) { - super(app, manifest); - this.settings = DEFAULT_SETTINGS; + // Find the key/value pair where the value is the highlight.id + const bookTitle = Object.entries(bookIdsMap).find( ([_key, value]) => value === highlight.id.toString() )?.[0]; + return bookTitle; } async onload() { await this.loadSettings(); // This creates an icon in the left ribbon. - const ribbonIconEl = this.addRibbonIcon( - 'dice', - 'Sample Plugin', - (evt: MouseEvent) => { - // Called when the user clicks the icon. - new Notice('This is a notice!'); + this.addRibbonIcon( + 'book-open', + 'Review highlights', + async (_evt: MouseEvent) => { + new Notice('This is a notice! I hope this changed.'); + await this.getTokenFromOfficialPlugin(); }, ); - // Perform additional things with the ribbon - ribbonIconEl.addClass('my-plugin-ribbon-class'); - - // This adds a status bar item to the bottom of the app. Does not work on mobile apps. - const statusBarItemEl = this.addStatusBarItem(); - statusBarItemEl.setText('Status Bar Text'); // This adds a simple command that can be triggered anywhere this.addCommand({ - id: 'open-sample-modal-simple', - name: 'Open sample modal (simple)', - callback: () => { - new SampleModal(this.app).open(); - }, - }); - // This adds an editor command that can perform some operation on the current editor instance - this.addCommand({ - id: 'sample-editor-command', - name: 'Sample editor command', - editorCallback: ( - editor: Editor, - view: MarkdownView | MarkdownFileInfo, - ) => { - console.log(editor.getSelection()); - editor.replaceSelection('Sample Editor Command'); + id: 'add-review-highlights', + name: 'Add daily review highlights to current note', + callback: async () => { + await this.getTokenFromOfficialPlugin(); + const review = await this.getReview(); + const highlights = review.highlights; + console.log(highlights); + console.log(highlights.map(this.highlightToMarkdown.bind(this))); }, }); + // This adds a complex command that can check whether the current state of the app allows execution of the command this.addCommand({ id: 'open-sample-modal-complex', @@ -92,8 +152,14 @@ export default class MyPlugin extends Plugin { }, }); + this.addCommand({ + id: 'find-readwise-token', + name: 'Set the Readwise API token from the official plugin settings', + callback: this.getTokenFromOfficialPlugin.bind(this), + }); + // This adds a settings tab so the user can configure various aspects of the plugin - this.addSettingTab(new SampleSettingTab(this.app, this)); + this.addSettingTab(new SettingTab(this.app, this)); // If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin) // Using this function will automatically remove the event listener when this plugin is disabled. @@ -109,9 +175,7 @@ export default class MyPlugin extends Plugin { onunload() {} - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } + async loadSettings() {} async saveSettings() { await this.saveData(this.settings); @@ -119,16 +183,13 @@ export default class MyPlugin extends Plugin { } class SampleModal extends Modal { - text: string; - constructor(app: App) { super(app); - this.text = 'Hello!'; } onOpen() { const { contentEl } = this; - contentEl.setText(this.text); + contentEl.setText('Woah!'); } onClose() { @@ -137,10 +198,10 @@ class SampleModal extends Modal { } } -class SampleSettingTab extends PluginSettingTab { - plugin: MyPlugin; +class SettingTab extends PluginSettingTab { + plugin: DailyHighlightsPlugin; - constructor(app: App, plugin: MyPlugin) { + constructor(app: App, plugin: DailyHighlightsPlugin) { super(app, plugin); this.plugin = plugin; } @@ -151,25 +212,18 @@ class SampleSettingTab extends PluginSettingTab { containerEl.empty(); new Setting(containerEl) - .setName('Setting #1') - .setDesc("It's a secret") + .setName('Readwise API token') + .setDesc( + 'API token from readwise.io. (Requires active Readwise subscription.)', + ) .addText((text) => text - .setPlaceholder('Enter your secret') - .setValue(this.plugin.settings.mySetting) + .setPlaceholder('n/a') + .setValue(this.plugin.settings.readwiseAPIToken!) .onChange(async (value) => { - this.plugin.settings.mySetting = value; + this.plugin.settings.readwiseAPIToken = value; await this.plugin.saveSettings(); }), ); } } - -const server = Bun.serve({ - port: 3000, - fetch(request) { - return new Response('Welcome to Bun!'); - }, -}); - -console.log(`Listening on localhost:${server.port}`); diff --git a/src/readwise.ts b/src/readwise.ts new file mode 100644 index 0000000..a16a70f --- /dev/null +++ b/src/readwise.ts @@ -0,0 +1,917 @@ +import { + App, + ButtonComponent, + DataAdapter, + debounce, + Editor, + MarkdownView, + Modal, + normalizePath, + Notice, + Plugin, + PluginSettingTab, + Setting, + Vault, +} from 'obsidian'; +import * as zip from '@zip.js/zip.js'; +import { StatusBar } from './status'; + +// the process.env variable will be replaced by its target value in the output main.js file +const baseURL = process.env.READWISE_SERVER_URL || 'https://readwise.io'; + +interface ReadwiseAuthResponse { + userAccessToken: string; +} + +interface ExportRequestResponse { + latest_id: number; + status: string; +} + +interface ExportStatusResponse { + totalBooks: number; + booksExported: number; + isFinished: boolean; + taskStatus: string; +} + +interface ReadwisePluginSettings { + token: string; + readwiseDir: string; + isSyncing: boolean; + frequency: string; + triggerOnLoad: boolean; + lastSyncFailed: boolean; + lastSavedStatusID: number; + currentSyncStatusID: number; + refreshBooks: boolean; + booksToRefresh: Array; + booksIDsMap: { [key: string]: string }; + reimportShowConfirmation: boolean; +} + +// define our initial settings +const DEFAULT_SETTINGS: ReadwisePluginSettings = { + token: '', + readwiseDir: 'Readwise', + frequency: '0', // manual by default + triggerOnLoad: true, + isSyncing: false, + lastSyncFailed: false, + lastSavedStatusID: 0, + currentSyncStatusID: 0, + refreshBooks: false, + booksToRefresh: [], + booksIDsMap: {}, + reimportShowConfirmation: true, +}; + +export default class ReadwisePlugin extends Plugin { + settings: ReadwisePluginSettings; + fs: DataAdapter; + vault: Vault; + scheduleInterval: null | number = null; + statusBar: StatusBar; + + getErrorMessageFromResponse(response: Response) { + if (response && response.status === 409) { + return 'Sync in progress initiated by different client'; + } + if (response && response.status === 417) { + return 'Obsidian export is locked. Wait for an hour.'; + } + return `${response ? response.statusText : "Can't connect to server"}`; + } + + handleSyncError(buttonContext: ButtonComponent, msg: string) { + this.clearSettingsAfterRun(); + this.settings.lastSyncFailed = true; + this.saveSettings(); + if (buttonContext) { + this.showInfoStatus( + buttonContext.buttonEl.parentElement, + msg, + 'rw-error', + ); + buttonContext.buttonEl.setText('Run sync'); + } else { + this.notice(msg, true, 4, true); + } + } + + clearSettingsAfterRun() { + this.settings.isSyncing = false; + this.settings.currentSyncStatusID = 0; + } + + handleSyncSuccess( + buttonContext: ButtonComponent, + msg: string = 'Synced', + exportID: number = null, + ) { + this.clearSettingsAfterRun(); + this.settings.lastSyncFailed = false; + this.settings.currentSyncStatusID = 0; + if (exportID) { + this.settings.lastSavedStatusID = exportID; + } + this.saveSettings(); + // if we have a button context, update the text on it + // this is the case if we fired on a "Run sync" click (the button) + if (buttonContext) { + this.showInfoStatus( + buttonContext.buttonEl.parentNode.parentElement, + msg, + 'rw-success', + ); + buttonContext.buttonEl.setText('Run sync'); + } + } + + async getExportStatus(statusID?: number, buttonContext?: ButtonComponent) { + const statusId = statusID || this.settings.currentSyncStatusID; + let url = `${baseURL}/api/get_export_status?exportStatusId=${statusId}`; + let response, data: ExportStatusResponse; + try { + response = await fetch(url, { + headers: this.getAuthHeaders(), + }); + } catch (e) { + console.log( + 'Readwise Official plugin: fetch failed in getExportStatus: ', + e, + ); + } + if (response && response.ok) { + data = await response.json(); + } else { + console.log( + 'Readwise Official plugin: bad response in getExportStatus: ', + response, + ); + this.handleSyncError( + buttonContext, + this.getErrorMessageFromResponse(response), + ); + return; + } + const WAITING_STATUSES = ['PENDING', 'RECEIVED', 'STARTED', 'RETRY']; + const SUCCESS_STATUSES = ['SUCCESS']; + if (WAITING_STATUSES.includes(data.taskStatus)) { + if (data.booksExported) { + const progressMsg = `Exporting Readwise data (${data.booksExported} / ${data.totalBooks}) ...`; + this.notice(progressMsg); + } else { + this.notice('Building export...'); + } + + // re-try in 1 second + await new Promise((resolve) => setTimeout(resolve, 1000)); + await this.getExportStatus(statusId, buttonContext); + } else if (SUCCESS_STATUSES.includes(data.taskStatus)) { + return this.downloadArchive(statusId, buttonContext); + } else { + this.handleSyncError(buttonContext, 'Sync failed'); + } + } + + async requestArchive( + buttonContext?: ButtonComponent, + statusId?: number, + auto?: boolean, + ) { + const parentDeleted = !(await this.app.vault.adapter.exists( + this.settings.readwiseDir, + )); + + let url = `${baseURL}/api/obsidian/init?parentPageDeleted=${parentDeleted}`; + if (statusId) { + url += `&statusID=${statusId}`; + } + if (auto) { + url += `&auto=${auto}`; + } + let response, data: ExportRequestResponse; + try { + response = await fetch(url, { + headers: this.getAuthHeaders(), + }); + } catch (e) { + console.log( + 'Readwise Official plugin: fetch failed in requestArchive: ', + e, + ); + } + if (response && response.ok) { + data = await response.json(); + if (data.latest_id <= this.settings.lastSavedStatusID) { + this.handleSyncSuccess(buttonContext); + this.notice('Readwise data is already up to date', false, 4, true); + return; + } + this.settings.currentSyncStatusID = data.latest_id; + await this.saveSettings(); + if (response.status === 201) { + this.notice('Syncing Readwise data'); + return this.getExportStatus(data.latest_id, buttonContext); + } else { + this.handleSyncSuccess(buttonContext, 'Synced', data.latest_id); // we pass the export id to update lastSavedStatusID + this.notice( + 'Latest Readwise sync already happened on your other device. Data should be up to date', + false, + 4, + true, + ); + } + } else { + console.log( + 'Readwise Official plugin: bad response in requestArchive: ', + response, + ); + this.handleSyncError( + buttonContext, + this.getErrorMessageFromResponse(response), + ); + return; + } + } + + notice(msg: string, show = false, timeout = 0, forcing: boolean = false) { + if (show) { + new Notice(msg); + } + // @ts-ignore + if (!this.app.isMobile) { + this.statusBar.displayMessage(msg.toLowerCase(), timeout, forcing); + } else { + if (!show) { + new Notice(msg); + } + } + } + + showInfoStatus(container: HTMLElement, msg: string, className = '') { + let info = container.find('.rw-info-container'); + info.setText(msg); + info.addClass(className); + } + + clearInfoStatus(container: HTMLElement) { + let info = container.find('.rw-info-container'); + info.empty(); + } + + getAuthHeaders() { + return { + AUTHORIZATION: `Token ${this.settings.token}`, + 'Obsidian-Client': `${this.getObsidianClientID()}`, + }; + } + + async downloadArchive( + exportID: number, + buttonContext: ButtonComponent, + ): Promise { + let artifactURL = `${baseURL}/api/download_artifact/${exportID}`; + if (exportID <= this.settings.lastSavedStatusID) { + console.log( + `Readwise Official plugin: Already saved data from export ${exportID}`, + ); + this.handleSyncSuccess(buttonContext); + this.notice('Readwise data is already up to date', false, 4); + return; + } + + let response, blob; + try { + response = await fetch(artifactURL, { headers: this.getAuthHeaders() }); + } catch (e) { + console.log( + 'Readwise Official plugin: fetch failed in downloadArchive: ', + e, + ); + } + if (response && response.ok) { + blob = await response.blob(); + } else { + console.log( + 'Readwise Official plugin: bad response in downloadArchive: ', + response, + ); + this.handleSyncError( + buttonContext, + this.getErrorMessageFromResponse(response), + ); + return; + } + + this.fs = this.app.vault.adapter; + + const blobReader = new zip.BlobReader(blob); + const zipReader = new zip.ZipReader(blobReader); + const entries = await zipReader.getEntries(); + this.notice('Saving files...', false, 30); + if (entries.length) { + for (const entry of entries) { + let bookID: string; + const processedFileName = normalizePath( + entry.filename.replace(/^Readwise/, this.settings.readwiseDir), + ); + try { + // ensure the directory exists + let dirPath = processedFileName + .replace(/\/*$/, '') + .replace(/^(.+)\/[^\/]*?$/, '$1'); + const exists = await this.fs.exists(dirPath); + if (!exists) { + await this.fs.mkdir(dirPath); + } + // write the actual files + const contents = await entry.getData(new zip.TextWriter()); + let contentToSave = contents; + + let originalName = processedFileName; + // extracting book ID from file name + let split = processedFileName.split('--'); + if (split.length > 1) { + originalName = split.slice(0, -1).join('--') + '.md'; + bookID = split.last().match(/\d+/g)[0]; + this.settings.booksIDsMap[originalName] = bookID; + } + if (await this.fs.exists(originalName)) { + // if the file already exists we need to append content to existing one + const existingContent = await this.fs.read(originalName); + contentToSave = existingContent + contents; + } + await this.fs.write(originalName, contentToSave); + await this.saveSettings(); + } catch (e) { + console.log( + `Readwise Official plugin: error writing ${processedFileName}:`, + e, + ); + this.notice( + `Readwise: error while writing ${processedFileName}: ${e}`, + true, + 4, + true, + ); + if (bookID) { + this.settings.booksToRefresh.push(bookID); + await this.saveSettings(); + } + // communicate with readwise? + } + } + } + // close the ZipReader + await zipReader.close(); + await this.acknowledgeSyncCompleted(buttonContext); + this.handleSyncSuccess(buttonContext, 'Synced!', exportID); + this.notice('Readwise sync completed', true, 1, true); + // @ts-ignore + if (this.app.isMobile) { + this.notice( + "If you don't see all of your readwise files reload obsidian app", + true, + ); + } + } + + async acknowledgeSyncCompleted(buttonContext: ButtonComponent) { + let response; + try { + response = await fetch(`${baseURL}/api/obsidian/sync_ack`, { + headers: { + ...this.getAuthHeaders(), + 'Content-Type': 'application/json', + }, + method: 'POST', + }); + } catch (e) { + console.log( + 'Readwise Official plugin: fetch failed to acknowledged sync: ', + e, + ); + } + if (response && response.ok) { + return; + } else { + console.log( + 'Readwise Official plugin: bad response in acknowledge sync: ', + response, + ); + this.handleSyncError( + buttonContext, + this.getErrorMessageFromResponse(response), + ); + return; + } + } + + async configureSchedule() { + const minutes = parseInt(this.settings.frequency); + let milliseconds = minutes * 60 * 1000; // minutes * seconds * milliseconds + console.log( + 'Readwise Official plugin: setting interval to ', + milliseconds, + 'milliseconds', + ); + window.clearInterval(this.scheduleInterval); + this.scheduleInterval = null; + if (!milliseconds) { + // we got manual option + return; + } + this.scheduleInterval = window.setInterval( + () => this.requestArchive(null, null, true), + milliseconds, + ); + this.registerInterval(this.scheduleInterval); + } + + refreshBookExport(bookIds?: Array) { + bookIds = bookIds || this.settings.booksToRefresh; + if (!bookIds.length || !this.settings.refreshBooks) { + return; + } + try { + fetch(`${baseURL}/api/refresh_book_export`, { + headers: { + ...this.getAuthHeaders(), + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ exportTarget: 'obsidian', books: bookIds }), + }).then((response) => { + if (response && response.ok) { + let booksToRefresh = this.settings.booksToRefresh; + this.settings.booksToRefresh = booksToRefresh.filter( + (n) => !bookIds.includes(n), + ); + this.saveSettings(); + return; + } else { + console.log( + `Readwise Official plugin: saving book id ${bookIds} to refresh later`, + ); + let booksToRefresh = this.settings.booksToRefresh; + booksToRefresh.concat(bookIds); + this.settings.booksToRefresh = booksToRefresh; + this.saveSettings(); + return; + } + }); + } catch (e) { + console.log( + 'Readwise Official plugin: fetch failed in refreshBookExport: ', + e, + ); + } + } + + async addBookToRefresh(bookId: string) { + let booksToRefresh = this.settings.booksToRefresh; + booksToRefresh.push(bookId); + this.settings.booksToRefresh = booksToRefresh; + await this.saveSettings(); + } + + reimportFile(vault: Vault, fileName: string) { + const bookId = this.settings.booksIDsMap[fileName]; + try { + fetch(`${baseURL}/api/refresh_book_export`, { + headers: { + ...this.getAuthHeaders(), + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ exportTarget: 'obsidian', books: [bookId] }), + }).then((response) => { + if (response && response.ok) { + let booksToRefresh = this.settings.booksToRefresh; + this.settings.booksToRefresh = booksToRefresh.filter( + (n) => ![bookId].includes(n), + ); + this.saveSettings(); + vault.delete(vault.getAbstractFileByPath(fileName)); + this.startSync(); + } else { + this.notice('Failed to reimport. Please try again', true); + } + }); + } catch (e) { + console.log( + 'Readwise Official plugin: fetch failed in Reimport current file: ', + e, + ); + } + } + + startSync() { + if (this.settings.isSyncing) { + this.notice('Readwise sync already in progress', true); + } else { + this.settings.isSyncing = true; + this.saveSettings(); + this.requestArchive(); + } + console.log('started sync'); + } + + async onload() { + // @ts-ignore + if (!this.app.isMobile) { + this.statusBar = new StatusBar(this.addStatusBarItem()); + this.registerInterval( + window.setInterval(() => this.statusBar.display(), 1000), + ); + } + await this.loadSettings(); + this.refreshBookExport = debounce( + this.refreshBookExport.bind(this), + 800, + true, + ); + + this.refreshBookExport(this.settings.booksToRefresh); + this.app.vault.on('delete', async (file) => { + const bookId = this.settings.booksIDsMap[file.path]; + if (bookId) { + await this.addBookToRefresh(bookId); + } + this.refreshBookExport(); + delete this.settings.booksIDsMap[file.path]; + this.saveSettings(); + }); + this.app.vault.on('rename', (file, oldPath) => { + const bookId = this.settings.booksIDsMap[oldPath]; + if (!bookId) { + return; + } + this.settings.booksIDsMap[file.path] = bookId; + delete this.settings.booksIDsMap[oldPath]; + this.saveSettings(); + }); + if (this.settings.isSyncing) { + if (this.settings.currentSyncStatusID) { + await this.getExportStatus(); + } else { + // we probably got some unhandled error... + this.settings.isSyncing = false; + await this.saveSettings(); + } + } + this.addCommand({ + id: 'readwise-official-sync', + name: 'Sync your data now', + callback: () => { + this.startSync(); + }, + }); + this.addCommand({ + id: 'readwise-official-format', + name: 'Customize formatting', + callback: () => window.open(`${baseURL}/export/obsidian/preferences`), + }); + this.addCommand({ + id: 'readwise-official-reimport-file', + name: 'Delete and reimport this document', + editorCheckCallback: ( + checking: boolean, + editor: Editor, + view: MarkdownView, + ) => { + const activeFilePath = view.file.path; + const isRWfile = activeFilePath in this.settings.booksIDsMap; + if (checking) { + return isRWfile; + } + if (this.settings.reimportShowConfirmation) { + const modal = new Modal(view.app); + modal.contentEl.createEl('p', { + text: + 'Warning: Proceeding will delete this file entirely (including any changes you made) ' + + 'and then reimport a new copy of your highlights from Readwise.', + }); + const buttonsContainer = modal.contentEl.createEl('div', { + cls: 'rw-modal-btns', + }); + const cancelBtn = buttonsContainer.createEl('button', { + text: 'Cancel', + }); + const confirmBtn = buttonsContainer.createEl('button', { + text: 'Proceed', + cls: 'mod-warning', + }); + const showConfContainer = modal.contentEl.createEl('div', { + cls: 'rw-modal-confirmation', + }); + showConfContainer.createEl('label', { + attr: { for: 'rw-ask-nl' }, + text: "Don't ask me in the future", + }); + const showConf = showConfContainer.createEl('input', { + type: 'checkbox', + attr: { name: 'rw-ask-nl' }, + }); + showConf.addEventListener('change', (ev) => { + // @ts-ignore + this.settings.reimportShowConfirmation = !ev.target.checked; + this.saveSettings(); + }); + cancelBtn.onClickEvent(() => { + modal.close(); + }); + confirmBtn.onClickEvent(() => { + this.reimportFile(view.app.vault, activeFilePath); + modal.close(); + }); + modal.open(); + } else { + this.reimportFile(view.app.vault, activeFilePath); + } + }, + }); + this.registerMarkdownPostProcessor((el, ctx) => { + if (!ctx.sourcePath.startsWith(this.settings.readwiseDir)) { + return; + } + let matches: string[]; + try { + // @ts-ignore + matches = [...ctx.getSectionInfo(el).text.matchAll(/__(.+)__/g)].map( + (a) => a[1], + ); + } catch (TypeError) { + // failed interaction with a Dataview element + return; + } + const hypers = el + .findAll('strong') + .filter((e) => matches.contains(e.textContent)); + hypers.forEach((strongEl) => { + const replacement = el.createEl('span'); + while (strongEl.firstChild) { + replacement.appendChild(strongEl.firstChild); + } + replacement.addClass('rw-hyper-highlight'); + strongEl.replaceWith(replacement); + }); + }); + this.addSettingTab(new ReadwiseSettingTab(this.app, this)); + await this.configureSchedule(); + if ( + this.settings.token && + this.settings.triggerOnLoad && + !this.settings.isSyncing + ) { + await this.saveSettings(); + await this.requestArchive(null, null, true); + } + } + + onunload() { + // we're not doing anything here for now... + return; + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } + + getObsidianClientID() { + let obsidianClientId = window.localStorage.getItem('rw-ObsidianClientId'); + if (obsidianClientId) { + return obsidianClientId; + } else { + obsidianClientId = Math.random().toString(36).substring(2, 15); + window.localStorage.setItem('rw-ObsidianClientId', obsidianClientId); + return obsidianClientId; + } + } + + async getUserAuthToken(button: HTMLElement, attempt = 0) { + let uuid = this.getObsidianClientID(); + + if (attempt === 0) { + window.open(`${baseURL}/api_auth?token=${uuid}&service=obsidian`); + } + + let response, data: ReadwiseAuthResponse; + try { + response = await fetch(`${baseURL}/api/auth?token=${uuid}`); + } catch (e) { + console.log( + 'Readwise Official plugin: fetch failed in getUserAuthToken: ', + e, + ); + } + if (response && response.ok) { + data = await response.json(); + } else { + console.log( + 'Readwise Official plugin: bad response in getUserAuthToken: ', + response, + ); + this.showInfoStatus( + button.parentElement, + 'Authorization failed. Try again', + 'rw-error', + ); + return; + } + if (data.userAccessToken) { + this.settings.token = data.userAccessToken; + } else { + if (attempt > 20) { + console.log( + 'Readwise Official plugin: reached attempt limit in getUserAuthToken', + ); + return; + } + console.log( + `Readwise Official plugin: didn't get token data, retrying (attempt ${ + attempt + 1 + })`, + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + await this.getUserAuthToken(button, attempt + 1); + } + await this.saveSettings(); + return true; + } +} + +class ReadwiseSettingTab extends PluginSettingTab { + plugin: ReadwisePlugin; + + constructor(app: App, plugin: ReadwisePlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + let { containerEl } = this; + + containerEl.empty(); + containerEl.createEl('h1', { text: 'Readwise Official' }); + containerEl + .createEl('p', { text: 'Created by ' }) + .createEl('a', { text: 'Readwise', href: 'https://readwise.io' }); + containerEl.getElementsByTagName('p')[0].appendText(' 📚'); + containerEl.createEl('h2', { text: 'Settings' }); + + if (this.plugin.settings.token) { + new Setting(containerEl) + .setName('Sync your Readwise data with Obsidian') + .setDesc( + 'On first sync, the Readwise plugin will create a new folder containing all your highlights', + ) + .setClass('rw-setting-sync') + .addButton((button) => { + button + .setCta() + .setTooltip('Once the sync begins, you can close this plugin page') + .setButtonText('Initiate Sync') + .onClick(async () => { + if (this.plugin.settings.isSyncing) { + // NOTE: This is used to prevent multiple syncs at the same time. However, if a previous sync fails, + // it can stop new syncs from happening. Make sure to set isSyncing to false + // if there's ever errors/failures in previous sync attempts, so that + // we don't block syncing subsequent times. + new Notice('Readwise sync already in progress'); + } else { + this.plugin.clearInfoStatus(containerEl); + this.plugin.settings.isSyncing = true; + await this.plugin.saveData(this.plugin.settings); + button.setButtonText('Syncing...'); + await this.plugin.requestArchive(button); + } + }); + }); + let el = containerEl.createEl('div', { cls: 'rw-info-container' }); + containerEl.find('.rw-setting-sync > .setting-item-control ').prepend(el); + + new Setting(containerEl) + .setName('Customize formatting options') + .setDesc( + 'You can customize which items export to Obsidian and how they appear from the Readwise website', + ) + .addButton((button) => { + button.setButtonText('Customize').onClick(() => { + window.open(`${baseURL}/export/obsidian/preferences`); + }); + }); + + new Setting(containerEl) + .setName('Customize base folder') + .setDesc( + 'By default, the plugin will save all your highlights into a folder named Readwise', + ) + // TODO: change this to search filed when the API is exposed (https://github.com/obsidianmd/obsidian-api/issues/22) + .addText((text) => + text + .setPlaceholder('Defaults to: Readwise') + .setValue(this.plugin.settings.readwiseDir) + .onChange(async (value) => { + this.plugin.settings.readwiseDir = normalizePath( + value || 'Readwise', + ); + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName('Configure resync frequency') + .setDesc( + 'If not set to Manual, Readwise will automatically resync with Obsidian when the app is open at the specified interval', + ) + .addDropdown((dropdown) => { + dropdown.addOption('0', 'Manual'); + dropdown.addOption('60', 'Every 1 hour'); + dropdown.addOption((12 * 60).toString(), 'Every 12 hours'); + dropdown.addOption((24 * 60).toString(), 'Every 24 hours'); + + // select the currently-saved option + dropdown.setValue(this.plugin.settings.frequency); + + dropdown.onChange((newValue) => { + // update the plugin settings + this.plugin.settings.frequency = newValue; + this.plugin.saveSettings(); + + // destroy & re-create the scheduled task + this.plugin.configureSchedule(); + }); + }); + new Setting(containerEl) + .setName('Sync automatically when Obsidian opens') + .setDesc( + 'If enabled, Readwise will automatically resync with Obsidian each time you open the app', + ) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.triggerOnLoad); + toggle.onChange((val) => { + this.plugin.settings.triggerOnLoad = val; + this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) + .setName('Resync deleted files') + .setDesc( + 'If enabled, you can refresh individual items by deleting the file in Obsidian and initiating a resync', + ) + .addToggle((toggle) => { + toggle.setValue(this.plugin.settings.refreshBooks); + toggle.onChange(async (val) => { + this.plugin.settings.refreshBooks = val; + await this.plugin.saveSettings(); + if (val) { + this.plugin.refreshBookExport(); + } + }); + }); + + if (this.plugin.settings.lastSyncFailed) { + this.plugin.showInfoStatus( + containerEl.find('.rw-setting-sync .rw-info-container').parentElement, + 'Last sync failed', + 'rw-error', + ); + } + } + if (!this.plugin.settings.token) { + new Setting(containerEl) + .setName('Connect Obsidian to Readwise') + .setClass('rw-setting-connect') + .setDesc( + 'The Readwise plugin enables automatic syncing of all your highlights from Kindle, Instapaper, Pocket, and more. Note: Requires Readwise account.', + ) + .addButton((button) => { + button + .setButtonText('Connect') + .setCta() + .onClick(async (evt) => { + const success = await this.plugin.getUserAuthToken( + evt.target as HTMLElement, + ); + if (success) { + this.display(); + } + }); + }); + let el = containerEl.createEl('div', { cls: 'rw-info-container' }); + containerEl + .find('.rw-setting-connect > .setting-item-control ') + .prepend(el); + } + const help = containerEl.createEl('p'); + help.innerHTML = + "Question? Please see our Documentation or email us at hello@readwise.io 🙂"; + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json index 102e218..75abdef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,35 +1,23 @@ { "compilerOptions": { - "types": ["bun-types"], - - // from bun docs > "enable latest features" - "lib": ["DOM", "ESNext"], + "target": "ES2020", + "useDefineForClassFields": true, "module": "ESNext", - "target": "ES6", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, - // from bun docs (no reason given) + /* Bundler mode */ "moduleResolution": "bundler", - "noEmit": true, "allowImportingTsExtensions": true, - "moduleDetection": "force", - "allowJs": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, - // from bun docs > "best practices" + /* Linting */ "strict": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "composite": true, - "downlevelIteration": true, - "allowSyntheticDefaultImports": true, - - // unsure about all these - "baseUrl": ".", - "noImplicitAny": true, - "importHelpers": true, - "isolatedModules": true, - "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, - "include": [ - "**/*.ts" - ] -} \ No newline at end of file + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9ba6f39 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(() => { + return { + plugins: [], + build: { + lib: { + entry: 'src/main.ts', + name: 'main', + fileName: () => 'main.js', + formats: ['cjs' as const], + }, + minify: false, + outDir: '.', + rollupOptions: { + output: { + exports: 'named' as const, + }, + external: ['obsidian', 'electron'], + }, + }, + }; +});