From fe36970476693c5c8b681385e4610c880fc4f80a Mon Sep 17 00:00:00 2001 From: Dunemask Date: Sat, 24 Jul 2021 23:06:32 -0600 Subject: [PATCH] Dunestash Public Frontend 0.0.1-a.1 --- .gitignore | 4 + Dockerfile | 10 + package.json | 54 +++++ public/extras/blank_user.svg | 9 + public/icons/android-chrome-192x192.png | Bin 0 -> 1737 bytes public/icons/android-chrome-512x512.png | Bin 0 -> 6149 bytes .../apple-touch-icon-114x114-precomposed.png | Bin 0 -> 1192 bytes public/icons/apple-touch-icon-114x114.png | Bin 0 -> 970 bytes .../apple-touch-icon-120x120-precomposed.png | Bin 0 -> 1280 bytes public/icons/apple-touch-icon-120x120.png | Bin 0 -> 1101 bytes .../apple-touch-icon-144x144-precomposed.png | Bin 0 -> 1734 bytes public/icons/apple-touch-icon-144x144.png | Bin 0 -> 1100 bytes .../apple-touch-icon-152x152-precomposed.png | Bin 0 -> 1557 bytes public/icons/apple-touch-icon-152x152.png | Bin 0 -> 1178 bytes .../apple-touch-icon-180x180-precomposed.png | Bin 0 -> 2034 bytes public/icons/apple-touch-icon-180x180.png | Bin 0 -> 1426 bytes .../apple-touch-icon-57x57-precomposed.png | Bin 0 -> 745 bytes public/icons/apple-touch-icon-57x57.png | Bin 0 -> 541 bytes .../apple-touch-icon-60x60-precomposed.png | Bin 0 -> 752 bytes public/icons/apple-touch-icon-60x60.png | Bin 0 -> 594 bytes .../apple-touch-icon-72x72-precomposed.png | Bin 0 -> 904 bytes public/icons/apple-touch-icon-72x72.png | Bin 0 -> 746 bytes .../apple-touch-icon-76x76-precomposed.png | Bin 0 -> 1036 bytes public/icons/apple-touch-icon-76x76.png | Bin 0 -> 725 bytes public/icons/apple-touch-icon-precomposed.png | Bin 0 -> 2034 bytes public/icons/apple-touch-icon.png | Bin 0 -> 1426 bytes public/icons/browserconfig.xml | 9 + public/icons/favicon-16x16.png | Bin 0 -> 11427 bytes public/icons/favicon-32x32.png | Bin 0 -> 12107 bytes public/icons/favicon.ico | Bin 0 -> 15086 bytes public/icons/logo.svg | 1 + public/icons/mstile-150x150.png | Bin 0 -> 1330 bytes public/icons/safari-pinned-tab.svg | 1 + public/icons/site.webmanifest | 19 ++ public/index.html | 26 +++ public/manifest.json | 13 ++ public/robots.txt | 3 + src/App.js | 31 +++ src/Stash.jsx | 139 ++++++++++++ src/index.js | 10 + src/setupProxy.js | 12 ++ src/stash/FileBox.jsx | 56 +++++ src/stash/FileDisplay.jsx | 125 +++++++++++ src/stash/StashContextMenu.jsx | 150 +++++++++++++ src/stash/StashDropzone.jsx | 31 +++ src/stash/StashUpload.jsx | 198 ++++++++++++++++++ src/stash/Stashbar.jsx | 39 ++++ src/stash/api.json | 20 ++ src/stash/scss/Stash.scss | 17 ++ src/stash/scss/_global.scss | 35 ++++ src/stash/scss/global/_animations.scss | 121 +++++++++++ src/stash/scss/global/_colors.scss | 16 ++ src/stash/scss/global/_fonts.scss | 48 +++++ src/stash/scss/global/_measurements.scss | 13 ++ src/stash/scss/stash/FileBox.scss | 61 ++++++ src/stash/scss/stash/FileDisplay.scss | 23 ++ src/stash/scss/stash/Searchbar.scss | 120 +++++++++++ src/stash/scss/stash/StashContextMenu.scss | 48 +++++ src/stash/scss/stash/StashDropzone.scss | 6 + src/stash/scss/stash/StashUploadDialog.scss | 161 ++++++++++++++ src/stash/scss/stash/StashUploadWatcher.scss | 79 +++++++ src/stash/scss/stash/Stashbar.scss | 34 +++ src/stash/stashbar/StashbarMenu.jsx | 33 +++ src/stash/stashbar/StashbarSearch.jsx | 174 +++++++++++++++ .../stashbar/StashbarSearchTagDisplay.jsx | 38 ++++ src/stash/uploader/StashUploadDialog.jsx | 108 ++++++++++ src/stash/uploader/StashUploadWatcher.jsx | 84 ++++++++ 67 files changed, 2179 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 package.json create mode 100644 public/extras/blank_user.svg create mode 100644 public/icons/android-chrome-192x192.png create mode 100644 public/icons/android-chrome-512x512.png create mode 100644 public/icons/apple-touch-icon-114x114-precomposed.png create mode 100644 public/icons/apple-touch-icon-114x114.png create mode 100644 public/icons/apple-touch-icon-120x120-precomposed.png create mode 100644 public/icons/apple-touch-icon-120x120.png create mode 100644 public/icons/apple-touch-icon-144x144-precomposed.png create mode 100644 public/icons/apple-touch-icon-144x144.png create mode 100644 public/icons/apple-touch-icon-152x152-precomposed.png create mode 100644 public/icons/apple-touch-icon-152x152.png create mode 100644 public/icons/apple-touch-icon-180x180-precomposed.png create mode 100644 public/icons/apple-touch-icon-180x180.png create mode 100644 public/icons/apple-touch-icon-57x57-precomposed.png create mode 100644 public/icons/apple-touch-icon-57x57.png create mode 100644 public/icons/apple-touch-icon-60x60-precomposed.png create mode 100644 public/icons/apple-touch-icon-60x60.png create mode 100644 public/icons/apple-touch-icon-72x72-precomposed.png create mode 100644 public/icons/apple-touch-icon-72x72.png create mode 100644 public/icons/apple-touch-icon-76x76-precomposed.png create mode 100644 public/icons/apple-touch-icon-76x76.png create mode 100644 public/icons/apple-touch-icon-precomposed.png create mode 100644 public/icons/apple-touch-icon.png create mode 100644 public/icons/browserconfig.xml create mode 100644 public/icons/favicon-16x16.png create mode 100644 public/icons/favicon-32x32.png create mode 100644 public/icons/favicon.ico create mode 100644 public/icons/logo.svg create mode 100644 public/icons/mstile-150x150.png create mode 100644 public/icons/safari-pinned-tab.svg create mode 100644 public/icons/site.webmanifest create mode 100644 public/index.html create mode 100644 public/manifest.json create mode 100644 public/robots.txt create mode 100644 src/App.js create mode 100644 src/Stash.jsx create mode 100644 src/index.js create mode 100644 src/setupProxy.js create mode 100644 src/stash/FileBox.jsx create mode 100644 src/stash/FileDisplay.jsx create mode 100644 src/stash/StashContextMenu.jsx create mode 100644 src/stash/StashDropzone.jsx create mode 100644 src/stash/StashUpload.jsx create mode 100644 src/stash/Stashbar.jsx create mode 100644 src/stash/api.json create mode 100644 src/stash/scss/Stash.scss create mode 100644 src/stash/scss/_global.scss create mode 100644 src/stash/scss/global/_animations.scss create mode 100644 src/stash/scss/global/_colors.scss create mode 100644 src/stash/scss/global/_fonts.scss create mode 100644 src/stash/scss/global/_measurements.scss create mode 100644 src/stash/scss/stash/FileBox.scss create mode 100644 src/stash/scss/stash/FileDisplay.scss create mode 100644 src/stash/scss/stash/Searchbar.scss create mode 100644 src/stash/scss/stash/StashContextMenu.scss create mode 100644 src/stash/scss/stash/StashDropzone.scss create mode 100644 src/stash/scss/stash/StashUploadDialog.scss create mode 100644 src/stash/scss/stash/StashUploadWatcher.scss create mode 100644 src/stash/scss/stash/Stashbar.scss create mode 100644 src/stash/stashbar/StashbarMenu.jsx create mode 100644 src/stash/stashbar/StashbarSearch.jsx create mode 100644 src/stash/stashbar/StashbarSearchTagDisplay.jsx create mode 100644 src/stash/uploader/StashUploadDialog.jsx create mode 100644 src/stash/uploader/StashUploadWatcher.jsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d196ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# For Deploy +build/ +node_modules/ +package-lock.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bf311e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:latest +RUN apt-get update && apt-get upgrade -y +RUN apt-get install libsass-dev build-essential -y +WORKDIR /dunestorm/khufu +COPY build /dunestorm/khufu/build/ +RUN npm i -g serve +ENV KHUFU_DEV_TOKEN $KHUFU_DEV_TOKEN +CMD ["serve", "-s", "build", "-l", "52026"] +# EXPOSE PORTS +EXPOSE 52026 diff --git a/package.json b/package.json new file mode 100644 index 0000000..08b48a4 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "dunestash-frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.35", + "@fortawesome/free-brands-svg-icons": "^5.15.3", + "@fortawesome/free-regular-svg-icons": "^5.15.3", + "@fortawesome/free-solid-svg-icons": "^5.15.3", + "@fortawesome/react-fontawesome": "^0.1.14", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^12.0.0", + "@testing-library/user-event": "^13.2.0", + "http-proxy-middleware": "^2.0.1", + "react": "^17.0.2", + "react-axios": "^2.0.5", + "react-dom": "^17.0.2", + "react-dropzone": "^11.3.4", + "react-fontawesome": "^1.7.1", + "react-router-dom": "^5.2.0", + "react-scripts": "^4.0.3", + "react-toastify": "^7.0.4", + "remove": "^0.1.5", + "sass": "^1.36.0", + "web-vitals": "^2.1.0" + }, + "scripts": { + "start": "PORT=52026 react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "serve": "^12.0.0" + } +} diff --git a/public/extras/blank_user.svg b/public/extras/blank_user.svg new file mode 100644 index 0000000..03fcf85 --- /dev/null +++ b/public/extras/blank_user.svg @@ -0,0 +1,9 @@ + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + +Layer 1 + + + \ No newline at end of file diff --git a/public/icons/android-chrome-192x192.png b/public/icons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..27923ab64bb6f91a6ee55189ffa057048d30beed GIT binary patch literal 1737 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 literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-114x114-precomposed.png b/public/icons/apple-touch-icon-114x114-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..97f1d276d50bda2df7017a57b52da71d5f585168 GIT binary patch literal 1192 zcmV;Z1XufsP)Px#OHfQyMNDaN|NsB}^xFQ&9smFT|NsC0|NsC0|NsC0 z|NsC0|NsB$sQ%0@|Ni>__1pf~VE+2;{m(%D@yz|>l>ORp{nJtX?Y#Wog!$EDb#VUI z0000AbW%=J03PiBzI-D#h?Ltv|4EAx z3@R8Sr;BE%)0xx^Us;y@cNsjLUUs`)P@BQf<$61L?0j#t+WovF{2#X>iQGjzGI#vkD6t{Xn zh5XbT2avGC?iZygjZ`K~AmPqO6vIO*4R2{$NGd}4ZCthiDmp23HXP^e zBYuMP`gGyP=TQo^xdN~z3dy^7i4E;b5POkq4unz(ZHh2Are}%Czf+GY3GD2Q5LOOU z0{WUFOR6N?bs={5sT3S*diVFd?Et0c;&Bq|tLlE*3uu|B070MK)*Bp{cP zFkqELF*pXUz+3S-5TlgD>~=KEddz6H&$-V>NF7!Z8>g$WO+2k++Gv+LoC-FS#1<#B z?b3`5dNw67{Xgob$7b0*#0*M87-Sv5NtC4BDjr|^6=Az|TEFz9Qxds6c|vKGq+z7f z<_gKO#7(Ru1dO#8qJTL63UQPKu#|1kEU_ctE*JsBLh5WtoXN{rBwMx!bM@EZ+DW(0 z?W2`s-ga$8raeN9L{2`Xyw^&C?I#u1t~jLKd>Ch&U%{6&6|S&%T7^&uq1xr~O8g|4Xm4`hYwVihjMRk-?B z;eum@Yn(MMt5&#@TjAnyjqBAlE|FKbDqrJ;fEuqoRC&3h;43VDP7sGvnrha$00012 zdQ@0+Qek%>aB^>EX>4U6ba`-PAZc)PV*mhnoa6Eg2ys>@D9TUE%t_@^00ScnE@KN5 zBNI!L6ay0=M1VBIWCJ6!R3OXP)X2ol#2my2%YaCrN-hBE7ZG&wLN%2D0000Px#Ay7UBv#~S|4HT~CM|MlDd=%fGZum1Y) z{_x5D--Q+OK+ymI0^~_VK~z}7?U=o9+CUV*J=DdFK0IYRJO2P53M!imB~2!Vq*6My z3W?HAk)SH=L(RR{iGYDcG3;f zZFF|VOioJTwjc{qNg`ZF{ywDN_G*)2b&mqA|7azfckI3$N>`Pw-o z7b?fT?|UQixvL=ANuVQ`YQJ+zK>MYFEQjpEp(C;!_%gt9vfQR8AW|W9CAi(PD7JQJ z4SK(>Kel~3rHKCC4ppusU~2&<$MpC(qTXOFSJq43rUSv<@8=gQq~nS!xZ+ppRa&^( zmB-u_#9dJ3$_d=p&(B{XeiUj~jv`n$dswJzmZZ`O0*}qyRfEx>dK|cmF)G9 zJO^;S6C&DZl`F}!C;9s)sUc}rpmGHzu3Ud2_vb8`+dEOY0?oAhRpJUccwWc>Lt(JEagsD-ZJ;rgCDVrf(H#(N>#y?H*MGZqv49Z z>B{(iq2Y=x>|`=mN~-sY049$@OIQ6Cgx@G{a;A sBePT>%h=S&#LUDT#0SfONT5nC0O}VJbn-$ql>h($07*qoM6N<$g5&JXwEzGB literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-120x120-precomposed.png b/public/icons/apple-touch-icon-120x120-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..464c684357f79bb2c9021241d43abe6015d21df0 GIT binary patch literal 1280 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 literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-144x144.png b/public/icons/apple-touch-icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..ba92f5dcf229018a2fea7643164c8c2f1386d866 GIT binary patch literal 1100 zcmV-S1he~zP)Px#8&FJCMgGVg|NsB~%PRlz%KrA@{^_aw&qn>=hx^uM zOIbwa000AQNklg2b5QZa54O}CFxJFdaz3wisTq8hm?Jm$gMtt_zmXhU& zfZg)5OYE48^I^t-R;^^8Y7!o1#^cv-Qp38^Ff>SmG)RLq=zoFiKf+j^4JtSW9OV8= zAfs|Eka9>_AmxxUL8>8Tf>cAw1}TP=4N?qA0#XY}0#XY}22u)122u)13Q`G43Q`G4 z4pIn74l*k3lLH4Lmw-S(&drC2NP{%!bA!mty9S8Nf3-Ugn~Oo@FT1e0 z6tv}6)+t}VTcbhKqZ${z6fH-C?%YV%yNPJ078aEM3Qve(jFWeBZTzig1l`($!C7YiEG8nbn;zQ&Uy&xhMR3}_zYQ+NqyW>7ABEuP~IOdnB>{c@at zafRdpFt|G##1_60Z>c6v>`-az8>X`-!Sshv`=Cs=A5n`A7Ss`&nU!f6+%gT~eKL@s zf|ur#?D}fI2skAN2?B+N$?;*Yal3zF50ksy1Y4$ol#%fex(+8{0*ek3R6XXithL*9 zKPFH1Mpf9%1^c2elqsAN2Ms#bmzUlQR;(3b$7o>#>%>Kb%=Y^cuRR(x1?$~OXzc{8 zkWgvk3r@E&62z3&t`7#aWD6R`GgjElKF#Q#!*xwONOBeu#0uT@z@!-y^de^Y2`zMo z|EBpW>Kql~pNNKJ9B^>ZeHJBmIE&ym$p%$i(9h96>%qLA&ZT=z0pJO5%U2hltM0q z3R4nP$ekca5``Scminmc9X`9~NIpA;;ADMTz#%3fW9)b34#l zYEcBel7bSg%=)UcVr|vHDrCw7+8FeC5v!2Jl!s5ShXv`EI{(y%*vO6cE`09iMyKOl z2ntCF({;w1h{t}r{V=-5D-Clt({F1u=(B;!XO8$AcI*3@qo)7it3ev1K^mk%9|!af zoq9avo~%oD00012dQ@0+Qek%>aB^>EX>4U6ba`-PAZc)PV*mhnoa6Eg2ys>@D9TUE z%t_@^00ScnE@KN5BNI!L6ay0=M1VBIWCJ6!R3OXP)X2ol#2my2%YaCrN-hBE7ZG&w SLN%2D00000{{R3FC5Sl00004XF*Lt006O% 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 literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-180x180-precomposed.png b/public/icons/apple-touch-icon-180x180-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..97d306d97a4096ece6a73a717771f285ee646d07 GIT binary patch literal 2034 zcmVPx#MNmvsMNDaN{PNcR$Q}Rx|NsC0|NsC0|NsC0|NsC0 z|NsB~#~T0s{QcNu{_ep1$twQnr~dlw{LVf5+SEm!+=#jg%000KkNkl!42EZEXWDX-gYf>3x}hhDpyEcU ztYt1{-*mdaVo35QboluR>~{IR-f?>X!}sg&<@U2^x8HkvFuVWGb6>9QA?iO`>#E;{l%EC?ZNFDy2iadyxnj0F$dIDFk9-X{5j&+e{D7Pi6OVPBD>Y*9G-(* z+N$hQ-$K8Z`qoxv&Z~cGgZ`~2uYe3wop(6@xYP%Isn4aIQ(x+!LmNt0sVqt1Z-9(4{h`xbQ$bkj&&A@r9#Boq2O9+CEBoLw{qfH-Iiry#;h}57`2`_*!oRU6^_!=%&LJLKpSxt)LrLaIH6kwtmekgf3U} zZ3$h>ueXG5nCx~izAyAXMo;LijLq1&(kJu@eL|nmC-ez@LZ8ql^h!hM@dH%@ZBd0F zRv7g{ux4}cJ)jX1lIq7S6mzbr6dPl24~-hrEBNbLiUu7R8X*%A%UJhfEj402K}Ddg zW9WEiLJ_u(Sx{UA8Z|^s_EUJbX0a`E+m?WidInhRbZFwh_J>An$}@(=?pfkulgFpu z8`^F_3i@`KYo(ONu$f0q3N@0?C$=;6XajQ95hEC5!jNA1+?p&dwhbFEFWVIwH4vFv zvS&WA|5&%7o~z@f{fHR-{IB~#+m!8ZK;D_rV%l`$4+WH23>)tquvcqC| zh36!kOy@^r-fza`UK!CM_Zla-taTFZKMXXr1rwNap-1~~)?fX^qoN;f*n+dnnb3@a z0)xz!MM!v_?e-%YKQ;34p;_i!Xa!5TrC3IW=YrL^^b^A{Oq-?zG*3z!t_t!Kqt zGkrA9#F~_rxh>N|5Om>4%6#nx-l{91#v7*w;O{)`e|iY$mFo9a-V4&d$BAt9BxzMNqIopYm37_{qnU+)K=OAtHG;=OAgN;%}CI=OuhcH>_ zf66~R7iTh|0Wx3E97N6LN9P*+!(2v<^WH3I4&d3m8&#~Xg2GA6+d)tJDX9LnU$Odq zO5f?1GUrCnJarHx=%^zsT(A*Vj&o0PH3gtCO_sJu!1KwNQz?56`U<3g8c#@aFhs*8|VG%XfW`Q>&!Ae21sK0U5$e0xzS+;cPD0WhNSACcM`@umaGF!*QM5 zUwuuC_E+N#S?VAxPisf=W%q$T^b8v?I_b=caST;Ll9%BEYTTcuJ;$~#r91~f0yHG~ zas_MaS6rfnV0thdCw>w{u=R@<3!;y}(L07bp#B~mJ2dO>^FzX6XgfXl&mZW6e)QDP z2t<~dN)8QQbDgO}a-m^AI+pl8U0|;uotl(-ljynHDHB>MAo}K}mSC~9jFwuThTwc~ z+)jDP4~Zp;M-|USs3=3&V0Fe{bNP6=@-)PHp`jg0@=xC2`#N z#p@uj5-o)EqonX00=6;TSOZ71co8*@yA@}Rfr~sCt--RYfEwpit{jd_h4X5vY-qK1 zo*xuf@Dy#zyXD5|^~k|~1hd8>W9I)nfNu9APt!Jq)OhMVdempK$P67D`gvGEz2=M< z>u$w%{^pl+kehxeXT4eVO@}Xkqv&-2XXVy7zXo+epU@}t34KDJ&?oc(p?h*9Q%TV8 zLv{zTHVyjdc0!-fbI|quA@~;8`6u_QyxgG5Yb2_?u%pVWQL4Ozrq1hrs=QdK$}6nu zyezEBYuW0&0I$lc2J5`kvCiu=>%6G6$}3^(ynMIPYmV!@P`b{myDPmUy^`zb{{d=) zs;R6^#i{@R09SfcSaechcOY6Cgx@G{a;ABePT>%h=S&#LUDT#0SfONT5nC0O}VJbn-$q Ql>h($07*qoM6N<$f}&dUApigX literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-180x180.png b/public/icons/apple-touch-icon-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..8bfb5417a4c7218af84cde0d642e163f9f1802b4 GIT binary patch literal 1426 zcmV;D1#S9?P)Px#8&FJCMgRZ*{>UBv$Qu3FW&ZEQ{`lzp&O!d@rTpK9 zNJ-W4000ECNklf#j5QcrQm0UeY;OlNAtJj2la6|$Uj!1#P)igCZwrYVK z6NQle?AqS-$9mT_V{BEab`&WB%;RNtX5QJcVQAB)O`A4t+O+-8Hj`{p7kcmK?Wr#G zxh>gCbLdWU=(?7zLOac&Yg)Dv-DwV8&$88Mr#W;j%Pv4W&7tk@&7qy<&^36w5Z!4G zef+YE(Vgbd_Iq<^r#bYovHdr+vpv`*NHoIwp99^tz0BWdS+z!Mv_@<6zd{4CDQN-? z#J}0@eIT`?!QIUa!EpubK|yd)&${aVB^)-$Lu|+Oz#0 z`-XK+R97L5qtD25i8LUeOOyEiocZPC3JHXd#5 z)j4GD`MRUuW_cCRa()Emg547+rG$Cyf#A^tMOG8$5^H?zf89k8b7`Z|%JR&kBpj3S z|57z9N1NyB3c3=%v;eHi^v&x6xe~-7h#|Cq4PR3-jZRr{z`8)q8 z+1(rD(n;-(a&(4#V~BMsNXnQ!y*Q>>AiKn^NiY)5p2j;}Nznz}-6U^6jRGHy6R*8g zK*Qri2#1(O+rIC|UJK%}0@@LEcbb+9nl_k0hT_rf{AKs5?O5SuHx?r&XbOv zvUQ8~89GuWDMLeR&HHt!GZ9=8wIO2LQ-gQbH&6 z(PxlrCOdsu zMbTS*RqF`E-wih&O>SQS-CherMnnZ+|iGIAM(HgDMjiTSWwOHTK zrcIkRZQ8VH)28is`vdmQtjqc&+WG(h09SfcSaechcOY6Cgx@G{a;ABePT>%h=S&#LUDT g#0SfONT5nC0O}VJbn-$ql>h($07*qoM6N<$g2Z;dE&u=k literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-57x57-precomposed.png b/public/icons/apple-touch-icon-57x57-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf0c1da6c066c6aaeb1e6915c9b58211800ea97 GIT binary patch literal 745 zcmVPx#MNmvsMNDaN{>UBv$Qu6QivR!r|NsC0|NsC0|NsC0 z|NsB~%PIcSNdDSu|MSoP>#YC$@&3*?{`lnn)mi=Lp#JT*{@#B6@WvQb%0B=A019+c zPE!E?|Nj9&|J^K(NI0Le0005WNkl<4gNoo6zrMOTh;uF;pFfE|P_q<-Z{Tqk{xu#3DHP zWT8d^_ZG)T@psO+G70lyvujId*nz>9NFZ*Fh!;+%xIi%QEM#Rxl6%dx=hz~`<7SZ} zylC7{6_A zjm;`sfu|L#a24W$wN^PS{)Ck!B{IgO^i?YX(=B4bG2C7b&RW4WWNtJ#$z-1uTeq_9 zxNTH0Q?x?8+;68ND+x2)IsD~WD~8JW_ado*)G@1VNLF~wijdig6x*=!WMx(rD?G&R zq7{77ycN@sm91bCaikws0?&P}T9NEASXg(Q5Zp(Q{H(lY#q5qBv?BdN8?5-xvuAIG z)U3Q8cj95?^hq7Novien@t=qG-~X>DdLrze?1*9P1jh^azW%#rXSw6;!kzlUJ^sSC zL%hqM{9Z}WFTfE*{8Y!hlK=n!S9(-fbW&k=AaHVTW@&6?Aar?fWguyAbYlPjc%0+% z3K74o@Px#Ay7>>YzG+Nl2uCs0YekIj8~S$5T7g- z&B(HtnvrEOHKWL~C>tXI3p80&;5s;D*^0_=%d%MtN|-@qywGJ?G=VHh6^KG(blIhX zldFm+3MYWrslsTo$&JlGiDbcWAe(Dq7^*DWM9~ExU6nwqSu{iw&}2IyHgW|@8e0mQ zpt+e%4TzU7V_bGz&=_m7zK73 zb|0~5N~i$|JU%iq#p)xLRZD;=4~LIfR6AOM1}vTd^pP8ek4k|oQ6Mud0gI241)J5C z8zF`!qx;BdBE&~#SbY=-G87m+k|hi*l^8zifcZ#NT@|a3z#tiikD#C!K74o@G|K literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-60x60-precomposed.png b/public/icons/apple-touch-icon-60x60-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..d2f24d04dd9be9443a3d1b22dbdaf867cb02c5ea GIT binary patch literal 752 zcmVPx#KTu3mMNDaN|NsB~$|e8t$p8QU|NsB~$Q=Lw|NsC0 z|NsC0_TK*4Xa3DI{^5oH`|$qMRQ~L<{m?@G-FW@ukpAbNRl2vj0000AbW%=J0RR90 zkBt8?-2tqf%gz7*0oF-GK~zY`?U&n@gCGn=HFzuc+y8&-5TG4(RS|fbHF>eCc1vKsmjr414g}07{ap|cYo`&(UzNfgv}Z-oKUl{=Z$qpV~Vhg>-!M5pI45{1_$KCS{>RMyG9aCumWDNisg)X*yYklq6X2cy>_=F>ElT1?3AXGrq6XF z8Gj7iB*@a@NMbt^VMQFtodV#BBN@%j<@(b|$_FE9VXt*=6C6oVIg(*pp`N9Yw8fD; z+KhV-Dpxa-haEE-eN5>2rVq+_VgBHM6;#C$rlc+ouN7_4_u@LY!&BeOdVAj6tM7o) zcQoodl!fEbI@KKq=Y;+MG>|-zh-JhJ00012dQ@0+Qek%>aB^>EX>4U6ba`-PAZc)P zV*mhnoa6Eg2ys>@D9TUE%t_@^00ScnE@KN5BNI!L6ay0=M1VBIWCJ6!R3OXP)X2ol i#2my2%YaCrN-hBE7ZG&wLN%2D0000Px#Ay7#_R3;Dk}yNLGs{GH&DuUz-Rcb`O$Iv+1Q<01eBhvE`0 zJg$9PMy?8cuIbzS#M=fAr+tPqNcG);!lmGgU0bJkDEzz9nr!w~o^;}|d* z=&BsS^O{~_&A$f!$3JVCo3II17)JmA09SfcSaechcOY6Cgx@G{a;ABePT>%h=S&#LUDT g#0SfONT5nC0O}VJbn-$ql>h($07*qoM6N<$f)#QSssI20 literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-72x72-precomposed.png b/public/icons/apple-touch-icon-72x72-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..6ba6c2dee8018cc7a6084c41e6ac6375a5ec7643 GIT binary patch literal 904 zcmV;319$w1P)Px#KTu3mMNDaN{>UBw|NsB>+5i9l|NsC0|NsB{@&Et- z|NsB~%rE}O8vfyk{@QB(?YREdTK>u={pOzj&p!RqO#R$^J}b0&Yn}K~z}7?U>oNs~`+OMNW$)k@^3>^)T5|y(%bg_vERrRd-3U z8Okukm>w@SUI_MjPD2qtr>Qe8oR_aJo|ti!>bSIHM~7! zWwe{Q^o-83jF~p~Z2@ue6_R!WHIsHht)zWWBk2ItMmhpDkq$vEq+<}Vcow1#bRP2` zs1pdffPiqr6~Ijp@L5ns1)5tRAX4OIk|Br9kD!Gr5Exyowp4zTzz9|a0_DOSV*(xO z_z6{k0xQNE5D**Y57UN#ue8B{66Q^UFq1VuuTjcM*xY;YiNwtzJ^RtJz=2Z`= z(6Lyc@|KxK3rf`rnGE>68cf$(W5RM>E2I_0a-5S1vo6lOdLcQ6icsxlhjcSbz|uM) zm5kzG6w;S!B_s*e(iJs|ri8-4GQQBU#VkYIr|yCkF$l|m9J z@&Pp^vlf!J35n85C)j3%l(j;}`AHoQK4v{jA^qx92(aI^_+l|B()ivhWO!moDuonw zbKYV?1|es$66pvOvRdyyi;$a@?LbI0n_arFVf=LdylekDPS~{A()GUEe5~<1oYOdO zMtk9W6m9sgkA8kHycBK)8tKJgYtZd01lEVc`u$yh^Z`BlU1@Z?Wa)S9)9)gx+tpaV z%epYA=hf@OM;^{ErN%$=d@=tT00012dQ@0+Qek%>aB^>EX>4U6ba`-PAZc)PV*mhn zoa6Eg2ys>@D9TUE%t_@^00ScnE@KN5BNI!L6ay0=M1VBIWCJ6!R3OXP)X2ol#2my2 e%YaCrN-hBE7ZG&wLN%2D0000Px#Bv4FLMgGVg|NsC0_1XT-G5*II|Ni>^;fns+Z2r_! z{_C;-`tAP8CTD3Ah5!Hp0ZBwbR7l6|mcL8GKoo!vyrJOq0vYUIuRJ%Ed75wLXm&9V4yEdB&zHYrQ@7{g)@(6(hLH?oS z&_)pUG$?&`pZ($p8>HB_GWsUeLYfVafp^YaR=IUSYGKl4$+YT!`(d-2CqI;>Bb?N za$%6;tZ3*xu}E9ci`7)QhguU?dCArq4D))mLJV31?b0vV+K1sSd3W`L`03Wh$Eq0c z*w4b?MFQQSZ7ZLrcr5XjY7L2}IXqMOJd;7za%@QV82ymR6pLb&lu!C~O3HUKnH_%^ zh{?8?B1X>2ZmQd zW6Cgx@G{a;ABePT>%h=S&#LUDT#0SfO cNT5nC0O}VJbn-$ql>h($07*qoM6N<$f@xw<9smFU literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-76x76-precomposed.png b/public/icons/apple-touch-icon-76x76-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..83b6b7fcb71acf38015613030262358b635bc7b2 GIT binary patch literal 1036 zcmV+n1oQieP)Px#R8UM*MNDaN|NsC0|Ns8T9smFT|NsC0|NsC0|NsC0 z|NsC0|Ns8S8vpz5|Ni>^*I)kOg#OS#{?bbR$s+yam;TEu{`TJe%{Bh<%>L=9{oHl_ z?YsWbL_1Y>5&!@I3UpFVQvgp*{~@5F3gdgWgIsg~00P}fL_t(o!|j;acB?Q9MM-HU zWNh#Z{QrMm$u2R)YcD^%;|78p}vW4`?a*3}!6sx1e5hxR+{ z4cZRub=n^6W!f(6RoXu6McNY>(%5rrGhwx!`e!z&T-a|WZ&r&>VQc$?{U;dI+FFS} zK8Jzi-WlUOmk)6=9|mHT1o$x}Xnl7zO77-9i@R!i-o>NVd*y8yev_L8fRmPniSFl~RI7 za!ha>{u;6?U`7otxY4?p3b4Xqk2Y8l;fd#qw&re`CGFV5(!;x;U+7$}Xy*{M#+WcJ zumqMM#$s}rqD@l3n&j5nDV&z6O$l)PTx7u{r|dJ|*W^YLRx=+pyo+Ih>8xyeMp0ii zo*?4Y|%DuGkIa6M7n`ol@o~OQT&6 zs^>AY4|j`Y#-r>$U#W2WP`JqWK|_iC42#w32OHMk8d?H2@VCw1!>HT5Z9iZot|E$F zk{rvyPSsaA*rDu|SJBJ3ve$$~FEGnqt(Lv?CEVfk{&n>6E1~`Y5F^BiY|kyo00012 zdQ@0+Qek%>aB^>EX>4U6ba`-PAZc)PV*mhnoa6Eg2ys>@D9TUE%t_@^00ScnE@KN5 zBNI!L6ay0=M1VBIWCJ6!R3OXP)X2ol#2my2%YaCrN-hBE7ZG&wLN%2D0000Px#Ay7I>@ zf*Pg{NFXMlEg-fOQ7Sfw0P!uc<0O=DX)^-iHYY#b}SJL-F?MKI~$hfE`g=FOJXVRk{G!xCQV?Nd6KjX4Et`oe-oqCEZFArV|P4u z#sRGB7%+BFtj5*J+KI50a||;)sNCFgm<#ip;2385V6PIg=)rwBRF?A{qnD5m9@m5p zTNfpyggRIq?ZpM#Q7LF=32d3YM6>h+(+BH*6gz?SVriWrKSl#)hBN1h_1z1nF zh3>i=);Pg+*S=yo&U7^DLrK1;ZFY|722*j%S-)kvFpJ7-w<2AnY~@}&P>G;{NP!SE zSU3-<@|sfC*HA!}UlqluD-Xd07|pIka`=#M3z!j~%_MD9-Wq3vjrHevH0lDwlh#90T^tv+_A7R8X>F9nA0sw1%EbS#)bGMR0CMK(=~RdTC;$Ke zS9(-fbW&k=AaHVTW@&6?Aar?fWguyAbYlPjc%0+%3K74o@(}8 literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon-precomposed.png b/public/icons/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..97d306d97a4096ece6a73a717771f285ee646d07 GIT binary patch literal 2034 zcmVPx#MNmvsMNDaN{PNcR$Q}Rx|NsC0|NsC0|NsC0|NsC0 z|NsB~#~T0s{QcNu{_ep1$twQnr~dlw{LVf5+SEm!+=#jg%000KkNkl!42EZEXWDX-gYf>3x}hhDpyEcU ztYt1{-*mdaVo35QboluR>~{IR-f?>X!}sg&<@U2^x8HkvFuVWGb6>9QA?iO`>#E;{l%EC?ZNFDy2iadyxnj0F$dIDFk9-X{5j&+e{D7Pi6OVPBD>Y*9G-(* z+N$hQ-$K8Z`qoxv&Z~cGgZ`~2uYe3wop(6@xYP%Isn4aIQ(x+!LmNt0sVqt1Z-9(4{h`xbQ$bkj&&A@r9#Boq2O9+CEBoLw{qfH-Iiry#;h}57`2`_*!oRU6^_!=%&LJLKpSxt)LrLaIH6kwtmekgf3U} zZ3$h>ueXG5nCx~izAyAXMo;LijLq1&(kJu@eL|nmC-ez@LZ8ql^h!hM@dH%@ZBd0F zRv7g{ux4}cJ)jX1lIq7S6mzbr6dPl24~-hrEBNbLiUu7R8X*%A%UJhfEj402K}Ddg zW9WEiLJ_u(Sx{UA8Z|^s_EUJbX0a`E+m?WidInhRbZFwh_J>An$}@(=?pfkulgFpu z8`^F_3i@`KYo(ONu$f0q3N@0?C$=;6XajQ95hEC5!jNA1+?p&dwhbFEFWVIwH4vFv zvS&WA|5&%7o~z@f{fHR-{IB~#+m!8ZK;D_rV%l`$4+WH23>)tquvcqC| zh36!kOy@^r-fza`UK!CM_Zla-taTFZKMXXr1rwNap-1~~)?fX^qoN;f*n+dnnb3@a z0)xz!MM!v_?e-%YKQ;34p;_i!Xa!5TrC3IW=YrL^^b^A{Oq-?zG*3z!t_t!Kqt zGkrA9#F~_rxh>N|5Om>4%6#nx-l{91#v7*w;O{)`e|iY$mFo9a-V4&d$BAt9BxzMNqIopYm37_{qnU+)K=OAtHG;=OAgN;%}CI=OuhcH>_ zf66~R7iTh|0Wx3E97N6LN9P*+!(2v<^WH3I4&d3m8&#~Xg2GA6+d)tJDX9LnU$Odq zO5f?1GUrCnJarHx=%^zsT(A*Vj&o0PH3gtCO_sJu!1KwNQz?56`U<3g8c#@aFhs*8|VG%XfW`Q>&!Ae21sK0U5$e0xzS+;cPD0WhNSACcM`@umaGF!*QM5 zUwuuC_E+N#S?VAxPisf=W%q$T^b8v?I_b=caST;Ll9%BEYTTcuJ;$~#r91~f0yHG~ zas_MaS6rfnV0thdCw>w{u=R@<3!;y}(L07bp#B~mJ2dO>^FzX6XgfXl&mZW6e)QDP z2t<~dN)8QQbDgO}a-m^AI+pl8U0|;uotl(-ljynHDHB>MAo}K}mSC~9jFwuThTwc~ z+)jDP4~Zp;M-|USs3=3&V0Fe{bNP6=@-)PHp`jg0@=xC2`#N z#p@uj5-o)EqonX00=6;TSOZ71co8*@yA@}Rfr~sCt--RYfEwpit{jd_h4X5vY-qK1 zo*xuf@Dy#zyXD5|^~k|~1hd8>W9I)nfNu9APt!Jq)OhMVdempK$P67D`gvGEz2=M< z>u$w%{^pl+kehxeXT4eVO@}Xkqv&-2XXVy7zXo+epU@}t34KDJ&?oc(p?h*9Q%TV8 zLv{zTHVyjdc0!-fbI|quA@~;8`6u_QyxgG5Yb2_?u%pVWQL4Ozrq1hrs=QdK$}6nu zyezEBYuW0&0I$lc2J5`kvCiu=>%6G6$}3^(ynMIPYmV!@P`b{myDPmUy^`zb{{d=) zs;R6^#i{@R09SfcSaechcOY6Cgx@G{a;ABePT>%h=S&#LUDT#0SfONT5nC0O}VJbn-$q Ql>h($07*qoM6N<$f}&dUApigX literal 0 HcmV?d00001 diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8bfb5417a4c7218af84cde0d642e163f9f1802b4 GIT binary patch literal 1426 zcmV;D1#S9?P)Px#8&FJCMgRZ*{>UBv$Qu3FW&ZEQ{`lzp&O!d@rTpK9 zNJ-W4000ECNklf#j5QcrQm0UeY;OlNAtJj2la6|$Uj!1#P)igCZwrYVK z6NQle?AqS-$9mT_V{BEab`&WB%;RNtX5QJcVQAB)O`A4t+O+-8Hj`{p7kcmK?Wr#G zxh>gCbLdWU=(?7zLOac&Yg)Dv-DwV8&$88Mr#W;j%Pv4W&7tk@&7qy<&^36w5Z!4G zef+YE(Vgbd_Iq<^r#bYovHdr+vpv`*NHoIwp99^tz0BWdS+z!Mv_@<6zd{4CDQN-? z#J}0@eIT`?!QIUa!EpubK|yd)&${aVB^)-$Lu|+Oz#0 z`-XK+R97L5qtD25i8LUeOOyEiocZPC3JHXd#5 z)j4GD`MRUuW_cCRa()Emg547+rG$Cyf#A^tMOG8$5^H?zf89k8b7`Z|%JR&kBpj3S z|57z9N1NyB3c3=%v;eHi^v&x6xe~-7h#|Cq4PR3-jZRr{z`8)q8 z+1(rD(n;-(a&(4#V~BMsNXnQ!y*Q>>AiKn^NiY)5p2j;}Nznz}-6U^6jRGHy6R*8g zK*Qri2#1(O+rIC|UJK%}0@@LEcbb+9nl_k0hT_rf{AKs5?O5SuHx?r&XbOv zvUQ8~89GuWDMLeR&HHt!GZ9=8wIO2LQ-gQbH&6 z(PxlrCOdsu zMbTS*RqF`E-wih&O>SQS-CherMnnZ+|iGIAM(HgDMjiTSWwOHTK zrcIkRZQ8VH)28is`vdmQtjqc&+WG(h09SfcSaechcOY6Cgx@G{a;ABePT>%h=S&#LUDT g#0SfONT5nC0O}VJbn-$ql>h($07*qoM6N<$g2Z;dE&u=k literal 0 HcmV?d00001 diff --git a/public/icons/browserconfig.xml b/public/icons/browserconfig.xml new file mode 100644 index 0000000..6e1de61 --- /dev/null +++ b/public/icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #ffc40d + + + diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..5a03560577303133eb6f6247e63dc483459de981 GIT binary patch literal 11427 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

