├── TODO.txt ├── asset ├── README.md ├── usage.png ├── matrix-meme.png ├── acknowledgement.png ├── dir.svg ├── file.svg ├── browser-download-symbolic.svg ├── browser-download-symbolic-dark.svg ├── favicon.svg ├── paste.svg └── no-wifi.svg ├── widgets └── theme-switch.html ├── new └── index.html ├── js ├── include-html.js ├── theme-switch.js ├── programming-txt-mime.js ├── my-marked-base-url.js ├── ipynp.notebook.min.js └── main.js ├── download ├── README.md ├── index.html ├── styles.css └── fetcher.js ├── css ├── ipynp.notebook.css ├── theme-switch.css ├── prism.css ├── main.css └── style.css ├── index.html └── README.md /TODO.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asset/README.md: -------------------------------------------------------------------------------- 1 | all images are here 2 | 3 | -------------------------------------------------------------------------------- /asset/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahemesam/repo-view/HEAD/asset/usage.png -------------------------------------------------------------------------------- /asset/matrix-meme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahemesam/repo-view/HEAD/asset/matrix-meme.png -------------------------------------------------------------------------------- /asset/acknowledgement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahemesam/repo-view/HEAD/asset/acknowledgement.png -------------------------------------------------------------------------------- /asset/dir.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asset/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /widgets/theme-switch.html: -------------------------------------------------------------------------------- 1 |
4 |
5 |
11 | let's say you have a really good private repo that you wanna add to your CV. 12 | wait! it is private. right ? then how ?? would you make it public ??? 13 | hell NO! it is private and MUST stay as is. 14 | sure you don't wanna your business proprietary code base be published as open-source. 15 | man!, you just wanna show it only to some employers. 16 | of course you can add these employers as collaborators with read-only permissions to this private repo. 17 | but, first you will have to get their GitHub usernames. also, you will have to add them one by one. 18 | maybe you gonna use a cloud storage service with code syntax highlighting ? 19 | oh, what about synchronization between your Github repo and these cloud services ?? 20 | what if the cloud shared folder got found by search engines ??? 21 |22 |
25 | there is a website called GitFront.io : 26 | it can host your private repo and provide a presentation url that is accessible to anyone who has it. 27 | - problem with GitFront is that: it allows hosting only less-than 100 MB repo for free account. 28 | if your private repo is larger than that, you gonna have to pay 💵💵💲 29 | ( this was the reason why I created this project ) 30 | - your GitFront repo does not get directly synchronized with GitHub 31 |32 |
33 | my repo-view does exactly the same except that: 34 | - it is a static web page (no backend) -> your code exists only on GitHub. it does not goto any 3rd party server 35 | - it uses official GitHub REST API, it gets files directly from GitHub 36 | - no problem if repo is larger than 100 MB. you still can preview it 37 | - it is a single page application 38 | - it has some extra features eg: dark theme switch, preview pdf, detect internet disconnect... 39 |40 |
41 |
42 |
48 | it is a static web page. so, configuration is gonna be passed as url params. 49 |73 |params :-
50 | token : string => github access_token 51 | warning: use only tokens with only permissions: repos read-only
52 | repo : string => the repo name on github
53 | owner : string => the repo owner username (or organization username) on github 54 | if omitted, it would be fetched from Github (the username who created the access_token). 55 | so, if that username is not the owner of the repo, you gonna get ERROR 404
56 | token_ready : bool => it is optional: default is "false" => 57 | if true: provide token param is gonna be used as is 58 | else: 59 | the provided token param must be provied as encodes base64 string using js "btoa" method 60 | it first will be decoded back ( using js "atob" method ) 61 | why? 62 | - the token does not exist readily on url: 63 | if any web scrapper found the url and got the token and tried it on their terminal: 64 | it wont work. it fist need to be decoded 65 | - if the one ,to whom you send the url, tried to use the token (in their terminal) 66 | before they view the url on web browser. and it worked. 67 | they may think they got hands on a treasure. and they may exploit this token. 68 | but if it did not work (ie: it was base64-encoded): sooner or later, after they open the url, 69 | they gonna find that they can just get the real token from the web page. 70 | so, they wont get too excited about that token.
71 | so, the url would be: https://ibrahemesam.github.io/repo-view/?token=<token>&repo=<repo-name>&owner=<owner-username> 72 |
77 |87 |First, go to this page 👉 Create repo-view URL
78 | Then, 👇👇👇👇👇 79 |80 |
86 |81 | NB: If you have to, append this string "&hide_acknowledgement=true" 82 | to the created url to hide this acknowledgement. 83 |
84 | Otherwise, leave it to support me ❤️. 85 |
88 | NB: 89 | Github Repository: should be just the repo name. Not the whole url. ie: "foo" instead of "https://github.com/username/foo" 90 | NB: 91 | The resulting repo-view url has its token as encrypted string. 92 | - Why being encrypted? 93 | = Because when putting the url in a github.io page (or anywhere on Github), 94 | if the token is not encrypted, Github detects it and think it exists by mistake as a vulnerability. 95 | So, Github immediately disables the token, and the repo-view url is disabled as well. 96 |97 |
99 | this demo repo is a private github repository. if you visit it, you get 404 because it is Private. 100 |101 |
NB: public repos can also be viewed102 |
104 | !! Warning !!: any one with the url can preview and clone the repo 105 | so, put it only on your CV and send it only to employers 106 | do NOT put it on places where it may be stolen eg: https://<your_username>.github.io 107 | or the production business website of the private repo 108 |109 |
111 | 1 - go to Github > settings > Developer settings > Personal access tokens > Fine-gained tokens 112 | 2 - set "Token name" and "Expiration" date. 113 | 3 - on "Repository access" section: select "Only select repositories" then select the repo you wanna use. 114 | 4 - under "Permissions section": under "Repository permissions": select "Contents" with "Read-only" access level. 115 | 5 - click "Generate token" then copy it. 116 |117 |
118 | !! Warning !!: any one with the url can get the token. 119 | so, when creating the token, do NOT add any permissions to the token other than 120 | read-only access to repository content (access to only one private repo. NOT all !). 121 | otherwise, the token may be exploited !! 122 |123 |
if you like this project, give it a Star ⭐126 |
129 | This project is provided "AS IS" with absolutely "NO WARRANTY". 130 | If you gonna use its source-code somewhere: make a clear credit refering to this repo-view repository. 131 |132 | 133 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | /*a { color: #dc8100; text-decoration: none; } 2 | a:hover { color: #efe8e8; text-decoration: none; }*/ 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | width: 100vw !important; 7 | top: 0 !important; 8 | position: fixed; 9 | overflow-x: hidden; 10 | overflow: auto; 11 | } 12 | 13 | .container { 14 | height: 100vh !important; 15 | } 16 | 17 | img.file { 18 | content: url("../asset/file.svg"); 19 | } 20 | 21 | img.dir { 22 | content: url("../asset/dir.svg"); 23 | } 24 | 25 | .location#location-div * { 26 | user-select: none; 27 | } 28 | 29 | .blob-view .content { 30 | overflow: auto; 31 | padding: 2%; 32 | overflow-x: auto; 33 | } 34 | 35 | @media screen and (orientation: landscape) { 36 | .blob-view .content > .nb-notebook { 37 | margin-inline-start: 10%; 38 | } 39 | } 40 | 41 | @media screen and (orientation: portrait) { 42 | .blob-view .content > .nb-notebook { 43 | margin-inline-start: 25%; 44 | } 45 | } 46 | 47 | .blob-view .content pre { 48 | width: 100%; 49 | margin-top: 0; 50 | margin-bottom: 0; 51 | margin: 0; 52 | } 53 | 54 | /*.blob-view .content pre code { 55 | width: calc(100% - 4.8em); 56 | }*/ 57 | 58 | .blob-view .content pre.line-numbers { 59 | width: calc(100% - 4.8em); 60 | } 61 | 62 | .blob-view .content:has(iframe[src*="pdf.js"]) { 63 | overflow-y: hidden; 64 | } 65 | 66 | .blob-view .content pre:not(.line-numbers) { 67 | padding: 1% 1%; 68 | width: 98%; 69 | /* text-wrap: balance; */ 70 | } 71 | 72 | ul.tree { 73 | max-height: 40vh; 74 | overflow: auto; 75 | } 76 | 77 | body:has(ul.tree[hidden]), 78 | body:has(#preview-div[hidden]) { 79 | height: 100vh !important; 80 | overflow: hidden; 81 | } 82 | 83 | body:has(#preview-div[hidden]) ul.tree { 84 | max-height: 75vh; 85 | } 86 | 87 | body:has(ul.tree[hidden]) .blob-view .content { 88 | height: 70vh !important; 89 | } 90 | 91 | div.footer { 92 | /* position: fixed; */ 93 | bottom: 2px; 94 | } 95 | 96 | html[acknowledgement-hidden="true"] div.footer:has(.made-with-love) { 97 | display: none; 98 | } 99 | 100 | html[acknowledgement-hidden="true"] #main-loader-spinner-div { 101 | top: 70%; 102 | } 103 | 104 | body:has(#preview-div[hidden]) div.footer 105 | /*body:has(ul.tree[hidden]) div.footer*/ { 106 | position: fixed; 107 | bottom: 8px; 108 | /* transform: translateY(-50%); */ 109 | } 110 | 111 | #main-loader-spinner-div { 112 | position: fixed; 113 | top: 60%; 114 | zoom: 5; 115 | left: 50%; 116 | transform: translate(-50%, -100%); 117 | } 118 | body:has(ul.tree:not([hidden])) div#main-loader-spinner-div, 119 | body:has(#preview-div:not([hidden])) div#main-loader-spinner-div, 120 | body:has(.create-url:not([hidden])) div#main-loader-spinner-div { 121 | display: none; 122 | } 123 | 124 | body:has(ul.tree[hidden]):has(#preview-div[hidden]):has( 125 | .container div.location[hidden] 126 | ) 127 | div.footer { 128 | position: fixed; 129 | top: 60%; 130 | left: 50%; 131 | transform: translateX(-50%); 132 | margin: auto; 133 | padding: 10px; 134 | outline: none; 135 | user-select: none; 136 | height: fit-content; 137 | width: max-content; 138 | } 139 | 140 | div.location * { 141 | direction: ltr; 142 | } 143 | 144 | body:has(#tree-ul a[style*="text-decoration: none"][style*="cursor: default"]) 145 | div#loader-ellipsis-div { 146 | display: inline-block; 147 | } 148 | div#loader-ellipsis-div { 149 | display: none; 150 | position: fixed; 151 | top: 0vh; 152 | right: 9vw; 153 | z-index: 999999; 154 | } 155 | 156 | div#loader-ellipsis-div:before { 157 | content: ""; 158 | background-color: lavenderblush; 159 | height: 37px; 160 | position: fixed; 161 | top: 2vh; 162 | width: 62px; 163 | border-radius: 30px; 164 | } 165 | 166 | div#no-internet-div { 167 | background-color: lightcoral; 168 | position: fixed; 169 | top: 2vh; 170 | left: 50%; 171 | transform: translateX(-50%); 172 | border-radius: 10px; 173 | background-image: url(../asset/no-wifi.svg); 174 | background-repeat: no-repeat; 175 | background-position: 8px center; 176 | background-size: 30px; 177 | height: auto; 178 | align-items: center; 179 | width: max-content; 180 | padding: 8px; 181 | font-weight: bold; 182 | text-indent: 35px; 183 | } 184 | 185 | .footer > .made-with-love { 186 | border: solid; 187 | border-radius: 5px; 188 | border-color: cornflowerblue; 189 | outline: inherit; 190 | user-select: inherit; 191 | width: fit-content; 192 | margin: auto; 193 | /* margin-top: 2rem; */ 194 | padding: 5px; 195 | } 196 | 197 | body:has(#preview-div[hidden]) div.footer > .made-with-love { 198 | margin-top: 2rem; 199 | } 200 | 201 | body:has(.create-url:not([hidden])) div.footer { 202 | position: relative !important; 203 | margin: auto !important; 204 | padding: 10px !important; 205 | outline: none !important; 206 | user-select: none !important; 207 | height: fit-content !important; 208 | width: max-content !important; 209 | bottom: 0 !important; 210 | top: 0 !important; 211 | transform: initial !important; 212 | left: 0 !important; 213 | } 214 | 215 | body:has(.create-url:not([hidden])) div.footer > div { 216 | margin-top: 5px; 217 | } 218 | 219 | .create-url { 220 | width: 90vw; 221 | background-color: rgba(0, 0, 0, 0.2); 222 | position: relative; 223 | margin: auto; 224 | margin-top: 2rem; 225 | border-radius: 10px; 226 | backdrop-filter: blur(10px); 227 | border: 2px solid rgba(255, 255, 255, 0.1); 228 | box-shadow: 0 0 40px rgba(8, 7, 16, 0.6); 229 | padding: 50px 35px; 230 | padding-top: 0; 231 | padding-bottom: 70px; 232 | bottom: 0; 233 | user-select: none; 234 | } 235 | .create-url * { 236 | font-family: "Poppins", sans-serif; 237 | /* color: #ffffff; */ 238 | letter-spacing: 0.5px; 239 | outline: none; 240 | border: none; 241 | } 242 | .create-url h3 { 243 | font-size: xx-large; 244 | font-weight: bolder; 245 | line-height: 25px; 246 | text-align: center; 247 | } 248 | 249 | .create-url label { 250 | display: block; 251 | margin-top: 30px; 252 | font-size: 16px; 253 | font-weight: 500; 254 | } 255 | .create-url .input-field { 256 | display: flex; 257 | flex-direction: row; 258 | height: 50px; 259 | overflow: hidden; 260 | border-radius: 5px; 261 | } 262 | .create-url .input-field > input { 263 | height: inherit; 264 | flex-grow: 1; 265 | background-color: rgba(255, 255, 255, 0.65); 266 | border-radius: 3px; 267 | /* padding: 0 10px; */ 268 | font-size: 14px; 269 | font-weight: 300; 270 | text-align: center; 271 | text-overflow: ellipsis; 272 | padding-left: 10px; 273 | } 274 | .create-url .input-field > button { 275 | background-image: url("../asset/paste.svg"); 276 | background-size: 90% 90%; 277 | background-position: center center; 278 | background-repeat: no-repeat; 279 | width: 40px; 280 | cursor: pointer; 281 | } 282 | .create-url input::placeholder { 283 | color: gray; 284 | font-weight: bold; 285 | text-align: center; 286 | } 287 | .create-url input#token { 288 | font-size: x-small; 289 | font-weight: 200; 290 | } 291 | .create-url > button { 292 | margin-top: 20px; 293 | width: 100%; 294 | background-color: #ffffff; 295 | color: #080710; 296 | padding: 15px 0; 297 | font-size: 18px; 298 | font-weight: 600; 299 | border-radius: 5px; 300 | cursor: pointer; 301 | position: relative; 302 | left: 5px; 303 | } 304 | .create-url .created-url { 305 | position: absolute; 306 | bottom: 10px; 307 | left: 50%; 308 | transform: translateX(-50%); 309 | height: 40px; 310 | display: flex; 311 | flex-direction: row; 312 | width: calc(100% - 2 * 20px); 313 | border-radius: 10px; 314 | border: 1px solid rgba(0, 0, 0, 0.5); 315 | } 316 | .create-url .created-url > a { 317 | flex-grow: 1; 318 | border: inherit; 319 | border-top-left-radius: inherit; 320 | border-bottom-left-radius: inherit; 321 | overflow: hidden; 322 | text-overflow: ellipsis; 323 | user-select: initial; 324 | font-size: xx-small; 325 | padding: 4px; 326 | line-height: 10px; 327 | } 328 | .create-url .created-url > button { 329 | width: 80px; 330 | border: inherit; 331 | border-top-right-radius: inherit; 332 | border-bottom-right-radius: inherit; 333 | color: #080710; 334 | font-size: 18px; 335 | font-weight: 600; 336 | cursor: pointer; 337 | } 338 | 339 | div.download-logo { 340 | background-image: url(../asset/browser-download-symbolic.svg); 341 | background-position: center center; 342 | background-size: 20px 50px; 343 | background-repeat: no-repeat; 344 | height: 15px; 345 | width: 40px; 346 | border: 1px solid gray; 347 | border-radius: 5px; 348 | } 349 | 350 | html:has(head > style.darkreader) div.download-logo { 351 | background-image: url(../asset/browser-download-symbolic-dark.svg); 352 | } 353 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-color: #333; 3 | --link-color: #16b; 4 | --background-color: #fff; 5 | 6 | --border-color-hard: #e0e0e0; 7 | --border-color-soft: #f0f0f0; 8 | 9 | --markdown-line-color: #eee; 10 | --markdown-code-color: #f2f2f2; 11 | 12 | --header-color: #444; 13 | --header-background-color: #fafafa; 14 | 15 | --button-color: #000; 16 | --button-background-color: #eee; 17 | --button-border-color: #ddd; 18 | } 19 | 20 | html, 21 | body { 22 | min-height: 100vh; 23 | } 24 | 25 | body { 26 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 27 | Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 28 | margin-top: 0; 29 | margin-bottom: 0; 30 | color: var(--text-color); 31 | } 32 | 33 | input { 34 | color: var(--text-color); 35 | background: var(--background-color); 36 | border: 1px solid var(--border-color-hard); 37 | border-radius: 5px; 38 | } 39 | 40 | .container { 41 | position: relative; 42 | max-height: 100vh; 43 | overflow: auto; 44 | } 45 | 46 | .container > *:first-child { 47 | padding-top: 2rem; 48 | margin-top: 0; 49 | } 50 | 51 | .container > .space { 52 | padding-bottom: 3rem; 53 | } 54 | 55 | .tree { 56 | font-size: 0.9rem; 57 | border: 1px solid var(--border-color-hard); 58 | list-style-type: none; 59 | padding: 0; 60 | margin: 0; 61 | } 62 | 63 | .tree li { 64 | padding: 1em; 65 | } 66 | 67 | .tree .entry { 68 | position: relative; 69 | display: flex; 70 | border-top: 1px solid var(--border-color-soft); 71 | } 72 | 73 | .tree .entry a { 74 | color: var(--text-color); 75 | } 76 | 77 | .tree .entry a::before { 78 | content: ""; 79 | position: absolute; 80 | left: 0; 81 | top: 0; 82 | width: 100%; 83 | height: 100%; 84 | z-index: 1; 85 | } 86 | 87 | .tree .entry a { 88 | text-decoration: none; 89 | } 90 | .tree .entry a:hover { 91 | text-decoration: underline; 92 | text-underline-position: under; 93 | } 94 | 95 | article.readme, 96 | .tree, 97 | .blob-view, 98 | .location { 99 | border-radius: 5px; 100 | margin: 2em auto; 101 | margin-bottom: 1.2em; 102 | max-width: 95vw; 103 | } 104 | 105 | article.readme, 106 | .blob-view { 107 | border: 1px solid var(--border-color-hard); 108 | } 109 | 110 | .location { 111 | color: #aaa; 112 | } 113 | .location > * { 114 | margin: 0 0.1em; 115 | color: var(--text-color); 116 | } 117 | 118 | .location a { 119 | color: var(--link-color); 120 | text-decoration: none; 121 | } 122 | .location a:hover { 123 | text-decoration: underline; 124 | } 125 | 126 | .location *:nth-child(1) { 127 | font-weight: bold; 128 | filter: brightness(80%); 129 | } 130 | 131 | .header { 132 | display: flex; 133 | flex-direction: row; 134 | align-items: center; 135 | padding: 1em; 136 | font-size: 0.9rem; 137 | background-color: var(--header-background-color); 138 | color: var(--header-color); 139 | overflow-x: auto; 140 | } 141 | 142 | .header > div:first-child { 143 | flex-grow: 1; 144 | } 145 | 146 | .last { 147 | display: grid; 148 | grid-template-columns: 1fr; 149 | grid-gap: 4px; 150 | margin: -1em auto; 151 | margin-inline-start: 5px; 152 | } 153 | 154 | .btn { 155 | display: inline-block; 156 | font-size: 10pt; 157 | background-color: var(--button-background-color); 158 | border: 1px solid var(--button-border-color); 159 | text-decoration: none; 160 | color: var(--button-color); 161 | cursor: pointer; 162 | padding: 0.5em 1.5em; 163 | border-radius: 5px; 164 | min-width: 3em; 165 | text-align: center; 166 | 167 | -webkit-touch-callout: none; 168 | -webkit-user-select: none; 169 | -khtml-user-select: none; 170 | -moz-user-select: none; 171 | -ms-user-select: none; 172 | user-select: none; 173 | } 174 | 175 | .icon { 176 | margin: auto 1em auto 0; 177 | width: 16px; 178 | height: 16px; 179 | } 180 | 181 | .content { 182 | padding: 1em; 183 | border-top: 1px solid var(--border-color-soft); 184 | } 185 | 186 | .content .content-img { 187 | display: flex; 188 | justify-content: center; 189 | } 190 | .content .content-img img { 191 | max-width: 100%; 192 | } 193 | 194 | .footer { 195 | zoom: 1.5; 196 | bottom: 0; 197 | width: 100%; 198 | /* height: 3rem; */ 199 | 200 | margin: auto; 201 | text-align: center; 202 | font-size: 0.8rem; 203 | } 204 | 205 | .footer a { 206 | color: var(--link-color); 207 | } 208 | 209 | .markdown { 210 | font-size: 0.9rem; 211 | padding: 2em; 212 | } 213 | .markdown h1 { 214 | font-size: 1.5rem; 215 | margin: 1em 0; 216 | padding-bottom: 0.3em; 217 | border-bottom: 1px solid var(--markdown-line-color); 218 | } 219 | .markdown h2 { 220 | font-size: 1.3rem; 221 | margin: 1em 0; 222 | padding-bottom: 0.3em; 223 | border-bottom: 1px solid var(--markdown-line-color); 224 | } 225 | .markdown p { 226 | line-height: 1.5; 227 | } 228 | .markdown img { 229 | vertical-align: middle; 230 | max-width: 100%; 231 | } 232 | .markdown a { 233 | color: var(--link-color); 234 | text-decoration: none; 235 | } 236 | .markdown a:hover { 237 | text-decoration: underline; 238 | } 239 | 240 | .markdown pre { 241 | background-color: var(--background-color); 242 | font-size: 0.8rem; 243 | padding: 0.8em 1em; 244 | border: 1px solid var(--markdown-line-color); 245 | border-radius: 5px; 246 | } 247 | .markdown pre code { 248 | background-color: inherit; 249 | padding: 0; 250 | } 251 | .markdown code { 252 | background-color: var(--markdown-code-color); 253 | border-radius: 5px; 254 | padding: 2px 4px; 255 | } 256 | .markdown p code { 257 | font-size: 0.88em; 258 | } 259 | .markdown table { 260 | border-spacing: 0; 261 | border-collapse: collapse; 262 | } 263 | .markdown td, 264 | .markdown th { 265 | padding: 0.5em 1em; 266 | border: 1px solid var(--markdown-line-color); 267 | } 268 | .markdown br { 269 | display: none; 270 | } 271 | 272 | /* Spinner elipses */ 273 | 274 | .loader--ellipsis { 275 | display: inline-block; 276 | position: relative; 277 | height: 64px; 278 | width: 64px; 279 | } 280 | 281 | .loader--ellipsis div { 282 | position: absolute; 283 | top: 27px; 284 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 285 | border-radius: 50%; 286 | height: 11px; 287 | width: 11px; 288 | } 289 | 290 | .loader--ellipsis div:nth-child(1) { 291 | left: 6px; 292 | animation: loader--ellipsis1 0.6s infinite; 293 | } 294 | 295 | .loader--ellipsis div:nth-child(2) { 296 | left: 6px; 297 | animation: loader--ellipsis2 0.6s infinite; 298 | } 299 | 300 | .loader--ellipsis div:nth-child(3) { 301 | left: 26px; 302 | animation: loader--ellipsis2 0.6s infinite; 303 | } 304 | 305 | .loader--ellipsis div:nth-child(4) { 306 | left: 45px; 307 | animation: loader--ellipsis3 0.6s infinite; 308 | } 309 | 310 | @keyframes loader--ellipsis1 { 311 | 0% { 312 | transform: scale(0); 313 | } 314 | 315 | 100% { 316 | transform: scale(1); 317 | } 318 | } 319 | 320 | @keyframes loader--ellipsis3 { 321 | 0% { 322 | transform: scale(1); 323 | } 324 | 325 | 100% { 326 | transform: scale(0); 327 | } 328 | } 329 | 330 | @keyframes loader--ellipsis2 { 331 | 0% { 332 | transform: translate(0, 0); 333 | } 334 | 335 | 100% { 336 | transform: translate(19px, 0); 337 | } 338 | } 339 | 340 | .loader--spinner div:after, 341 | .loader--ellipsis div { 342 | background: crimson; 343 | } 344 | 345 | .loader--spinner { 346 | display: inline-block; 347 | position: relative; 348 | color: official; 349 | height: 60px; 350 | width: 60px; 351 | } 352 | 353 | .loader--spinner div { 354 | animation: loader--spinner 1.2s linear infinite; 355 | transform-origin: 30px 30px; 356 | } 357 | 358 | .loader--spinner div:after { 359 | display: block; 360 | position: absolute; 361 | top: 3px; 362 | left: 27px; 363 | border-radius: 20%; 364 | content: " "; 365 | height: 10px; 366 | width: 5px; 367 | } 368 | 369 | .loader--spinner div:nth-child(1) { 370 | animation-delay: -1.1s; 371 | transform: rotate(0deg); 372 | } 373 | 374 | .loader--spinner div:nth-child(2) { 375 | animation-delay: -1s; 376 | transform: rotate(30deg); 377 | } 378 | 379 | .loader--spinner div:nth-child(3) { 380 | animation-delay: -0.9s; 381 | transform: rotate(60deg); 382 | } 383 | 384 | .loader--spinner div:nth-child(4) { 385 | animation-delay: -0.8s; 386 | transform: rotate(90deg); 387 | } 388 | 389 | .loader--spinner div:nth-child(5) { 390 | animation-delay: -0.7s; 391 | transform: rotate(120deg); 392 | } 393 | 394 | .loader--spinner div:nth-child(6) { 395 | animation-delay: -0.6s; 396 | transform: rotate(150deg); 397 | } 398 | 399 | .loader--spinner div:nth-child(7) { 400 | animation-delay: -0.5s; 401 | transform: rotate(180deg); 402 | } 403 | 404 | .loader--spinner div:nth-child(8) { 405 | animation-delay: -0.4s; 406 | transform: rotate(210deg); 407 | } 408 | 409 | .loader--spinner div:nth-child(9) { 410 | animation-delay: -0.3s; 411 | transform: rotate(240deg); 412 | } 413 | 414 | .loader--spinner div:nth-child(10) { 415 | animation-delay: -0.2s; 416 | transform: rotate(270deg); 417 | } 418 | 419 | .loader--spinner div:nth-child(11) { 420 | animation-delay: -0.1s; 421 | transform: rotate(300deg); 422 | } 423 | 424 | .loader--spinner div:nth-child(12) { 425 | animation-delay: 0s; 426 | transform: rotate(330deg); 427 | } 428 | 429 | @keyframes loader--spinner { 430 | 0% { 431 | opacity: 1; 432 | } 433 | 434 | 100% { 435 | opacity: 0; 436 | } 437 | } 438 | 439 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | window.DEFAULT_API_HEADERS = { 2 | headers: { 3 | "X-GitHub-Api-Version": "2022-11-28", 4 | "Accept-Charset": "UTF-8", 5 | }, 6 | }; 7 | const NEW_LINE_EXP = /\n(?!$)/g; 8 | 9 | const rawBtn = document.getElementById("raw-btn"), 10 | previewDiv = document.getElementById("preview-div"), 11 | previewItemNameDiv = document.getElementById("preview-item-name-div"), 12 | previewItemContentDiv = document.getElementById("preview-item-content-div"), 13 | treeUl = document.getElementById("tree-ul"), 14 | cwdNameDiv = document.getElementById("cwd-name-div"), 15 | locationDiv = document.getElementById("location-div"), 16 | treeHeaderLast = document.querySelector(".tree > .header > .last"), 17 | noInternetDiv = document.getElementById("no-internet-div"), 18 | footerDiv = document.querySelector("div.footer"), 19 | mainLoaderSpinnerDiv = document.getElementById("main-loader-spinner-div"), 20 | createUrlDiv = document.getElementById("create-url"), 21 | aCreatedUrl = document.querySelector("#created-url > a"), 22 | btnCopyCreatedUrl = document.querySelector( 23 | 'button[name="btn-copy-created-url"]' 24 | ), 25 | inpToken = document.getElementById("token"), 26 | inpUsername = document.getElementById("owner"), 27 | inpRepo = document.getElementById("repo"), 28 | btnDownloadDir = document.getElementById("btnDownloadDir"), 29 | btnDownloadFile = document.getElementById("btnDownloadFile"); 30 | 31 | var onlineLock = {}; 32 | onlineLock.lock = () => { 33 | onlineLock.p = new Promise((r) => (onlineLock.unlock = r)); 34 | }; 35 | onlineLock.wait = () => onlineLock.p; 36 | 37 | window.addEventListener("online", () => { 38 | onlineLock.unlock(); 39 | noInternetDiv.hidden = true; 40 | }); 41 | 42 | window.addEventListener("offline", () => { 43 | noInternetDiv.hidden = false; 44 | }); 45 | 46 | if (!navigator.onLine) noInternetDiv.hidden = true; 47 | 48 | async function initMarkdownView(md) { 49 | var el = document.createElement("pre"); 50 | el.classList.add("markdown"); 51 | // console.log(md); 52 | el.innerHTML = DOMPurify.sanitize(await marked.parse(md)); 53 | el.querySelectorAll("[data-repoview-lazy-src]").forEach(async (el) => { 54 | // lazy-loading src 55 | const observer = new IntersectionObserver((entries) => { 56 | entries.forEach(async (entry) => { 57 | if (entry.isIntersecting) { 58 | observer.disconnect(); // only load src once 59 | // console.log(el); 60 | var obj = JSON.parse(el.getAttribute("data-repoview-lazy-src")); 61 | el.removeAttribute("data-repoview-lazy-src"); 62 | el.setAttribute( 63 | "src", 64 | resopnse2imgSrc( 65 | await octokit.request( 66 | "GET /repos/{owner}/{repo}/contents/{path}", 67 | { 68 | owner: obj.owner, 69 | repo: obj.repo, 70 | path: obj.path, 71 | headers: DEFAULT_API_HEADERS.headers, 72 | } 73 | ) 74 | ) 75 | ); 76 | } 77 | }); 78 | }); 79 | observer.observe(el); 80 | }); 81 | // :href 82 | el.querySelectorAll("a[data-repoview-href]").forEach((el) => { 83 | var path = el.getAttribute("data-repoview-href"); 84 | el.removeAttribute("data-repoview-href"); 85 | if (el.hasAttribute("href")) e.removeAttribute("href"); 86 | el.style.cursor = "pointer"; 87 | el.setAttribute("title", path); 88 | el.setAttribute("onclick", `gotoPath("${path}"); return false;`); 89 | }); 90 | // md links 91 | el.querySelectorAll('a[href^="data-repoview-href="]').forEach((el) => { 92 | var path = el.getAttribute("href").slice("data-repoview-href=".length); 93 | el.removeAttribute("href"); 94 | el.style.cursor = "pointer"; 95 | el.setAttribute("onclick", `gotoPath("${path}"); return false;`); 96 | }); 97 | /* open external links in new tab */ 98 | el.querySelectorAll("a").forEach((el) => { 99 | if (!el.hasAttribute("onclick")) { 100 | el.setAttribute("target", "_blank"); 101 | el.setAttribute("rel", "noreferrer"); 102 | } 103 | }); 104 | // el.innerHTML = (await octokit.request('POST /markdown', { 105 | // text: md, 106 | // headers: DEFAULT_API_HEADERS.headers 107 | // })).data; 108 | previewItemContentDiv.appendChild(el); 109 | } 110 | 111 | function decodeContent(str) { 112 | // Going backwards: from bytestream, to percent-encoding, to original string. 113 | return decodeURIComponent( 114 | atob(str) 115 | .split("") 116 | .map(function (c) { 117 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); 118 | }) 119 | .join("") 120 | ); 121 | } 122 | 123 | function updateHistory(path) { 124 | history.pushState( 125 | "", 126 | "", 127 | `?token=${window.tokenUrlParam}&owner=${window.owner}&repo=${window.repo}${ 128 | path ? "&path=" + path : "" 129 | }` 130 | ); 131 | } 132 | 133 | window.resopnse2imgSrc = (response) => { 134 | // extracts img-src from GitHub API octokit response 135 | // console.log(response); 136 | var download_url = response.data.download_url; 137 | if (download_url) { 138 | return download_url; 139 | } else if (response.data.content) { 140 | return `data:${mime.getType( 141 | response.data.name 142 | )};base64,${response.data.content.replaceAll("\n", "")}`; 143 | } else { 144 | return null; 145 | } 146 | }; 147 | 148 | window.gotoPath = async function ( 149 | path, 150 | boolUpdateHistory = true, 151 | disableAels = true 152 | ) { 153 | /* first of all: remove click event to prevent multiple concurrency calls to this methods */ 154 | if (disableAels) { 155 | Array.from(locationDiv.querySelectorAll("a")) 156 | .concat(Array.from(treeUl.querySelectorAll("tree-item a"))) 157 | .forEach((a) => { 158 | a.onclick = undefined; 159 | var style = a.style; 160 | style.textDecoration = "none"; 161 | // style.color = 'inherit'; 162 | style.cursor = "default"; 163 | }); 164 | } 165 | // .. 166 | if (path.endsWith("..")) { 167 | // go back 168 | path = path.split("/").slice(0, -2).join("/"); 169 | } 170 | if (boolUpdateHistory) updateHistory(path); 171 | var headerName = path ? path.split("/").at(-1) : window.repo; 172 | // get required path 173 | // 2 caces: requied path is a dir || file 174 | // if it is a dir: do dir view & view dir's README.md if exists 175 | // if it is a file: show content of this file 176 | try { 177 | var response = await octokit.request( 178 | "GET /repos/{owner}/{repo}/contents/{path}", 179 | { 180 | owner, 181 | repo, 182 | path, 183 | headers: DEFAULT_API_HEADERS.headers, 184 | } 185 | ); 186 | } catch (err) { 187 | if (err.response.status === 404) { 188 | // if path is invalid or repo is invalid => panic:- 189 | // owner, repo or path is invalid => panic 190 | showErrorMsg("[404]: owner, repo or path is invalid"); 191 | return; 192 | } 193 | } 194 | treeUl.querySelectorAll("tree-item").forEach((el) => el.remove()); 195 | if (response.data.length) { 196 | /* path is a dir */ 197 | previewDiv.hidden = true; 198 | cwdNameDiv.innerHTML = headerName; 199 | treeUl.hidden = false; 200 | // set treeUl items 201 | if (path != "") { 202 | var treeItem = document.createElement("tree-item"); 203 | treeItem.setAttribute("data-item-name", ".."); 204 | treeItem.setAttribute("data-item-path", path + "/.."); 205 | treeItem.setAttribute("data-item-type", "dir"); 206 | treeUl.appendChild(treeItem); 207 | } 208 | var dirItems = [], 209 | fileItems = []; 210 | response.data.forEach((item) => { 211 | item.type === "dir" ? dirItems.push(item) : fileItems.push(item); 212 | }); 213 | dirItems.concat(fileItems).forEach((item) => { 214 | var treeItem = document.createElement("tree-item"); 215 | treeItem.setAttribute("data-item-name", item.name); 216 | treeItem.setAttribute("data-item-path", item.path); 217 | treeItem.setAttribute("data-item-type", item.type); 218 | treeUl.appendChild(treeItem); 219 | }); 220 | var readMeExists = 221 | treeUl.querySelector('[data-item-name*="readme"]') || 222 | treeUl.querySelector('[data-item-name*="README"]'); 223 | if (readMeExists) { 224 | try { 225 | response = await octokit 226 | .request("GET /repos/{owner}/{repo}/readme/{dir}", { 227 | owner, 228 | repo, 229 | dir: path, 230 | headers: DEFAULT_API_HEADERS.headers, 231 | }) 232 | .catch((e) => {}); 233 | previewItemContentDiv.innerHTML = ""; 234 | previewDiv.hidden = false; 235 | previewItemNameDiv.innerHTML = response.data.name; 236 | // set btn-download-file 237 | btnDownloadFile.setAttribute( 238 | "onclick", 239 | `downloadFile('${response.data.name}', '${response.data.download_url}')` 240 | ); 241 | // set Raw url 242 | rawBtn.setAttribute("href", response.data.download_url); 243 | initMarkdownView(decodeContent(response.data.content)); 244 | } catch (err) { 245 | if (err.response.status === 404 || err.status === 404) { 246 | // no README.md, this is ok 247 | } else { 248 | throw err; 249 | } 250 | } 251 | } 252 | } else { 253 | /* path is a file */ 254 | treeUl.hidden = true; 255 | previewItemNameDiv.innerHTML = headerName; 256 | previewItemContentDiv.innerHTML = ""; 257 | var m = String(mime.getType(path)); // m.slice(0, m.indexOf('/')) 258 | if (m.startsWith("image/")) { 259 | // console.log(response); 260 | // img render 261 | previewItemContentDiv.innerHTML = ` 262 |