diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | assets/icons.svg | 0 | ||||
| -rw-r--r-- | assets/main.js | 40 | ||||
| -rw-r--r-- | assets/style.css | 2 | ||||
| -rw-r--r-- | css/main.scss | 203 | ||||
| -rw-r--r-- | js/KeyValueStore.ts | 89 | ||||
| -rw-r--r-- | js/main.ts | 191 | ||||
| -rw-r--r-- | template.go | 68 |
8 files changed, 473 insertions, 122 deletions
| @@ -7,7 +7,7 @@ dev: build | |||
| 7 | 7 | ||
| 8 | build: clean | 8 | build: clean |
| 9 | sassc -t compressed css/main.scss assets/style.css | 9 | sassc -t compressed css/main.scss assets/style.css |
| 10 | tsc --strict --outFile assets/main.js js/main.ts | 10 | tsc --strict --module none --outFile /dev/stdout js/KeyValueStore.ts js/main.ts | uglifyjs -c -m -o assets/main.js -- |
| 11 | pyftsubset fonts/iosevka-term-ss03-regular.ttf --name-IDs+=0,4,6 --text-file=glyphs.txt --flavor='woff' --output-file='assets/iosevka-term-ss03-regular.woff' | 11 | pyftsubset fonts/iosevka-term-ss03-regular.ttf --name-IDs+=0,4,6 --text-file=glyphs.txt --flavor='woff' --output-file='assets/iosevka-term-ss03-regular.woff' |
| 12 | pyftsubset fonts/iosevka-term-ss03-regular.ttf --name-IDs+=0,4,6 --text-file=glyphs.txt --flavor='woff2' --output-file='assets/iosevka-term-ss03-regular.woff2' | 12 | pyftsubset fonts/iosevka-term-ss03-regular.ttf --name-IDs+=0,4,6 --text-file=glyphs.txt --flavor='woff2' --output-file='assets/iosevka-term-ss03-regular.woff2' |
| 13 | go build -o ./gopherproxy ./cmd/gopherproxy/main.go | 13 | go build -o ./gopherproxy ./cmd/gopherproxy/main.go |
diff --git a/assets/icons.svg b/assets/icons.svg new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/assets/icons.svg | |||
diff --git a/assets/main.js b/assets/main.js index 46cb7ee..cbabeef 100644 --- a/assets/main.js +++ b/assets/main.js | |||
| @@ -1,39 +1 @@ | |||
| 1 | "use strict"; | "use strict";var KeyValueStore=function(){function e(e){this.data=e;for(var t=0,n=Object.keys(e);t<n.length;t++){var a=n[t],l=e[a];if(l.valueRange&&-1===l.valueRange.indexOf(l.value))throw new Error('Invalid value "'+l.value+'" for ID "'+a+'"')}}return e.prototype.getValue=function(e){return this.data[e].value},e.prototype.setValue=function(e,t){var n=this.data[e];if(n.valueRange&&-1===n.valueRange.indexOf(t))throw new Error('Invalid value "'+t+'" for ID "'+e+'"');n.value=t,n.callbacks&&n.callbacks.forEach(function(e){e(t)})},e.prototype.cycleValue=function(e,t){void 0===t&&(t=1);var n=this.data[e];if(!n)throw new Error('Invalid ID "'+e+'"');var a=n.value;if(n.valueRange){var l=n.valueRange.indexOf(a)+t;l>=n.valueRange.length?l=0:l<0&&(l=n.valueRange.length-1),a=n.value=n.valueRange[l]}else{if("number"!=typeof a)throw new Error("Can't cycle \""+e+'"');a+=t,n.value=a}return n.callbacks&&n.callbacks.forEach(function(e){e(a)}),a},e.prototype.addCallback=function(e,t){var n=this.data[e];n.callbacks||(n.callbacks=[]),n.callbacks.push(t)},e}();function ensureSetting(e,t){var n=localStorage.getItem(e);return null===n&&(n=t,localStorage.setItem(e,n)),n}var settings=new KeyValueStore({imagePreviews:{value:"1"===ensureSetting("image-previews","1"),valueRange:[!1,!0]}});function generateImageThumbnails(){for(var i=document.querySelectorAll(".link--IMG, .link--GIF"),s=i.length,e=function(){var e=i[s],t=e.href.replace(/^(.*?)\/I/,"$1/T"),n=document.createTextNode("\n"),a=document.createElement("span");a.classList.add("type-annotation"),a.textContent=" -> ";var l=document.createElement("img");l.src=t,l.addEventListener("load",function(e){l.classList.remove("faded")});var r=document.createElement("a");r.classList.add("img-preview"),r.href=e.href,r.addEventListener("click",function(e){return e.preventDefault(),l.classList.add("faded"),l.classList.contains("expanded")?(l.classList.remove("expanded"),l.src=t):(l.classList.add("expanded"),l.src=r.href),!1}),r.append(l),e.parentNode.insertBefore(r,e.nextSibling),e.parentNode.insertBefore(a,r),e.parentNode.insertBefore(n,a)};s--;)e()}function removeImageThumbnails(){for(var e=document.querySelectorAll(".link--IMG, .link--GIF"),t=e.length;t--;)for(var n=e[t],a=3;a--&&n.nextSibling;)n.nextSibling.remove()}!function(){for(var e=document.getElementsByClassName("link--QRY"),t=e.length;t--;)e[t].addEventListener("click",function(e){e.preventDefault();var t=prompt("Please enter required input: ","");return null!==t&&""!==t&&(window.location.href=e.target.href+"?"+t),!1})}(),document.getElementsByClassName("location__prefix")[0].addEventListener("click",function(e){e.preventDefault();var t=prompt("Please enter new location: ","");return null!==t&&""!==t&&(window.location.href=window.location.origin+"/"+t),!1}),function(){function e(e,t){void 0===t&&(t=!1),e?generateImageThumbnails():t||removeImageThumbnails(),localStorage.setItem("image-previews",e?"1":"0"),n.textContent=e?"[yes]":"[no]"}var n=document.getElementsByClassName("setting--image-previews")[0].getElementsByClassName("setting__value")[0];n.addEventListener("click",function(e){return e.preventDefault(),settings.cycleValue("imagePreviews"),!1}),e(settings.getValue("imagePreviews"),!0),settings.addCallback("imagePreviews",e)}(),function(){for(var a=document.getElementsByClassName("modal"),l=a.length,e=function(){var t=a[l],n=t.getElementsByClassName("modal__content")[0],e=t.getElementsByClassName("modal__close-btn")[0];document.addEventListener("click",function(e){t.classList.contains("modal--visible")&&(e.target===n||n.contains(e.target)||(t.classList.remove("modal--visible"),e.preventDefault(),e.stopPropagation()))},!0),document.addEventListener("keydown",function(e){t.classList.contains("modal--visible")&&27===e.keyCode&&t.classList.remove("modal--visible")}),e.addEventListener("click",function(e){return e.preventDefault(),t.classList.remove("modal--visible"),!1})};l--;)e();var t=document.getElementsByClassName("settings-btn")[0],n=document.getElementsByClassName("modal--settings")[0];t.addEventListener("click",function(e){return e.preventDefault(),n.classList.add("modal--visible"),!1})}(); \ No newline at end of file | |
| 2 | var linkQryEls = document.getElementsByClassName('link--QRY'); | ||
| 3 | var i = linkQryEls.length; | ||
| 4 | while (i--) { | ||
| 5 | linkQryEls[i].addEventListener('click', function (e) { | ||
| 6 | e.preventDefault(); | ||
| 7 | var resp = prompt('Please enter required input: ', ''); | ||
| 8 | if ((resp !== null) && (resp !== "")) { | ||
| 9 | window.location.href = e.target.href + '?' + resp; | ||
| 10 | } | ||
| 11 | return false; | ||
| 12 | }); | ||
| 13 | } | ||
| 14 | var imgPreviewEls = document.getElementsByClassName('img-preview'); | ||
| 15 | i = imgPreviewEls.length; | ||
| 16 | var _loop_1 = function () { | ||
| 17 | var imgPreviewEl = imgPreviewEls[i]; | ||
| 18 | var child = imgPreviewEl.children[0]; | ||
| 19 | var thumbnailUrl = child.src; | ||
| 20 | child.addEventListener('load', function (e) { | ||
| 21 | child.classList.remove('faded'); | ||
| 22 | }); | ||
| 23 | imgPreviewEls[i].addEventListener('click', function (e) { | ||
| 24 | e.preventDefault(); | ||
| 25 | child.classList.add('faded'); | ||
| 26 | if (child.classList.contains('expanded')) { | ||
| 27 | child.classList.remove('expanded'); | ||
| 28 | child.src = thumbnailUrl; | ||
| 29 | } | ||
| 30 | else { | ||
| 31 | child.classList.add('expanded'); | ||
| 32 | child.src = imgPreviewEl.href; | ||
| 33 | } | ||
| 34 | return false; | ||
| 35 | }); | ||
| 36 | }; | ||
| 37 | while (i--) { | ||
| 38 | _loop_1(); | ||
| 39 | } | ||
diff --git a/assets/style.css b/assets/style.css index 46ad8cb..3ab3b36 100644 --- a/assets/style.css +++ b/assets/style.css | |||
| @@ -1 +1 @@ | |||
| @font-face{font-family:'Iosevka Term SS03';font-style:normal;font-weight:normal;src:url("/iosevka-term-ss03-regular.woff2") format("woff2"),url("/iosevka-term-ss03-regular.woff") format("woff")}body{margin:0;padding:0;background-color:#14171a;color:#cad1d8}button{font-family:'Iosevka Term SS03', 'IBM Plex Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', 'Droid Sans Mono', Monaco, Consolas, Courier, monospace;font-size:1.0625em;line-height:1.5;background:none;border:0;padding:0;color:#fff}img{display:inline-block;vertical-align:top;max-width:8em;margin:.1em 0}img::selection{background-color:rgba(239,198,138,0.35)}img.expanded{max-width:40em;max-width:80ch}img.faded{opacity:.5}::selection{color:#000;background-color:rgba(239,198,138,0.996)}:link{color:#fff}:visited{color:#cad1d8}.header{font-family:'Iosevka Term SS03', 'IBM Plex Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', 'Droid Sans Mono', Monaco, Consolas, Courier, monospace;font-size:1.0625em;line-height:1.5;padding:.9em 1em;border-bottom:1px solid #353a3f;line-height:1.3;color:#686f76}.header__uripart{color:#929ba3}.header__uripart--last{color:#fff}.wrap{text-align:center}.content{font-family:'Iosevka Term SS03', 'IBM Plex Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', 'Droid Sans Mono', Monaco, Consolas, Courier, monospace;font-size:1.0625em;line-height:1.5;display:inline-block;min-width:50em;min-width:85ch;margin:0;padding:2em 1em;text-align:left}.type-annotation{color:#929ba3} | @font-face{font-family:'Iosevka Term SS03';font-style:normal;font-weight:normal;src:url("/iosevka-term-ss03-regular.woff2") format("woff2"),url("/iosevka-term-ss03-regular.woff") format("woff")}body{margin:0;padding:0;background-color:#14171a;color:#cad1d8;font-family:"Iosevka Term SS03","IBM Plex Mono","Fira Code","Fira Mono","Roboto Mono","Droid Sans Mono",Monaco,Consolas,Courier,monospace;font-size:1.0625em;line-height:1.5}h1,h2,h3,h4,h5,h6{font:inherit;color:#fff;margin:0}button{background:none;border:0;padding:0;color:#fff;font:inherit;text-decoration:underline;cursor:pointer}button:focus{outline:1px dotted currentColor}img{display:inline-block;vertical-align:top;max-width:8em;margin:.1em 0}img::selection{background-color:rgba(239,198,138,0.35)}img.expanded{max-width:40em;max-width:80ch}img.faded{opacity:.5}strong{font-weight:normal}::selection{color:#000;background-color:rgba(239,198,138,0.996)}:link{color:#fff}:visited{color:#cad1d8}:link:hover,:visited:hover{color:#fff}.novisit{color:#fff}.header-base{display:flex;flex-flow:row wrap;align-items:center;justify-content:space-between}.header-base::after{display:block;width:100%;white-space:nowrap;overflow:hidden;color:#5d646a;content:'----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------'}.header{padding:.9em 1em 0;color:#929ba3}.location{color:#5d646a}.location__prefix{color:#5d646a}.location__prefix:hover{color:#fff}.location__uripart{color:#fff}.location__uripart+.location__slash+.location__uripart{color:#cad1d8}.location__uripart+.location__slash+.location__uripart:hover{color:#fff}.wrap{text-align:center}.content{display:inline-block;min-width:50em;min-width:85ch;margin:0;padding:2em 1em;text-align:left;font:inherit}.type-annotation{color:#929ba3}.modal{position:fixed;top:0;left:0;z-index:100;display:none;width:100%;height:100%;box-sizing:border-box;padding:2em;background-color:rgba(0,0,0,0.75)}.modal--visible{display:block}.modal__content{max-width:30em;padding:1.5em 1.8em;margin:0 auto;background-color:#14171a;box-shadow:0 .3em 2em #000;text-align:left}.modal__head{margin-bottom:1.5em}.modal__title{padding-right:1em;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.setting{display:flex;flex-direction:row;align-items:center;justify-content:space-between}.setting__label{white-space:nowrap;overflow:hidden;margin-right:.5em}.setting__label::after{color:#5d646a;content:' . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .'}#none{position:fixed;top:0;left:0;width:0;height:0}@media screen and (max-width: 800px){.location__uripart ~ *{display:none}.modal{padding:1em}.modal__content{padding:1em 1.3em}} | ||
diff --git a/css/main.scss b/css/main.scss index 0d5a5d9..6334f42 100644 --- a/css/main.scss +++ b/css/main.scss | |||
| @@ -2,7 +2,7 @@ $accent: scale-color(#e8ad58, $lightness: 30%); | |||
| 2 | $background: hsl(210, 14%, 9%); | 2 | $background: hsl(210, 14%, 9%); |
| 3 | $text: mix(hsl(210, 60%, 95%), $background, 85%); | 3 | $text: mix(hsl(210, 60%, 95%), $background, 85%); |
| 4 | $text-minus: mix(hsl(210, 100%, 95%), $background, 60%); | 4 | $text-minus: mix(hsl(210, 100%, 95%), $background, 60%); |
| 5 | $text-faint: mix(hsl(210, 100%, 95%), $background, 40%); | 5 | $text-faint: mix(hsl(210, 100%, 95%), $background, 35%); |
| 6 | $text-plus: #fff; | 6 | $text-plus: #fff; |
| 7 | $link-idle: $text-plus; | 7 | $link-idle: $text-plus; |
| 8 | $link-visited: $text; | 8 | $link-visited: $text; |
| @@ -10,11 +10,7 @@ $border: mix(hsl(210, 100%, 95%), $background, 16%); | |||
| 10 | $sel-background: rgba($accent, .996); | 10 | $sel-background: rgba($accent, .996); |
| 11 | $sel-text: #000; | 11 | $sel-text: #000; |
| 12 | 12 | ||
| 13 | @mixin monospace-font { | 13 | $font-monospace: 'Iosevka Term SS03', 'IBM Plex Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', 'Droid Sans Mono', Monaco, Consolas, Courier, monospace; |
| 14 | font-family: 'Iosevka Term SS03', 'IBM Plex Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', 'Droid Sans Mono', Monaco, Consolas, Courier, monospace; | ||
| 15 | font-size: 1 / 16 * 17em; | ||
| 16 | line-height: 1.5; | ||
| 17 | } | ||
| 18 | 14 | ||
| 19 | @font-face { | 15 | @font-face { |
| 20 | font-family: 'Iosevka Term SS03'; | 16 | font-family: 'Iosevka Term SS03'; |
| @@ -35,15 +31,29 @@ body { | |||
| 35 | // hsla(210, 100%, 95%, .025) 2px | 31 | // hsla(210, 100%, 95%, .025) 2px |
| 36 | // ); | 32 | // ); |
| 37 | color: $text; | 33 | color: $text; |
| 34 | font-family: $font-monospace; | ||
| 35 | font-size: 1 / 16 * 17em; | ||
| 36 | line-height: 1.5; | ||
| 37 | } | ||
| 38 | |||
| 39 | h1, h2, h3, h4, h5, h6 { | ||
| 40 | font: inherit; | ||
| 41 | color: $text-plus; | ||
| 42 | margin: 0; | ||
| 38 | } | 43 | } |
| 39 | 44 | ||
| 40 | button { | 45 | button { |
| 41 | @include monospace-font; | 46 | background: none; |
| 47 | border: 0; | ||
| 48 | padding: 0; | ||
| 49 | color: $text-plus; | ||
| 50 | font: inherit; | ||
| 51 | text-decoration: underline; | ||
| 52 | cursor: pointer; | ||
| 42 | 53 | ||
| 43 | background: none; | 54 | &:focus { |
| 44 | border: 0; | 55 | outline: 1px dotted currentColor; |
| 45 | padding: 0; | 56 | } |
| 46 | color: $text-plus; | ||
| 47 | } | 57 | } |
| 48 | 58 | ||
| 49 | img { | 59 | img { |
| @@ -66,6 +76,10 @@ img { | |||
| 66 | } | 76 | } |
| 67 | } | 77 | } |
| 68 | 78 | ||
| 79 | strong { | ||
| 80 | font-weight: normal; | ||
| 81 | } | ||
| 82 | |||
| 69 | ::selection { | 83 | ::selection { |
| 70 | color: $sel-text; | 84 | color: $sel-text; |
| 71 | background-color: $sel-background; | 85 | background-color: $sel-background; |
| @@ -79,44 +93,175 @@ img { | |||
| 79 | color: $link-visited; | 93 | color: $link-visited; |
| 80 | } | 94 | } |
| 81 | 95 | ||
| 96 | :link, | ||
| 97 | :visited { | ||
| 98 | &:hover { | ||
| 99 | color: $text-plus; | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | .novisit { | ||
| 104 | color: $link-idle; | ||
| 105 | } | ||
| 106 | |||
| 82 | // :link, :visited { | 107 | // :link, :visited { |
| 83 | // &:hover { | 108 | // &:hover { |
| 84 | // color: $text-plus; | 109 | // color: $text-plus; |
| 85 | // } | 110 | // } |
| 86 | // } | 111 | // } |
| 87 | 112 | ||
| 88 | .header { | 113 | .header-base { |
| 89 | @include monospace-font; | 114 | display: flex; |
| 115 | flex-flow: row wrap; | ||
| 116 | align-items: center; | ||
| 117 | justify-content: space-between; | ||
| 90 | 118 | ||
| 91 | padding: .9em 1em; | 119 | &::after { |
| 92 | border-bottom: 1px solid $border; | 120 | display: block; |
| 93 | line-height: 1.3; | 121 | width: 100%; |
| 94 | color: $text-faint; | 122 | white-space: nowrap; |
| 123 | overflow: hidden; | ||
| 124 | color: $text-faint; | ||
| 125 | content: '----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------'; | ||
| 126 | } | ||
| 95 | } | 127 | } |
| 96 | 128 | ||
| 97 | .header__uripart { | 129 | // |
| 98 | color: $text-minus; | 130 | |
| 131 | .header { | ||
| 132 | padding: .9em 1em 0; | ||
| 133 | color: $text-minus; | ||
| 99 | } | 134 | } |
| 100 | 135 | ||
| 101 | .header__uripart--last { | 136 | // |
| 102 | color: $text-plus; | 137 | |
| 138 | .location { | ||
| 139 | color: $text-faint; | ||
| 140 | |||
| 141 | &__prefix { | ||
| 142 | color: $text-faint; | ||
| 143 | |||
| 144 | &:hover { | ||
| 145 | color: $text-plus; | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | &__uripart { | ||
| 150 | color: $text-plus; | ||
| 151 | |||
| 152 | + .location__slash + .location__uripart { | ||
| 153 | color: $text; | ||
| 154 | |||
| 155 | &:hover { | ||
| 156 | color: $text-plus; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 103 | } | 160 | } |
| 104 | 161 | ||
| 162 | // | ||
| 163 | |||
| 105 | .wrap { | 164 | .wrap { |
| 106 | text-align: center; | 165 | text-align: center; |
| 107 | } | 166 | } |
| 108 | 167 | ||
| 109 | .content { | 168 | .content { |
| 110 | @include monospace-font; | 169 | display: inline-block; |
| 111 | 170 | min-width: 50em; | |
| 112 | display: inline-block; | 171 | min-width: 5ch + 80; |
| 113 | min-width: 50em; | 172 | margin: 0; |
| 114 | min-width: 5ch + 80; | 173 | padding: 2em 1em; |
| 115 | margin: 0; | 174 | text-align: left; |
| 116 | padding: 2em 1em; | 175 | font: inherit; |
| 117 | text-align: left; | ||
| 118 | } | 176 | } |
| 119 | 177 | ||
| 178 | // | ||
| 179 | |||
| 120 | .type-annotation { | 180 | .type-annotation { |
| 121 | color: $text-minus; | 181 | color: $text-minus; |
| 122 | } | 182 | } |
| 183 | |||
| 184 | // | ||
| 185 | |||
| 186 | .modal { | ||
| 187 | position: fixed; | ||
| 188 | top: 0; | ||
| 189 | left: 0; | ||
| 190 | z-index: 100; | ||
| 191 | display: none; | ||
| 192 | width: 100%; | ||
| 193 | height: 100%; | ||
| 194 | box-sizing: border-box; | ||
| 195 | padding: 2em; | ||
| 196 | background-color: rgba(#000, .75); | ||
| 197 | |||
| 198 | &--visible { | ||
| 199 | display: block; | ||
| 200 | } | ||
| 201 | |||
| 202 | &__content { | ||
| 203 | max-width: 30em; | ||
| 204 | padding: 1.5em 1.8em; | ||
| 205 | margin: 0 auto; | ||
| 206 | background-color: $background; | ||
| 207 | box-shadow: 0 .3em 2em #000; | ||
| 208 | text-align: left; | ||
| 209 | } | ||
| 210 | |||
| 211 | &__head { | ||
| 212 | margin-bottom: 1.5em; | ||
| 213 | } | ||
| 214 | |||
| 215 | &__title { | ||
| 216 | padding-right: 1em; | ||
| 217 | white-space: nowrap; | ||
| 218 | text-overflow: ellipsis; | ||
| 219 | overflow: hidden; | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | // | ||
| 224 | |||
| 225 | .setting { | ||
| 226 | display: flex; | ||
| 227 | flex-direction: row; | ||
| 228 | align-items: center; | ||
| 229 | justify-content: space-between; | ||
| 230 | |||
| 231 | &__label { | ||
| 232 | white-space: nowrap; | ||
| 233 | overflow: hidden; | ||
| 234 | margin-right: .5em; | ||
| 235 | |||
| 236 | &::after { | ||
| 237 | color: $text-faint; | ||
| 238 | content: ' . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .'; | ||
| 239 | } | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | // | ||
| 244 | |||
| 245 | #none { | ||
| 246 | position: fixed; | ||
| 247 | top: 0; | ||
| 248 | left: 0; | ||
| 249 | width: 0; | ||
| 250 | height: 0; | ||
| 251 | } | ||
| 252 | |||
| 253 | // | ||
| 254 | |||
| 255 | @media screen and (max-width: 800px) { | ||
| 256 | .location__uripart ~ * { | ||
| 257 | display: none; | ||
| 258 | } | ||
| 259 | |||
| 260 | .modal { | ||
| 261 | padding: 1em; | ||
| 262 | } | ||
| 263 | |||
| 264 | .modal__content { | ||
| 265 | padding: 1em 1.3em; | ||
| 266 | } | ||
| 267 | } | ||
diff --git a/js/KeyValueStore.ts b/js/KeyValueStore.ts new file mode 100644 index 0000000..fd8f39e --- /dev/null +++ b/js/KeyValueStore.ts | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | type KeyValueStoreCallback<T> = (value: T) => void; | ||
| 2 | |||
| 3 | interface IKeyValueStoreParam<T> { | ||
| 4 | value: T; | ||
| 5 | valueRange?: T[]; | ||
| 6 | callbacks?: KeyValueStoreCallback<T>[]; | ||
| 7 | } | ||
| 8 | |||
| 9 | type KeyValueStoreData<T> = { [K in keyof T]: IKeyValueStoreParam<T[K]> }; | ||
| 10 | |||
| 11 | class KeyValueStore<T> { | ||
| 12 | constructor(private data: KeyValueStoreData<T>) { | ||
| 13 | for (const id of Object.keys(data)) { | ||
| 14 | const props = data[id as keyof typeof data]; | ||
| 15 | |||
| 16 | if (props.valueRange && props.valueRange.indexOf(props.value) === -1) { | ||
| 17 | throw new Error(`Invalid value "${props.value}" for ID "${id}"`); | ||
| 18 | } | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | public getValue(id: keyof T) { | ||
| 23 | return this.data[id].value; | ||
| 24 | } | ||
| 25 | |||
| 26 | public setValue<K extends keyof T>(id: K, value: T[K]) { | ||
| 27 | const props = this.data[id]; | ||
| 28 | |||
| 29 | if (props.valueRange) { | ||
| 30 | const valueIndex = props.valueRange.indexOf(value); | ||
| 31 | if (valueIndex === -1) { | ||
| 32 | throw new Error(`Invalid value "${value}" for ID "${id}"`); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | props.value = value; | ||
| 37 | |||
| 38 | if (props.callbacks) { | ||
| 39 | props.callbacks.forEach(callback => { | ||
| 40 | callback(value); | ||
| 41 | }); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | public cycleValue<K extends keyof T>(id: K, dir: -1 | 1 = 1) { | ||
| 46 | const props = this.data[id]; | ||
| 47 | |||
| 48 | if (!props) { | ||
| 49 | throw new Error(`Invalid ID "${id}"`); | ||
| 50 | } | ||
| 51 | |||
| 52 | let value = props.value; | ||
| 53 | |||
| 54 | if (props.valueRange) { | ||
| 55 | let valueIndex = props.valueRange.indexOf(value) + dir; | ||
| 56 | |||
| 57 | if (valueIndex >= props.valueRange.length) { | ||
| 58 | valueIndex = 0; | ||
| 59 | } else if (valueIndex < 0) { | ||
| 60 | valueIndex = props.valueRange.length - 1; | ||
| 61 | } | ||
| 62 | |||
| 63 | value = props.value = props.valueRange[valueIndex]; | ||
| 64 | } else if (typeof value === "number") { | ||
| 65 | (value as number) += dir; | ||
| 66 | props.value = value; | ||
| 67 | } else { | ||
| 68 | throw new Error(`Can't cycle "${id}"`); | ||
| 69 | } | ||
| 70 | |||
| 71 | if (props.callbacks) { | ||
| 72 | props.callbacks.forEach(callback => { | ||
| 73 | callback(value); | ||
| 74 | }); | ||
| 75 | } | ||
| 76 | |||
| 77 | return value; | ||
| 78 | } | ||
| 79 | |||
| 80 | public addCallback<K extends keyof T>(id: K, callback: KeyValueStoreCallback<T[K]>) { | ||
| 81 | const props = this.data[id]; | ||
| 82 | |||
| 83 | if (!props.callbacks) { | ||
| 84 | props.callbacks = []; | ||
| 85 | } | ||
| 86 | |||
| 87 | props.callbacks.push(callback); | ||
| 88 | } | ||
| 89 | } | ||
| @@ -1,42 +1,181 @@ | |||
| 1 | let linkQryEls = document.getElementsByClassName('link--QRY'); | 1 | /// <reference path="./KeyValueStore.ts" /> |
| 2 | let i = linkQryEls.length; | 2 | |
| 3 | while (i--) { | 3 | // |
| 4 | linkQryEls[i].addEventListener('click', e => { | 4 | |
| 5 | function ensureSetting(key: string, defaultValue: string): string { | ||
| 6 | let strValue = localStorage.getItem(key); | ||
| 7 | if (strValue === null) { | ||
| 8 | strValue = defaultValue; | ||
| 9 | localStorage.setItem(key, strValue); | ||
| 10 | } | ||
| 11 | return strValue; | ||
| 12 | } | ||
| 13 | |||
| 14 | const settings = new KeyValueStore({ | ||
| 15 | imagePreviews: { | ||
| 16 | value: ensureSetting('image-previews', '1') === '1', | ||
| 17 | valueRange: [false, true] | ||
| 18 | } | ||
| 19 | }); | ||
| 20 | |||
| 21 | // | ||
| 22 | |||
| 23 | (() => { | ||
| 24 | const linkQryEls = document.getElementsByClassName('link--QRY'); | ||
| 25 | let i = linkQryEls.length; | ||
| 26 | while (i--) { | ||
| 27 | linkQryEls[i].addEventListener('click', e => { | ||
| 28 | e.preventDefault(); | ||
| 29 | |||
| 30 | const resp = prompt('Please enter required input: ', ''); | ||
| 31 | if ((resp !== null) && (resp !== "")) { | ||
| 32 | window.location.href = (e.target as HTMLAnchorElement).href + '?' + resp; | ||
| 33 | } | ||
| 34 | |||
| 35 | return false; | ||
| 36 | }); | ||
| 37 | } | ||
| 38 | })(); | ||
| 39 | |||
| 40 | (() => { | ||
| 41 | const locationPrefixEl = document.getElementsByClassName('location__prefix')[0]; | ||
| 42 | locationPrefixEl.addEventListener('click', e => { | ||
| 5 | e.preventDefault(); | 43 | e.preventDefault(); |
| 6 | 44 | ||
| 7 | const resp = prompt('Please enter required input: ', ''); | 45 | const resp = prompt('Please enter new location: ', ''); |
| 8 | if ((resp !== null) && (resp !== "")) { | 46 | if ((resp !== null) && (resp !== "")) { |
| 9 | window.location.href = (e.target as HTMLAnchorElement).href + '?' + resp; | 47 | window.location.href = window.location.origin + '/' + resp; |
| 10 | } | 48 | } |
| 11 | 49 | ||
| 12 | return false; | 50 | return false; |
| 13 | }); | 51 | }); |
| 14 | } | 52 | })(); |
| 15 | 53 | ||
| 16 | let imgPreviewEls = document.getElementsByClassName('img-preview'); | 54 | (() => { |
| 17 | i = imgPreviewEls.length; | 55 | const settingImagePreviewEl = document.getElementsByClassName('setting--image-previews')[0]; |
| 18 | while (i--) { | 56 | const settingImagePreviewValueEl = settingImagePreviewEl.getElementsByClassName('setting__value')[0]; |
| 19 | const imgPreviewEl = imgPreviewEls[i] as HTMLAnchorElement; | 57 | const settingImagePreviewCallback = (value: boolean, init = false) => { |
| 20 | const child = imgPreviewEl.children[0] as HTMLImageElement; | 58 | if (value) { |
| 21 | const thumbnailUrl = child.src; | 59 | generateImageThumbnails(); |
| 60 | } else if (!init) { | ||
| 61 | removeImageThumbnails(); | ||
| 62 | } | ||
| 22 | 63 | ||
| 23 | child.addEventListener('load', e => { | 64 | localStorage.setItem('image-previews', value ? '1' : '0'); |
| 24 | child.classList.remove('faded'); | 65 | settingImagePreviewValueEl.textContent = value ? '[yes]' : '[no]'; |
| 25 | }); | 66 | } |
| 26 | 67 | ||
| 27 | imgPreviewEls[i].addEventListener('click', e => { | 68 | settingImagePreviewValueEl.addEventListener('click', e => { |
| 28 | e.preventDefault(); | 69 | e.preventDefault(); |
| 70 | settings.cycleValue('imagePreviews'); | ||
| 71 | return false; | ||
| 72 | }); | ||
| 29 | 73 | ||
| 30 | child.classList.add('faded'); | 74 | settingImagePreviewCallback(settings.getValue('imagePreviews'), true); |
| 75 | settings.addCallback('imagePreviews', settingImagePreviewCallback); | ||
| 76 | })(); | ||
| 31 | 77 | ||
| 32 | if (child.classList.contains('expanded')) { | 78 | (() => { |
| 33 | child.classList.remove('expanded'); | 79 | const modalEls = document.getElementsByClassName('modal'); |
| 34 | child.src = thumbnailUrl; | 80 | let i = modalEls.length; |
| 35 | } else { | 81 | while (i--) { |
| 36 | child.classList.add('expanded'); | 82 | const modalEl = modalEls[i]; |
| 37 | child.src = imgPreviewEl.href; | 83 | const modalContentEl = modalEl.getElementsByClassName('modal__content')[0]; |
| 38 | } | 84 | const modalCloseBtnEl = modalEl.getElementsByClassName('modal__close-btn')[0]; |
| 39 | 85 | ||
| 86 | document.addEventListener('click', e => { | ||
| 87 | if (!modalEl.classList.contains('modal--visible')) { | ||
| 88 | return; | ||
| 89 | } | ||
| 90 | if (e.target !== modalContentEl && !modalContentEl.contains(e.target as Element)) { | ||
| 91 | modalEl.classList.remove('modal--visible'); | ||
| 92 | e.preventDefault(); | ||
| 93 | e.stopPropagation(); | ||
| 94 | } | ||
| 95 | }, true); | ||
| 96 | |||
| 97 | document.addEventListener('keydown', e => { | ||
| 98 | if (!modalEl.classList.contains('modal--visible')) { | ||
| 99 | return; | ||
| 100 | } | ||
| 101 | if (e.keyCode === 27) { | ||
| 102 | modalEl.classList.remove('modal--visible'); | ||
| 103 | } | ||
| 104 | }); | ||
| 105 | |||
| 106 | modalCloseBtnEl.addEventListener('click', e => { | ||
| 107 | e.preventDefault(); | ||
| 108 | modalEl.classList.remove('modal--visible'); | ||
| 109 | return false; | ||
| 110 | }); | ||
| 111 | } | ||
| 112 | |||
| 113 | // | ||
| 114 | |||
| 115 | const settingsBtnEl = document.getElementsByClassName('settings-btn')[0]; | ||
| 116 | const settingsModalEl = document.getElementsByClassName('modal--settings')[0]; | ||
| 117 | |||
| 118 | settingsBtnEl.addEventListener('click', e => { | ||
| 119 | e.preventDefault(); | ||
| 120 | settingsModalEl.classList.add('modal--visible'); | ||
| 40 | return false; | 121 | return false; |
| 41 | }); | 122 | }); |
| 123 | })(); | ||
| 124 | |||
| 125 | function generateImageThumbnails() { | ||
| 126 | const linkImgEls = document.querySelectorAll('.link--IMG, .link--GIF'); | ||
| 127 | let i = linkImgEls.length; | ||
| 128 | while (i--) { | ||
| 129 | const linkImgEl = linkImgEls[i] as HTMLAnchorElement; | ||
| 130 | const thumbnailUrl = linkImgEl.href.replace(/^(.*?)\/I/, '$1/T'); | ||
| 131 | |||
| 132 | const lineBreakEl = document.createTextNode('\n'); | ||
| 133 | |||
| 134 | const typeAnnotEl = document.createElement('span'); | ||
| 135 | typeAnnotEl.classList.add('type-annotation'); | ||
| 136 | typeAnnotEl.textContent = ' -> '; | ||
| 137 | |||
| 138 | const thumbnailEl = document.createElement('img'); | ||
| 139 | thumbnailEl.src = thumbnailUrl; | ||
| 140 | thumbnailEl.addEventListener('load', e => { | ||
| 141 | thumbnailEl.classList.remove('faded'); | ||
| 142 | }); | ||
| 143 | |||
| 144 | const thumbnailAnchorEl = document.createElement('a'); | ||
| 145 | thumbnailAnchorEl.classList.add('img-preview'); | ||
| 146 | thumbnailAnchorEl.href = linkImgEl.href; | ||
| 147 | thumbnailAnchorEl.addEventListener('click', e => { | ||
| 148 | e.preventDefault(); | ||
| 149 | |||
| 150 | thumbnailEl.classList.add('faded'); | ||
| 151 | |||
| 152 | if (thumbnailEl.classList.contains('expanded')) { | ||
| 153 | thumbnailEl.classList.remove('expanded'); | ||
| 154 | thumbnailEl.src = thumbnailUrl; | ||
| 155 | } else { | ||
| 156 | thumbnailEl.classList.add('expanded'); | ||
| 157 | thumbnailEl.src = thumbnailAnchorEl.href; | ||
| 158 | } | ||
| 159 | |||
| 160 | return false; | ||
| 161 | }); | ||
| 162 | |||
| 163 | thumbnailAnchorEl.append(thumbnailEl); | ||
| 164 | linkImgEl.parentNode!.insertBefore(thumbnailAnchorEl, linkImgEl.nextSibling); | ||
| 165 | linkImgEl.parentNode!.insertBefore(typeAnnotEl, thumbnailAnchorEl); | ||
| 166 | linkImgEl.parentNode!.insertBefore(lineBreakEl, typeAnnotEl); | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | function removeImageThumbnails() { | ||
| 171 | const linkImgEls = document.querySelectorAll('.link--IMG, .link--GIF'); | ||
| 172 | let i = linkImgEls.length; | ||
| 173 | while (i--) { | ||
| 174 | const linkImgEl = linkImgEls[i]; | ||
| 175 | let j = 3; | ||
| 176 | |||
| 177 | while (j-- && linkImgEl.nextSibling) { | ||
| 178 | linkImgEl.nextSibling.remove(); | ||
| 179 | } | ||
| 180 | } | ||
| 42 | } | 181 | } |
diff --git a/template.go b/template.go index f5f4f2b..dfda36d 100644 --- a/template.go +++ b/template.go | |||
| @@ -12,31 +12,38 @@ var tpltext = `<!doctype html> | |||
| 12 | </style> | 12 | </style> |
| 13 | </head> | 13 | </head> |
| 14 | <body> | 14 | <body> |
| 15 | <header class="header"> | 15 | <header class="header header-base"> |
| 16 | {{- $href := "" -}} | 16 | <div class="location"> |
| 17 | {{- $uriParts := split .URI "/" -}} | 17 | {{- $href := "" -}} |
| 18 | {{- $uriLast := $uriParts | last -}} | 18 | {{- $uriParts := split .URI "/" -}} |
| 19 | {{- $uriParts = $uriParts | pop -}} | 19 | {{- $uriLast := $uriParts | last -}} |
| 20 | {{- if eq $uriLast "" -}} | 20 | {{- $uriParts = $uriParts | pop -}} |
| 21 | {{- $uriLast = $uriParts | last -}} | 21 | {{- if eq $uriLast "" -}} |
| 22 | {{- $uriParts = $uriParts | pop -}} | 22 | {{- $uriLast = $uriParts | last -}} |
| 23 | {{- end -}} | 23 | {{- $uriParts = $uriParts | pop -}} |
| 24 | {{- if eq (len $uriParts) 1 -}} | 24 | {{- end -}} |
| 25 | {{- $uriLast = $uriParts | last -}} | 25 | {{- if eq (len $uriParts) 1 -}} |
| 26 | {{- $uriParts = $uriParts | pop -}} | 26 | {{- $uriLast = $uriParts | last -}} |
| 27 | {{- end -}} | 27 | {{- $uriParts = $uriParts | pop -}} |
| 28 | {{- end -}} | ||
| 28 | 29 | ||
| 29 | {{- range $i, $part := $uriParts -}} | 30 | <button class="location__prefix">gopher://</button> {{ range $i, $part := $uriParts -}} |
| 30 | {{- if ne $i 1 -}} | 31 | {{- if ne $i 1 -}} |
| 31 | {{- $href = printf "%s/%s" $href . -}} | 32 | {{- $href = printf "%s/%s" $href . -}} |
| 32 | {{- if ne $i 0 }} / {{ end -}} | 33 | {{- if ne $i 0 -}} |
| 33 | <a href="{{ $href }}" class="header__uripart">{{ . }}</a> | 34 | <span class="location__slash"> / </span> |
| 34 | {{- else -}} | 35 | {{- end -}} |
| 35 | {{- $href = printf "%s/1" $href -}} | 36 | <a href="{{ $href }}" class="location__uripart">{{ . }}</a> |
| 37 | {{- else -}} | ||
| 38 | {{- $href = printf "%s/1" $href -}} | ||
| 39 | {{- end -}} | ||
| 40 | {{- end -}} | ||
| 41 | {{- if ne (len $uriParts) 0 -}} | ||
| 42 | <span class="location__slash"> / </span> | ||
| 36 | {{- end -}} | 43 | {{- end -}} |
| 37 | {{- end -}} | 44 | <span class="location__uripart">{{ $uriLast -}}</span> |
| 38 | {{- if ne (len $uriParts) 0 }} / {{ end -}} | 45 | </div> |
| 39 | <span class="header__uripart header__uripart--last">{{ $uriLast -}}</span> | 46 | <button class="settings-btn">Settings</button> |
| 40 | </header> | 47 | </header> |
| 41 | <main class="wrap"> | 48 | <main class="wrap"> |
| 42 | <pre class="content"> | 49 | <pre class="content"> |
| @@ -48,9 +55,6 @@ var tpltext = `<!doctype html> | |||
| 48 | {{- end -}} | 55 | {{- end -}} |
| 49 | {{- if .Link -}} | 56 | {{- if .Link -}} |
| 50 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\">%s </span><a class=\"link link--%s\" href=\"%s\">%s</a>" .Type .Type .Link (.Text | HTMLEscape)) -}} | 57 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\">%s </span><a class=\"link link--%s\" href=\"%s\">%s</a>" .Type .Type .Link (.Text | HTMLEscape)) -}} |
| 51 | {{- if or (eq .Type "IMG") (eq .Type "GIF") -}} | ||
| 52 | {{- $content = printf "%s\n%s" $content (printf "<span class=\"type-annotation\"> -> </span><a class=\"img-preview\" href=\"%s\"><img src=\"%s\" /></a>" .Link (.Link | replace "^/(.*?)/I" "/$1/T")) -}} | ||
| 53 | {{- end -}} | ||
| 54 | {{- else -}} | 58 | {{- else -}} |
| 55 | {{- $content = printf "%s%s" $content (printf " %s" (.Text | HTMLEscape)) -}} | 59 | {{- $content = printf "%s%s" $content (printf " %s" (.Text | HTMLEscape)) -}} |
| 56 | {{- end -}} | 60 | {{- end -}} |
| @@ -61,6 +65,18 @@ var tpltext = `<!doctype html> | |||
| 61 | {{- end -}} | 65 | {{- end -}} |
| 62 | </pre> | 66 | </pre> |
| 63 | </main> | 67 | </main> |
| 68 | <aside class="modal modal--settings"> | ||
| 69 | <div class="modal__content"> | ||
| 70 | <header class="modal__head header-base"> | ||
| 71 | <h1 class="modal__title">Settings</h1> | ||
| 72 | <button class="modal__close-btn">Close</button> | ||
| 73 | </header> | ||
| 74 | <div class="setting setting--image-previews"> | ||
| 75 | <strong class="setting__label">Image thumbnails</strong> | ||
| 76 | <button class="setting__value">[N/A]</button> | ||
| 77 | </div> | ||
| 78 | </div> | ||
| 79 | </aside> | ||
| 64 | <script type="text/javascript"> | 80 | <script type="text/javascript"> |
| 65 | {{ .Script | safeJs }} | 81 | {{ .Script | safeJs }} |
| 66 | </script> | 82 | </script> |