rX9AP9WLzSJMsjU>jjSleqHqubLkjJ&NM6 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 literal 0 HcmV?d00001 diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b035a057955095c626e760c5b7c9a90a8ea2a2 GIT binary patch literal 12107 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 pZ>^>P%2JtYKZEC^zk{>yr8ra4lF \ No newline at end of file diff --git a/public/icons/mstile-150x150.png b/public/icons/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..ee50858a49c1fb14e8648132327f82eaed11760f GIT binary patch literal 1330 zcmZWodo&XY7@wAiwh`e-GvQLY8-?oFayJ_$*37)h+eNOtN84noc|@IJR>_n^A-nR* zBe$zE)HaVyUR{stwHvPM(K=b1-POOo^L^+0e!uTKzw`U&%Ow(AKpKV`0001lcg2zb z02T1}+6h!Ld8KoAlx=4e+5-&$)ZfySg{msCN;1g>1E}pY`lJN=iSA^advu~gE>+0B zD7IwZ2l=K#A^(vI^%m)x@=TpVB6tB-i6Rqv1vs6iYonAsg~y`F)bW2g==gDKgKW>+ zhsDi1=3Sl2qC&_<+UGo4kwU@t_Wk^Gu4Y8}l6 z`6vmE^m}dq^^Eb}1MxbJm0UZky4G5)*V#D$dPiSQ>sqz;5;Q;~CsR*Cot<4DxU?or z&Bo#8v~j-wC2n9A)=mXat3ar)fKUa?r9BV;X-+7$kLzqw+Yb$1^qH_()CS|4>n7qJ zpqK9On7yR*MvP1SdWZXm532)e zb-RjDdR7q)ebhk7y!W%pwg=g=rH|s76eG~uTx{M_-)q5E?O1p}0%;NGuvr!RWOyHlpau4U6{{h$Wqqwih0?A>Vl1mVxXv#4eKf@;lY zQoQMoTN~uv%!izjGWs%!iqrt<3BhS(0==&a7gX*NcxkK#oS_F|q|41ZJ{OgbFi-LY zgujf33!a^`D-IYXC7W=P3sMUEVJu{K!}(P*T%yZCWWuvqrbg`d%4_jx-a5N$G(Sc@0WG^(%So zE8)e>A@;}D%zWotM-e5>2#xVx*PG#<6j_f?G76N{y?NiN_@By)sHsoT0Wqe&3vQ_O zZFb-##T6+XluhSO#2G<*JL{YLfJzHOLDu6Gt!rJqRvvI}dR=I%KWh%7UmsSpz^u(yOPuVlJn5Y{)3>|UkmM^5z0IHucT{b&% z=*#Ct5!sH)mabI#-`@7l+ljnx#Q6w`4lIwGqg?vZ$$k`4=tWpUQha3eg$NjhmJk7p zPN0MX0MwG%^*!o|K9-jJGXf!go&*4zYQn=cbxd_m7wQ6aVA#vHoxcK28_6)i=+Il9 gkY~hG6I^zE=mrNY>T6ThF?K%;;c*0PEhgmZ|5t2gr2qf` literal 0 HcmV?d00001 diff --git a/public/icons/safari-pinned-tab.svg b/public/icons/safari-pinned-tab.svg new file mode 100644 index 0000000..7a94829 --- /dev/null +++ b/public/icons/safari-pinned-tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/site.webmanifest b/public/icons/site.webmanifest new file mode 100644 index 0000000..9a41863 --- /dev/null +++ b/public/icons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Dunestash", + "short_name": "Dunestash", + "icons": [ + { + "src": "/icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..ae6a035 --- /dev/null +++ b/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + Dune Drive + + + +

+ + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..ed35c17 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,13 @@ +{ + "short_name": "Dunestash", + "name": "Dunestash", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..764a057 --- /dev/null +++ b/src/App.js @@ -0,0 +1,31 @@ +//Module Imports +import React from "react"; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +//Local Imports +import Stash from "./Stash"; +//Constants +const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiYmVjYWNjNjdhNmRjLTBlN2QtMDBlNi1jYmVhLWVhZGNlYmUxIiwiaWF0IjoxNjI3MTcxMDU1LCJleHAiOjE2Mjk3NjMwNTV9.zqiHrYnJlB7ozwjMnpgVUsBAt9vfLHLICFgWB0MguLA" +localStorage.setItem("authToken", token); +class App extends React.Component { + render() { + return ( + <> + + + + ); + } +} + +export default App; diff --git a/src/Stash.jsx b/src/Stash.jsx new file mode 100644 index 0000000..cde6c40 --- /dev/null +++ b/src/Stash.jsx @@ -0,0 +1,139 @@ +//Module Imports +import React from "react"; +import axios from "axios"; +import { toast } from "react-toastify"; +//Local Imports +import Stashbar from "./stash/Stashbar"; +import StashUpload from "./stash/StashUpload"; +import StashContextMenu from "./stash/StashContextMenu"; +import { serverUrls, serverAddress } from "./stash/api.json"; +import "./stash/scss/Stash.scss"; +//Constants +const filesUrl = `${serverAddress}/${serverUrls.GET.filesUrl}`; +//Class +function getConfig() { + var authToken = localStorage.getItem("authToken"); + console.log({ headers: { authorization: `Bearer ${authToken}` } }); + return { + headers: { authorization: `Bearer ${authToken}`, withCredentials: true }, + }; +} +function buildFilebox(file, index) { + return { + file, + selected: false, + filtered: true, + position: index, + }; +} +class Stash extends React.Component { + constructor(props) { + super(props); + this.state = { + fileBoxes: {}, + contextMenu: null, + }; + } + + componentDidMount() { + axios + .get(filesUrl, getConfig()) + .then((res) => { + if (res.status === 401) { + console.log("Would redirect to login"); + return; + } + if (res.data === undefined || res.data.length === undefined) { + toast.error("Error Loading Files"); + return; + } + var fileBoxes = {}; + res.data.forEach((file, index) => { + fileBoxes[file.fileUuid] = buildFilebox(file, index); + }); + this.setState({ fileBoxes }); + }) + .catch((error) => { + if (error.response.status === 401) + console.log("Would redirect to login"); + else console.error(error); + }); + } + + fileBoxesChanged(fileBoxes) { + this.setState({ fileBoxes }); + } + + getSelectedBoxes() { + var selectedBoxes = []; + for (var f in this.state.fileBoxes) { + if (!this.state.fileBoxes[f].filtered) continue; + if (!this.state.fileBoxes[f].selected) continue; + selectedBoxes.push(f); + } + return selectedBoxes; + } + + addFilebox(file) { + var fileBoxes = this.state.fileBoxes; + fileBoxes[file.fileUuid] = buildFilebox( + file, + Object.keys(fileBoxes).length + ); + this.setState({ fileBoxes }); + } + + removeDriveContextMenu() { + if (this.state.contextMenu !== null) this.setState({ contextMenu: null }); + } + + /*Options Menu Functions*/ + contextMenu(e) { + this.removeDriveContextMenu(); + if (e.ctrlKey || e.shiftKey) return; + e.preventDefault(); + e.stopPropagation(); + this.setState({ + contextMenu: { + x: e.clientX, + y: e.clientY, + }, + }); + } + render() { + return ( +
+ + {this.state.contextMenu && ( + + )} + +
+ +
+
+ ); + } +} + +export default Stash; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b1ef1c0 --- /dev/null +++ b/src/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./App"; + +ReactDOM.render( + + + , + document.getElementById("root") +); diff --git a/src/setupProxy.js b/src/setupProxy.js new file mode 100644 index 0000000..002a45f --- /dev/null +++ b/src/setupProxy.js @@ -0,0 +1,12 @@ +const { createProxyMiddleware } = require("http-proxy-middleware"); +const { serverAddress } = require("./stash/api.json"); +module.exports = (app) => { + app.use( + "/api", + createProxyMiddleware({ + target: serverAddress, + changeOrigin: true, + logLevel: "silent", + }) + ); +}; diff --git a/src/stash/FileBox.jsx b/src/stash/FileBox.jsx new file mode 100644 index 0000000..7ccfe80 --- /dev/null +++ b/src/stash/FileBox.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faUsers, + faEye, + faShareSquare, +} from "@fortawesome/free-solid-svg-icons"; + +import PropTypes from "prop-types"; +import "./scss/stash/FileBox.scss"; +class FileBox extends React.Component { + readableDate() { + let d = new Date(parseInt(this.props.file.date)); + if (isNaN(d.getMonth())) return ""; + return `${ + d.getMonth() + 1 + }/${d.getDate()}/${d.getFullYear()} ${d.getHours()}:${d.getMinutes()}`; + } + selectBox(e) { + e.stopPropagation(); + e.preventDefault(); + this.props.onSelection(e, this.props.boxUuid); + this.props.removeDriveContextMenu(); + } + + render() { + return ( +
this.props.contextSelect(this.props.boxUuid)} + onKeyDown={this.props.onBoxKeyPress} + > +
+
+ {this.props.file.name} +
+
+ {this.readableDate()} + {this.props.file.public && ( + + {this.props.file.public && } + + )} +
+
+
+ ); + } +} +FileBox.propTypes = { + file: PropTypes.object, +}; + +export default FileBox; diff --git a/src/stash/FileDisplay.jsx b/src/stash/FileDisplay.jsx new file mode 100644 index 0000000..485e8b2 --- /dev/null +++ b/src/stash/FileDisplay.jsx @@ -0,0 +1,125 @@ +//Module Imports +import React from "react"; +import PropTypes from "prop-types"; +//Local Imports +import FileBox from "./FileBox"; +import "./scss/stash/FileDisplay.scss"; +class FileDisplay extends React.Component { + constructor(props) { + super(props); + this.state = { + firstSelectionBoxUuid: null, + }; + } + + displayClick(e) { + e.preventDefault(); + e.stopPropagation(); + this.deselectAll(); + this.props.removeDriveContextMenu(); + } + + fileBoxKeysByPosition() { + return Object.keys(this.props.fileBoxes).sort((a, b) => { + return a.position - b.position; + }); + } + + onSelection(e, boxUuid) { + var fileBoxes = this.props.fileBoxes; + var newBoxes; + const firstSelection = this.state.firstSelectionBoxUuid; + if (e.ctrlKey && firstSelection !== null) + newBoxes = this.segmentSelection(fileBoxes, boxUuid); + else if (e.shiftKey && firstSelection !== null) + newBoxes = this.multiSelection(fileBoxes, boxUuid); + else newBoxes = this.singleSelection(fileBoxes, boxUuid); + this.props.fileBoxesChanged(newBoxes); + } + + singleSelection(fileBoxes, boxUuid) { + this.deselectAll(); + this.setState({ firstSelectionBoxUuid: boxUuid }); + fileBoxes[boxUuid].selected = true; + return fileBoxes; + } + + segmentSelection(fileBoxes, boxUuid) { + fileBoxes[boxUuid].selected = !fileBoxes[boxUuid].selected; + return fileBoxes; + } + + multiSelection(fileBoxes, boxUuid) { + this.deselectAll(); + var firstIndex = fileBoxes[this.state.firstSelectionBoxUuid].position; + var endIndex = fileBoxes[boxUuid].position; + var boxKeys = this.fileBoxKeysByPosition(); + if (endIndex < firstIndex) { + let tmp = endIndex; + endIndex = firstIndex; + firstIndex = tmp; + } + //Send selection 1 more for the slice + boxKeys.slice(firstIndex, endIndex + 1).forEach((boxId, i) => { + if (!fileBoxes[boxId].filtered) return; + fileBoxes[boxId].selected = true; + }); + return fileBoxes; + } + + contextSelect(boxUuid) { + if (this.props.getSelectedBoxes().length > 1) return; + this.onSelection({}, boxUuid); + } + deselectAll() { + var fileBoxes = this.props.fileBoxes; + for (var f in fileBoxes) fileBoxes[f].selected = false; + this.props.fileBoxesChanged(fileBoxes); + } + selectAll() { + var fileBoxes = this.props.fileBoxes; + for (var f in fileBoxes) + if (fileBoxes[f].filtered) fileBoxes[f].selected = true; + this.props.fileBoxesChanged(fileBoxes); + } + onBoxKeyPress(e) { + if (e.keyCode !== 65 || !e.ctrlKey) return; + e.preventDefault(); + e.stopPropagation(); + this.selectAll(); + } + render() { + return ( +
+
+ {this.fileBoxKeysByPosition().map((boxUuid, index) => ( + + {this.props.fileBoxes[boxUuid].filtered && ( + + )} + + ))} +
+
+
+ ); + } +} + +FileDisplay.propTypes = { + files: PropTypes.object, +}; +export default FileDisplay; diff --git a/src/stash/StashContextMenu.jsx b/src/stash/StashContextMenu.jsx new file mode 100644 index 0000000..9f6e017 --- /dev/null +++ b/src/stash/StashContextMenu.jsx @@ -0,0 +1,150 @@ +import React from "react"; +import axios from "axios"; +import { toast } from "react-toastify"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faInfoCircle, + faFileDownload, + faTrash, + faEye, + faShareSquare, +} from "@fortawesome/free-solid-svg-icons"; +//Local Imports +import "./scss/stash/StashContextMenu.scss"; +import { serverUrls, serverAddress } from "./api.json"; +//Constants +const downloadUrl = `${serverAddress}/${serverUrls.POST.downloadUrl}`; +const deleteUrl = `${serverAddress}/${serverUrls.POST.deleteUrl}`; +const publicUrl = `${serverAddress}/${serverUrls.POST.publicUrl}`; +const rawUrl = `${serverAddress}/${serverUrls.GET.rawUrl}`; + +function getConfig() { + var authToken = localStorage.getItem("authToken"); + return { headers: { authorization: `Bearer ${authToken}` } }; +} + +export default class StashContextMenu extends React.Component { + infoView() { + var selectedCount = this.props.getSelectedBoxes().length; + if (selectedCount === 1) return "View"; + if (selectedCount > 1) return `${selectedCount} files selected`; + return "No Files Selected"; + } + + infoClick(e) { + const selectedBoxes = this.props.getSelectedBoxes(); + if (selectedBoxes.length !== 1) return; + const file = selectedBoxes[0]; + let win = window.open(`${rawUrl}?target=${file}`); + if (!win || win.closed || typeof win.closed == "undefined") { + window.location = `${rawUrl}?target=${file}`; + } + } + downloadClick() { + const selectedBoxes = this.props.getSelectedBoxes(); + //ZIPS ARE NOT SUPPORTED YET + if (selectedBoxes.length > 1) + return toast.error("Downloading multiple files is not yet supported!"); + else + return this.handleDownload(`${downloadUrl}?target=${selectedBoxes[0]}`); + } + deleteClick() { + const selectedBoxes = this.props.getSelectedBoxes(); + axios + .post(deleteUrl, selectedBoxes, getConfig()) + .then((res) => this.handleDelete(res, selectedBoxes)) + .catch((e) => this.handleDelete(e.response, selectedBoxes)); + } + publicClick() { + const selectedBoxes = this.props.getSelectedBoxes(); + axios + .post(publicUrl, selectedBoxes, getConfig()) + .then((res) => this.handlePublic(res, selectedBoxes)) + .catch((e) => this.handlePublic(e.response, selectedBoxes)); + } + + handlePublic(res, selectedBoxes) { + const failedFiles = res.data || []; + if (res.status !== 200) + toast.error("There was an issue making some files public!"); + let fileBoxes = this.props.fileBoxes; + selectedBoxes.forEach((selectedBoxId) => { + if (!failedFiles.includes(selectedBoxId)) { + fileBoxes[selectedBoxId].file.public = !fileBoxes[selectedBoxId].file + .public; + } else { + fileBoxes[selectedBoxId].selected = true; + } + }); + this.props.fileBoxesChanged(fileBoxes); + } + handleDownload(url) { + let win = window.open(url); + if (!win || win.closed || typeof win.closed == "undefined") + window.location = url; + } + /** + * Handles the response from the deleteClick() + * @param {String} response server response + * @param {Array} selectedBoxes Selected Boxes object list + * + */ + handleDelete(res, selectedBoxes) { + const failedFiles = res.data || []; + console.log(res); + if (res.status !== 200) toast.error("Error Deleting Some Files"); + let fileBoxes = this.props.fileBoxes; + selectedBoxes.forEach((selectedBoxId) => { + if (!failedFiles.includes(selectedBoxId)) { + delete fileBoxes[selectedBoxId]; + } else { + fileBoxes[selectedBoxId].selected = true; + } + }); + this.props.fileBoxesChanged(fileBoxes); + } + shareClick() {} + + styleCalc() { + const estimatedHeight = 180; //px + const esetimatedWidth = 290; //px + const bodyWidth = document.body.offsetWidth; + const bodyHeight = document.documentElement.offsetHeight; + let top = this.props.y; + let left = this.props.x; + const overFlowX = left + esetimatedWidth > bodyWidth; + const overFlowY = top + estimatedHeight > bodyHeight; + if (overFlowX) left = left - esetimatedWidth; + if (overFlowY) top = top - estimatedHeight; + return { top: `${top}px`, left: `${left}px` }; + } + + render() { + return ( +
+
    +
  • + + {this.infoView()} +
  • +
  • + + Download +
  • +
  • + + Delete +
  • +
  • + + Toggle Public +
  • +
  • + + Share +
  • +
+
+ ); + } +} diff --git a/src/stash/StashDropzone.jsx b/src/stash/StashDropzone.jsx new file mode 100644 index 0000000..3c98ffe --- /dev/null +++ b/src/stash/StashDropzone.jsx @@ -0,0 +1,31 @@ +import React from "react"; +import Dropzone from "react-dropzone"; +import FileDisplay from "./FileDisplay"; + +import "./scss/stash/StashDropzone.scss"; +class StashDropzone extends React.Component { + render() { + return ( + this.props.addUpload(acceptedFiles)}> + {({ getRootProps, getInputProps }) => ( +
+ + +
+ )} +
+ ); + } +} + +export default StashDropzone; diff --git a/src/stash/StashUpload.jsx b/src/stash/StashUpload.jsx new file mode 100644 index 0000000..fcebfd5 --- /dev/null +++ b/src/stash/StashUpload.jsx @@ -0,0 +1,198 @@ +//Module Imports +import React from "react"; +import axios, { CancelToken } from "axios"; +import { toast } from "react-toastify"; +//Local Imports +import StashDropzone from "./StashDropzone"; +import StashUploadDialog from "./uploader/StashUploadDialog"; +//Constants +import { serverUrls, serverFields, serverAddress } from "./api.json"; +const uploadUrl = `${serverAddress}/${serverUrls.POST.uploadUrl}`; +const uploadField = serverFields.uploadField; +const cancelMessage = "User Canceled"; +const successClearTime = 200; + +function buildUpload(file, uploadUuid, onProgress) { + var authToken = localStorage.getItem("authToken"); + var upload = { + file, + uploadUuid, + progress: 0, + status: null, + started: false, + }; + const cancelToken = new CancelToken((cancel) => { + upload.cancelUpload = () => cancel(cancelMessage); + }); + upload.config = { + headers: { authorization: `Bearer ${authToken}`, filesize: file.size }, + onUploadProgress: (e) => onProgress(e, uploadUuid), + cancelToken, + }; + return upload; +} + +export default class StashUpload extends React.Component { + constructor(props) { + super(props); + this.state = { + uploads: {}, + errorCount: 0, + fadeOnClear: false, + }; + } + + uploadProgress(e, uploadUuid) { + const totalLength = e.lengthComputable + ? e.total + : e.target.getResponseHeader("content-length") || + e.target.getResponseHeader("x-decompressed-content-length"); + if (totalLength !== null) { + const loaded = Math.round((e.loaded * 100) / totalLength); + var uploads = this.state.uploads; + if (loaded === 100 || uploads[uploadUuid] == null) return; + uploads[uploadUuid].progress = loaded; + this.setState({ uploads }); + } + } + + startAllUploads() { + var uploads = this.state.uploads; + for (var u in uploads) { + if (uploads[u].started) continue; + uploads[u].started = true; + this.startUpload(uploads[u]); + } + this.setState({ uploads }); + } + + startUpload(upload) { + const data = new FormData(); + data.append(uploadField, upload.file); + axios + .post(uploadUrl, data, upload.config) + .then((res) => this.uploadDone(res, upload)) + .catch((e) => this.uploadError(e, upload)); + } + + uploadDone(res, upload) { + if (res.status !== 200) return this.showError(upload.uploadUuid); + var uploads = this.state.uploads; + delete uploads[upload.uploadUuid].cancelToken; + this.setState({ uploads }); + this.showSuccess(upload.uploadUuid); + this.props.addFilebox(res.data); + } + + uploadError(e, upload) { + if (e.message === cancelMessage) console.log("Upload Canceled"); + else if (e.response == null) toast.error("Unknown Error Occured!"); + else if (e.response.status === 401) toast.error("Not Logged In!"); + else if (e.response.status === 500) toast.error("Drive Full!"); + this.showError(upload.uploadUuid); + } + + showError(uploadUuid) { + var uploads = this.state.uploads; + uploads[uploadUuid].status = "Error"; + this.setState({ uploads, errorCount: this.state.errorCount + 1 }); + } + + showSuccess(uploadUuid) { + var uploads = this.state.uploads; + uploads[uploadUuid].status = "Success"; + this.setState({ uploads }); + setTimeout(() => this.removeUpload(uploadUuid), successClearTime); + } + + addUpload(files) { + var uploads = this.state.uploads; + const uploadTime = Date.now(); + var uploadUuid; + files.forEach((file, i) => { + uploadUuid = `${uploadTime}-${i}`; + uploads[uploadUuid] = buildUpload( + file, + uploadUuid, + this.uploadProgress.bind(this) + ); + }); + this.setState({ uploads }, this.startAllUploads); + } + + retryUpload(uploadUuid) { + if (!uploadUuid) return; + var uploads = this.state.uploads; + var errorCount = this.state.errorCount; + const file = uploads[uploadUuid].file; + //Remove error count if the upload errored because we're now removing it + if (uploads[uploadUuid].status === "Error") errorCount--; + //Update and remove the upload + this.removeUpload(uploadUuid); + this.addUpload([file]); + this.setState({ errorCount }); + } + clearUpload(uploadUuid) { + var uploads = this.state.uploads; + if (uploads[uploadUuid].status !== null) this.removeUpload(uploadUuid); + else uploads[uploadUuid].cancelUpload(); + } + clearAll() { + var uploads = this.state.uploads; + var onlyPending = true; + var u; + for (u in uploads) { + if (uploads[u].status !== null) { + delete uploads[u]; + onlyPending = false; + } + } + //If onlypending cancel all uploads currently remaining + if (onlyPending) for (u in uploads) uploads[u].cancelUpload(); + this.setState({ uploads, errorCount: 0 }); + } + retryAll() { + var uploads = this.state.uploads; + //Splicing so itterate backwards + //(retryUpload is what calls the splice via removeUpload) + for (var u in uploads) { + if (uploads[u].status === "Error") this.retryUpload(u); + } + } + removeUpload(uploadUuid) { + if (!uploadUuid) return; + //Remove error count if the upload errored because we're now removing it + var errorCount = this.state.errorCount; + var fadeOnClear = this.state.fadeOnClear; + var uploads = this.state.uploads; + if (uploads[uploadUuid].status === "Error") errorCount--; + //Update and remove the upload + delete uploads[uploadUuid]; + if (Object.keys(uploads).length === 0) fadeOnClear = true; + this.setState({ uploads, errorCount, fadeOnClear }); + } + + render() { + return ( + <> + + + + ); + } +} diff --git a/src/stash/Stashbar.jsx b/src/stash/Stashbar.jsx new file mode 100644 index 0000000..f43dce1 --- /dev/null +++ b/src/stash/Stashbar.jsx @@ -0,0 +1,39 @@ +//Module Imports +import React from "react"; +//Local Imports +import StashbarMenu from "./stashbar/StashbarMenu.jsx"; +import StashbarSearch from "./stashbar/StashbarSearch.jsx"; +class Stashbar extends React.Component { + constructor(props) { + super(props); + this.state = { + searchMode: false, + }; + } + + setSearchMode(value) { + this.setState({ searchMode: value }); + } + + whichBar() { + if (this.state.searchMode) + return ( + + ); + else return ; + } + + render() { + return ( +
+
{this.whichBar()}
+
+ ); + } +} + +export default Stashbar; diff --git a/src/stash/api.json b/src/stash/api.json new file mode 100644 index 0000000..29c3d4e --- /dev/null +++ b/src/stash/api.json @@ -0,0 +1,20 @@ +{ + "serverAddress": "http://nubian.dunestorm.net:52001", + "serverUrls": { + "POST": { + "uploadUrl": "api/stash/upload", + "downloadUrl": "api/stash/download", + "deleteUrl": "api/stash/delete", + "publicUrl": "api/stash/public" + }, + "GET": { + "filesUrl": "api/stash/files", + "rawUrl": "api/stash/raw", + "avatar": "api/user/avatar" + } + }, + "serverFields": { + "uploadField": "user-selected-file" + }, + "constants": { "jwtHeader": "authorization" } +} diff --git a/src/stash/scss/Stash.scss b/src/stash/scss/Stash.scss new file mode 100644 index 0000000..b7be9f9 --- /dev/null +++ b/src/stash/scss/Stash.scss @@ -0,0 +1,17 @@ +@import "./global"; + +.dunestash { + position: fixed; + width: 100%; + height: 100%; +} + +.stash { + width: 100%; + height: calc(100% - #{$stashbarHeight}); + position: relative; + margin: auto; + display: flex; + overflow-x: hidden; + overflow-y: scroll; +} diff --git a/src/stash/scss/_global.scss b/src/stash/scss/_global.scss new file mode 100644 index 0000000..3c3bc04 --- /dev/null +++ b/src/stash/scss/_global.scss @@ -0,0 +1,35 @@ +@import "global/colors", "global/fonts", "global/measurements", + "global/animations"; +html, +body { + margin-left: auto; + margin-right: auto; + text-align: center; + margin-top: 0; + color: $foreground; + background: #eee; + height: 100%; + width: 100%; +} +*::-webkit-scrollbar { + width: 5px; + height: 5px; +} +*::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: $defaultBorderRadius; +} +*::-webkit-scrollbar-thumb { + border-radius: $defaultBorderRadius; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); + background-color: lighten($sectionMenuOptions, 40%); +} +*::-webkit-scrollbar-thumb:hover { + background-color: lighten($sectionMenuOptions, 50%); +} +input:focus, +select:focus, +textarea:focus, +button:focus { + outline: none; +} diff --git a/src/stash/scss/global/_animations.scss b/src/stash/scss/global/_animations.scss new file mode 100644 index 0000000..ee02741 --- /dev/null +++ b/src/stash/scss/global/_animations.scss @@ -0,0 +1,121 @@ +/*Shimmer for links*/ +@-webkit-keyframes glowingShimmer { + 0% { + background-position: -4rem top; + /*50px*/ + } + 70% { + background-position: 12.5rem top; + /*200px*/ + } + 100% { + background-position: 12.5rem top; + /*200px*/ + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes toastify-notification-fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + visibility: hidden; + } +} + +@keyframes toast-notification-fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 0.9; + visibility: visible; + } +} + +@mixin animation-breathe($b1, $b2, $f1, $f2) { + @keyframes breathe { + 0% { + background-color: $b1; + color: $f1; + } + 50% { + background-color: $b2; + color: $f2; + } + 100% { + background-color: $b1; + color: $f1; + } + } +} +@mixin animation-bounce($f1, $f2) { + @keyframes bounce { + 0% { + color: $f1; + } + + 50% { + color: $f2; + } + 100% { + color: $f1; + } + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes file-watcher-fade { + 0% { + opacity: 1; + } + 40% { + opacity: 0; + visibility: hidden; + } + 80% { + background: $successColor; + } + 100% { + opacity: 0; + visibility: hidden; + } +} + +@keyframes upload-dialog-expand { + 0% { + height: 50px; + } + + 100% { + height: 100%; + } +} + +@keyframes upload-dialog-minimize { + 0% { + height: 50%; + } + + 100% { + height: 50px; + } +} diff --git a/src/stash/scss/global/_colors.scss b/src/stash/scss/global/_colors.scss new file mode 100644 index 0000000..6c2d1c5 --- /dev/null +++ b/src/stash/scss/global/_colors.scss @@ -0,0 +1,16 @@ +/*Status Colors*/ +$successColor: limegreen; +$errorColor: #e20006; +$informationColor: #0095ff; +/*Background & Foreground colors*/ +$background: rgba(20, 20, 20, 1); +$backgroundGradientStart: $background; +$backgroundGradientEnd: #848486; +$foreground: white; +$sectionForeground: $foreground; +/*Visual "components" (divs, etc)*/ +$sectionBackground: rgba($background, 0.95); +$sectionMenu: darken(rgba($background, 0.8), 10%); +$sectionMenuOptions: rgba(lighten($sectionMenu, 16%), 98%); +$borderColor: #13120e; +/*Unique Components*/ diff --git a/src/stash/scss/global/_fonts.scss b/src/stash/scss/global/_fonts.scss new file mode 100644 index 0000000..6a4379e --- /dev/null +++ b/src/stash/scss/global/_fonts.scss @@ -0,0 +1,48 @@ + +h1 { + font-family: "Dejavu Sans"; + font-size: 33px; + font-style: normal; + font-variant: normal; + font-weight: 700; + } + +h2, label { + font-family: "Dejavu Sans"; + font-size: 28px; + font-style: normal; + font-variant: normal; + font-weight: 700; + } + +h3 { + font-family: "Dejavu Sans"; + font-size: 24px; + font-style: normal; + font-variant: normal; + font-weight: 700; +} + +p, a { + font-family: "Dejavu Sans"; + font-size: 18px; + font-style: normal; + font-variant: normal; + font-weight: 400; + } + +blockquote { + font-family: "Dejavu Sans"; + font-size: 21px; + font-style: normal; + font-variant: normal; + font-weight: 400; + line-height: 30px; } + +pre { + font-family: "Dejavu Sans"; + font-size: 13px; + font-style: normal; + font-variant: normal; + font-weight: 400; + } diff --git a/src/stash/scss/global/_measurements.scss b/src/stash/scss/global/_measurements.scss new file mode 100644 index 0000000..de682df --- /dev/null +++ b/src/stash/scss/global/_measurements.scss @@ -0,0 +1,13 @@ +/*Spacing*/ +$contentTopGap: 0px; +/*Size*/ +$pageMinWidth: 300px; +$pageMaxWidth: 780px; +$defaultBoxPadding: 15px; +/*Components*/ +$stashbarHeight: 4rem; +$fileFiltersHeight: 1.5rem; +$toastMinWidth: calc(#{$pageMinWidth} - 100px); +$toastMaxWidth: calc(50% - 20px); +/*Misc*/ +$defaultBorderRadius: 2px; diff --git a/src/stash/scss/stash/FileBox.scss b/src/stash/scss/stash/FileBox.scss new file mode 100644 index 0000000..7f45b27 --- /dev/null +++ b/src/stash/scss/stash/FileBox.scss @@ -0,0 +1,61 @@ +@import "../global"; +.filebox { + margin: 0.4rem 0.4rem; + display: flex; + max-height: 4rem; + height: 100%; + max-width: 25em; + padding: 0.4rem 0; + width: 100%; + align-self: flex-start; + position: relative; + background: $sectionMenuOptions; + box-shadow: 1px 3px 2px $sectionMenuOptions; +} + +.filebox:hover { + cursor: pointer; + background: lighten($sectionMenuOptions, 3%); +} + +.filebox.selected { + background: $informationColor; + box-shadow: 1px 3px 2px darken($informationColor, 20%); +} + +.filebox.selected:hover { + background: lighten($informationColor, 3%); +} + +.file { + color: white; + justify-content: center; + text-align: center; + max-height: 4rem; + height: 100%; + max-width: 25em; + padding: 0.4rem 0; + width: 100%; +} + +.file-details { + display: flex; + overflow: hidden; + white-space: nowrap; + width: 100%; + height: 50%; + margin: auto; + justify-content: center; +} + +.file-info-details { + overflow: hidden; + white-space: nowrap; + width: 100%; + padding: 0.125rem 0; + max-width: 39ch; +} + +.file-subinfo-details { + padding: 0 5px; +} diff --git a/src/stash/scss/stash/FileDisplay.scss b/src/stash/scss/stash/FileDisplay.scss new file mode 100644 index 0000000..8a4c850 --- /dev/null +++ b/src/stash/scss/stash/FileDisplay.scss @@ -0,0 +1,23 @@ +@import "../global"; +.file-display { + + height: 100%; +} + +.box-display{ + display: flex; + flex-flow: row; + flex-wrap: wrap; + flex-grow: 1; + align-items: flex-start; + justify-content: center; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; +} + +.file-display-spacer { + width: 100%; + height: 2rem; +} diff --git a/src/stash/scss/stash/Searchbar.scss b/src/stash/scss/stash/Searchbar.scss new file mode 100644 index 0000000..ff58d6b --- /dev/null +++ b/src/stash/scss/stash/Searchbar.scss @@ -0,0 +1,120 @@ +@import "../global"; +$searchIndent: 0.125rem; + +.file-searchbar { + width: 100%; + display: flex; + flex-wrap: wrap; + background: darken(#eee, 5%); + box-shadow: 0.25px 1px 0.5px darken(#eee, 8%); +} + +.file-searchbox { + margin: 0 auto; + display: inline-flex; + background: inherit; + color: $sectionMenuOptions; + width: 100%; + border-bottom: 1px darken(#eee, 22%) solid; +} +.file-searchbar svg { + margin: auto 0; + height: 100%; + padding: 0 0.15rem 0 0.5rem; +} + +#file-search { + width: 100%; + font-size: 1.125rem; + background: inherit; + text-indent: $searchIndent; + border: none; +} +#file-search::placeholder { + color: $sectionMenuOptions; +} +.file-searchbox-action { + padding: 0 0.325rem; + height: inherit; + display: block; +} + +.file-searchbox-action svg { + margin: auto; + height: 100%; +} + +#file-searchbox-hashtag { + padding-right: calc(0.325rem + #{$searchIndent} * 4); +} + +.file-searchbox-action:hover { + cursor: pointer; +} + +.file-searchbar-extensions { + width: 100%; +} + +.file-filters { + background: darken(#eee, 10%); + width: 100%; + display: inline-flex; + flex-grow: nowrap; + justify-content: flex-start; + align-items: flex-start; + overflow-x: auto; + border-bottom: 1px darken(#eee, 12%) solid; +} +.file-filter { + justify-content: center; + text-align: center; + display: inline-flex; + margin: auto 0.5rem; + padding: 2px; + width: 5rem; + background: $informationColor; + box-shadow: 0.375px 1.125px 0.75px darken($informationColor, 20%); +} + +.file-filter svg { + margin: auto 0; + width: 0.8rem; + height: 0.8rem; +} +.file-filter svg:hover { + cursor: pointer; +} + +.query-filters { + width: 100%; + background: darken(#eee, 10%); + border-bottom: 1px darken(#eee, 20%) solid; +} + +.query-filter-list { + width: 100%; + height: 1.5rem; + display: inline-flex; + flex-grow: nowrap; + justify-content: flex-start; + align-items: flex-start; + overflow-x: auto; + overflow-y: hidden; +} + +.query-filter { + display: inline-flex; + align-items: center; + color: $sectionMenuOptions; + width: 100%; + height: 100%; + justify-content: center; + text-align: center; + display: inline-flex; + border: 1px darken(#eee, 22%) solid; +} + +.query-filter-list span:first-child { + border-left: 0px; +} diff --git a/src/stash/scss/stash/StashContextMenu.scss b/src/stash/scss/stash/StashContextMenu.scss new file mode 100644 index 0000000..a03b66d --- /dev/null +++ b/src/stash/scss/stash/StashContextMenu.scss @@ -0,0 +1,48 @@ +@import "../global"; +//Context Menu +.drive-context-menu { + position: fixed; + display: flex; + background: lighten($sectionMenuOptions, 10%); + box-shadow: 1px 3px 2px lighten ($sectionMenuOptions, 5%); + color: $foreground; + z-index: 300; + width: 100%; + max-width: inherit; + font-size: 1rem; + max-width: 18em; + min-height: 11em; +} + +.drive-context-menu ul { + list-style-type: none; + padding: 0; + width: 100%; +} + +.drive-context-menu li:hover { + opacity: 0.85; + cursor: pointer; +} + +.drive-context-menu li { + width: inherit; + text-align: left; + padding: 0 0.5rem; +} + +.drive-context-menu svg { + min-width: 1rem; + padding: 0 0.5rem; + height: 100%; +} + +.drive-context-menu li:not(:last-child) { + margin-bottom: 0.85rem; +} + +.drive-context-menu a { + color: $foreground; + text-decoration: none; + font-size: inherit; +} diff --git a/src/stash/scss/stash/StashDropzone.scss b/src/stash/scss/stash/StashDropzone.scss new file mode 100644 index 0000000..3cd8971 --- /dev/null +++ b/src/stash/scss/stash/StashDropzone.scss @@ -0,0 +1,6 @@ +@import "../global"; +.stash-dropzone { + position: absolute; + height: 100%; + width: inherit; +} diff --git a/src/stash/scss/stash/StashUploadDialog.scss b/src/stash/scss/stash/StashUploadDialog.scss new file mode 100644 index 0000000..ba49789 --- /dev/null +++ b/src/stash/scss/stash/StashUploadDialog.scss @@ -0,0 +1,161 @@ +@import "../global"; +//Variables +$watcherIndicatorSize: 18px; +$watcherActionSize: 14px; +$uploadBoxesSize: 50px; +$actionButtonSize: 25px; +/*File Upload Dialog Styling*/ +.fud { + position: fixed; + bottom: 0; + left: 0; + background-color: $sectionMenuOptions; + width: 100%; + height: 100%; + max-width: 540px; + max-height: 250px; + overflow: hidden; + -webkit-transform: translate3d(0, 0, 0); + z-index: 100; + display: block; + visibility: hidden; +} + +.fud-fade { + visibility: visible; + animation: file-watcher-fade 2s forwards; +} + +.fud-maximized { + visibility: visible; + animation: upload-dialog-expand 1.2s forwards; + animation-timing-function: ease-out; +} + +.fud-minimized { + visibility: visible; + animation: upload-dialog-minimize 0.4s forwards; + animation-timing-function: ease-out; +} + +#fud-header { + display: inline-flex; + height: $uploadBoxesSize; + font-size: $watcherIndicatorSize; + background-color: darken($sectionMenuOptions, 10%); + width: 100%; +} + +.fud-header-title-wrapper { + margin: auto; + padding: 10px; + width: 100%; + overflow: hidden; + text-align: center; + padding-left: 10px; +} + +#fud-header-title { + margin-left: 40px; +} +#fud-header-title:hover { + opacity: 0.8; + cursor: pointer; +} + +#fud-minimize { + padding: 10px; +} + +#header-actions { + margin-right: 5px; +} + +.fud-actions { + //width: calc(#{$actionButtonSize} * 3); + display: inline-flex; + margin: auto; + align-items: center; + text-align: center; + height: $uploadBoxesSize; +} + +.fud-action { + min-width: $actionButtonSize; + padding: 0 5px; + + font-size: $watcherActionSize; +} + +.fud-action svg, +i { + margin: auto; +} + +.fud-actions span:hover { + opacity: 0.8; + cursor: pointer; +} + +#fud-header-status { + font-size: calc(#{$watcherIndicatorSize - 2px}); + min-width: $uploadBoxesSize; + min-height: $uploadBoxesSize; + max-width: $uploadBoxesSize; + max-height: $uploadBoxesSize; + display: flex; +} + +/*Error and status icon Start with errors displaying as none*/ +#fud-status-icon { + margin: auto; + font-size: 18px; +} + +.fud-error-wrapper { + display: none; + position: absolute; + top: 9px; + left: 32px; + font-size: 18px; +} +.fud-error-wrapper { + color: $errorColor; + max-width: 18px; + max-height: 18px; +} + +#fud-error-count { + font-size: 14px; + color: $foreground; + position: absolute; + top: 0; + width: 100%; +} + +.fud-status-error { + margin-top: 10px; +} + +#fud-queued-files { + max-height: 200px; + height: 100%; + height: 100%; + width: 100%; + overflow-y: scroll; +} +#fud-queued-files::-webkit-scrollbar { + width: 5px; +} +#fud-queued-files::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: $defaultBorderRadius; +} +#fud-queued-files::-webkit-scrollbar-thumb { + border-radius: $defaultBorderRadius; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); + background-color: lighten($sectionMenuOptions, 40%); +} +#fud-queued-files::-webkit-scrollbar-thumb:hover { + background-color: lighten($sectionMenuOptions, 50%); +} diff --git a/src/stash/scss/stash/StashUploadWatcher.scss b/src/stash/scss/stash/StashUploadWatcher.scss new file mode 100644 index 0000000..39fe3a4 --- /dev/null +++ b/src/stash/scss/stash/StashUploadWatcher.scss @@ -0,0 +1,79 @@ +@import "./StashUploadDialog.scss"; +/*File Watcher Styling*/ +.file-watcher { + height: 100%; + width: 100%; + display: inline-flex; + max-height: $uploadBoxesSize; + overflow: hidden; + word-wrap: break-word; + padding: none; +} +.file-watcher-progressbar { + min-width: $uploadBoxesSize; + min-height: $uploadBoxesSize; + max-width: $uploadBoxesSize; + max-height: $uploadBoxesSize; + display: flex; + max-height: $uploadBoxesSize; + background-color: lighten($sectionMenuOptions, 20%); +} +.file-watcher.active { + background-color: lighten($sectionMenuOptions, 10%); +} +.file-watcher.success { + animation: file-watcher-fade 2s forwards; +} +.file-watcher-progressbar-fill { + height: 100%; + width: 0%; + background: $successColor; + display: flex; + font-weight: bold; + //align-items: center; + transition: width 0.22s; +} +.file-watcher-progressbar-fill.error { + background: $errorColor; +} + +.file-watcher-progressbar-indicator { + min-width: $uploadBoxesSize; + min-height: $uploadBoxesSize; + max-width: $uploadBoxesSize; + max-height: $uploadBoxesSize; + color: $foreground; + display: flex; + font-size: $watcherIndicatorSize; +} + +.file-watcher-progressbar-indicator svg, +span { + margin: auto; +} + +.file-watcher-name { + margin: auto; + overflow: hidden; + word-wrap: break-word; + padding-left: 10px; + align-self: center; + width: 100%; +} +.file-watcher-action { + width: calc(#{$uploadBoxesSize} / 2); + height: $uploadBoxesSize; + display: flex; + font-weight: bold; + align-items: center; + padding: 0px 5px; + float: right; +} +.file-watcher-action svg { + margin: auto; + font-size: $watcherActionSize; +} +.file-watcher-action:hover { + opacity: 0.8; + cursor: pointer; +} diff --git a/src/stash/scss/stash/Stashbar.scss b/src/stash/scss/stash/Stashbar.scss new file mode 100644 index 0000000..afd6c2a --- /dev/null +++ b/src/stash/scss/stash/Stashbar.scss @@ -0,0 +1,34 @@ +@import "../global"; + +.stashbar { + width: 100%; + z-index: 200; +} +.stashbar-menu { + width: 100%; + height: $stashbarHeight; + display: inline-flex; + background-color: rgba($sectionMenu, 1); +} + +.stashbar-action { + font-size: 1.25rem; + width: 100%; +} + +.stashbar-action svg { + height: 100%; + width: 100%; +} + +.stashbar-action label { + font-size: inherit; +} +.stashbar-action label:hover { + cursor: pointer; +} + +.stashbar-action:hover { + opacity: 0.85; + cursor: pointer; +} diff --git a/src/stash/stashbar/StashbarMenu.jsx b/src/stash/stashbar/StashbarMenu.jsx new file mode 100644 index 0000000..3231840 --- /dev/null +++ b/src/stash/stashbar/StashbarMenu.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import { + faBars, + faCloudUploadAlt, + faSearch, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import "../scss/stash/Stashbar.scss"; + +class StashbarMenu extends React.Component { + render() { + return ( +
+ + + + + + + this.props.setSearchMode(true)} + > + + +
+ ); + } +} + +export default StashbarMenu; diff --git a/src/stash/stashbar/StashbarSearch.jsx b/src/stash/stashbar/StashbarSearch.jsx new file mode 100644 index 0000000..ce8fff6 --- /dev/null +++ b/src/stash/stashbar/StashbarSearch.jsx @@ -0,0 +1,174 @@ +import React from "react"; +import { + faTimes, + faArrowLeft, + faHashtag, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +//Local imports +import StashbarSearchTagDisplay from "./StashbarSearchTagDisplay"; +import "../scss/stash/Searchbar.scss"; +//Constants +const hashtagChar = "#"; +const searchFilters = ["Selected", "Public"]; +class StashbarSearch extends React.Component { + constructor(props) { + super(props); + this.state = { + tags: [], + query: "", + }; + } + + //Filtering Functions + + updateQuery(query) { + this.setState({ query }, this.updateFiltered); + } + + updateFiltered() { + var fileBoxes = this.markAllFiltered(); + const activeTags = this.state.tags; + const query = this.state.query.toLowerCase(); + for (var f in fileBoxes) { + //If file isn't selected + if (activeTags.includes(searchFilters[0]) && !fileBoxes[f].selected) + fileBoxes[f].filtered = false; + //If file isn't public + if (activeTags.includes(searchFilters[1]) && !fileBoxes[f].file.public) { + fileBoxes[f].filtered = false; + } + if (!fileBoxes[f].file.name.toLowerCase().includes(query)) + fileBoxes[f].filtered = false; + } + this.setState({ fileBoxes }); + } + + markAllFiltered() { + var fileBoxes = this.props.fileBoxes; + for (var f in fileBoxes) { + fileBoxes[f].filtered = true; + } + this.props.fileBoxesChanged(fileBoxes); + return fileBoxes; + } + + //Searchbar Functions + searchChanged(e) { + this.updateQuery(e.target.value); + } + + searchbarClose() { + this.markAllFiltered(); + this.props.setSearchMode(false); + } + //Tag Functions + hashtagClick() { + this.updateQuery(hashtagChar); + } + + queryTag(e) { + var query = this.state.query; + if (e.key !== "Enter" || query[0] !== hashtagChar) return; + const emptySpace = ""; + const space = " "; + query = query.substring(1).toLowerCase(); + const filters = this.getAvailableTags(); + var filter = null; + for (filter of filters) { + if (filter.toLowerCase().includes(query)) break; + } + if (filter !== null) this.addTag(filter); + var firstSpace = query.indexOf(space); + if (firstSpace === -1) query = emptySpace; + else query = query.substring(query.indexOf(space)); + this.updateQuery(query); + } + + removeTag(tag) { + var tags = this.state.tags; + tags.splice(tags.indexOf(tag), 1); + this.setState({ tags }, this.updateFiltered); + } + + addTag(tag) { + var tags = this.state.tags; + tags.push(tag); + this.setState({ tags }, this.updateFiltered); + } + + getAvailableTags() { + var availableFilters = []; + searchFilters.forEach((filter, i) => { + if (i === 0) return; //Ignore first filter 'Selected' triggered by selecting + if (this.state.tags.includes(filter)) return; + if ( + !filter + .toLowerCase() + .includes(this.state.query.substring(1).toLowerCase()) + ) + return; + availableFilters.push(filter); + }); + return availableFilters; + } + //Render + render() { + return ( + +
+
+ + + + + + + +
+
+
+ {this.state.query.includes(hashtagChar) && ( + + )} + {this.state.tags.length > 0 && ( +
+ {this.state.tags.map((filter, index) => ( + + {filter}{" "} + this.removeTag(filter)} + /> + + ))} +
+ )} +
+
+ ); + } +} + +export default StashbarSearch; diff --git a/src/stash/stashbar/StashbarSearchTagDisplay.jsx b/src/stash/stashbar/StashbarSearchTagDisplay.jsx new file mode 100644 index 0000000..98f5c33 --- /dev/null +++ b/src/stash/stashbar/StashbarSearchTagDisplay.jsx @@ -0,0 +1,38 @@ +import React from "react"; +class StashbarSearchTagDisplay extends React.Component { + filterClicked(tag) { + this.props.updateQuery(""); + this.props.addTag(tag); + } + displayTags() { + const tags = this.props.getAvailableTags(); + if (tags.length === 0) + return ( + No Filters Matched Your Search + ); + else + return ( + + {tags.map((tag, index) => ( + this.filterClicked.bind(this)(tag)} + > + {tag} + + ))} + + ); + } + + render() { + return ( +
+
{this.displayTags()}
+
+ ); + } +} + +export default StashbarSearchTagDisplay; diff --git a/src/stash/uploader/StashUploadDialog.jsx b/src/stash/uploader/StashUploadDialog.jsx new file mode 100644 index 0000000..7ca1d83 --- /dev/null +++ b/src/stash/uploader/StashUploadDialog.jsx @@ -0,0 +1,108 @@ +//Module Imports +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faExclamationTriangle, + faCloudUploadAlt, + faRedoAlt, + faTimes, + faAngleUp, + faAngleDown, +} from "@fortawesome/free-solid-svg-icons"; +//Local Imports +import StashUploadWatcher from "./StashUploadWatcher"; +import "../scss/stash/StashUploadDialog.scss"; +//Icons List +const successIcon = ; +const errorIcon = ; +const retryIcon = ; +const cancelIcon = ; +const upIcon = ; +const downIcon = ; +export default class StashUploadDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + isMinimized: false, + }; + } + + fudDisplay() { + const uploadLength = Object.values(this.props.uploads).length; + var className = "fud"; + if (this.props.fadeOnClear && uploadLength === 0) { + return (className += " fud-fade"); + } + if (this.state.isMinimized) return (className += " fud-minimized"); + if (uploadLength > 0) { + return (className += " fud-maximized"); + } + return className; + } + + render() { + return ( +
+
+
+ + {this.props.errorCount > 0 ? errorIcon : successIcon} + 0 ? "flex" : "none" }} + > + + {this.props.errorCount} + + +
+
+ + this.setState({ isMinimized: !this.state.isMinimized }) + } + > + Uploads + + {this.state.isMinimized && upIcon} + {!this.state.isMinimized && downIcon} + + +
+
+ + {retryIcon} + + + {cancelIcon} + +
+
+
+ {Object.values(this.props.uploads).map( + (upload, index) => + upload && ( + this.props.retryUpload(upload.uploadUuid)} + clearUpload={() => this.props.clearUpload(upload.uploadUuid)} + uploadProgress={upload.progress} + uploadStatus={upload.status} + /> + ) + )} +
+
+ ); + } +} diff --git a/src/stash/uploader/StashUploadWatcher.jsx b/src/stash/uploader/StashUploadWatcher.jsx new file mode 100644 index 0000000..5f06277 --- /dev/null +++ b/src/stash/uploader/StashUploadWatcher.jsx @@ -0,0 +1,84 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faExclamationTriangle, + faCheck, + faRedoAlt, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; +//Local Imports +import "../scss/stash/StashUploadWatcher.scss"; + +//Constants +const successIcon = ; +const errorIcon = ; +const retryIcon = ; +const cancelIcon = ; +export default class StashUploadWatcher extends React.Component { + constructor(props) { + super(props); + this.retryUpload = props.retryUpload; + this.clearUpload = props.clearUpload; + } + + isUploading() { + return this.props.uploadProgress > 0 && this.props.uploadProgress !== 100; + } + + progressIndicator() { + if (this.props.uploadStatus === "Success") return successIcon; + if (this.props.uploadStatus === "Error") return errorIcon; + if (this.isUploading()) + return ( + + {this.props.uploadProgress}% + + ); + return ; + } + + watcherStatus() { + var className = "file-watcher"; + if (this.isUploading() && this.props.uploadStatus !== "Error") + className += " active"; + if (this.props.uploadStatus === "Success") className += " success"; + return className; + } + + render() { + return ( +
+
+
+ + {this.progressIndicator()} + +
+
+ {this.props.file.name} +
+ {this.props.uploadStatus === "Error" && ( + + {retryIcon} + + )} + + {cancelIcon} + +
+
+ ); + } +}