Merge branch 'v1.1'

This commit is contained in:
StormyCloud
2025-06-24 16:24:37 -05:00
13 changed files with 1654 additions and 1370 deletions

5
.env
View File

@ -1,5 +0,0 @@
I2PCAKE_SECRET_KEY='3de1d3c8c7c7d41f77e68e4c77609d57'
I2PCAKE_ADMIN_PASSWORD='z^K4@qR9#pGv$L!w'
I2PCAKE_ENCRYPTION_KEY='bZgG05n1hVDFs3qc0N2n-HqH8J1nZ_0mY7yK6xX5_wI='
RATELIMIT_STORAGE_URI=redis://localhost:6379

11
.gitignore vendored
View File

@ -1,3 +1,14 @@
uploads/ uploads/
database.db database.db
.DS_Store .DS_Store
upload*
*.db
.env
wsgi.py
# Ignore Python cache files
__pycache__/
*.py[cod]
# Ignore virtual environment directories
venv/

View File

@ -1,2 +1,2 @@
# i2pcake # i2pcake

1124
app.py

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
/* /*
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com ! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
*/ */
/* 1. Preflight (Tailwind's base styles) */ /* 1. Preflight (Tailwind's base styles) */
*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::before,::after{--tw-content:''}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none} *,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::before,::after{--tw-content:''}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}
/* 2. Utility Classes */ /* 2. Utility Classes */
*,::before,::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: } *,::before,::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }
.h-6{height:1.5rem}.h-12{height:3rem}.h-20{height:5rem}.h-24{height:6rem}.w-6{width:1.5rem}.w-12{width:3rem}.w-20{width:5rem}.w-24{width:6rem}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-w:56rem}.flex-shrink-0{flex-shrink:0}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-16{margin-top:4rem}.inline-block{display:inline-block}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.list-disc{list-style-type:disc}.list-inside{list-style-position:inside}.min-h-screen{min-height:100vh}.max-h-60vh{max-height:60vh}.object-contain{object-fit:contain}.justify-center{justify-content:center}.items-center{align-items:center}.gap-8{gap:2rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.overflow-x-auto{overflow-x:auto}.overflow-hidden{overflow:hidden}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-6{padding-top:1.5rem}.text-center{text-align:center}.font-sans{font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}.text-xs{font-size:.75rem;line-height:1rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.font-mono{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}.text-white{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253 / var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128 / var(--tw-text-opacity))}.shadow-lg{--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.ring-blue-500{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246 / var(--tw-ring-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:text-blue-300:hover{--tw-text-opacity:1;color:rgb(147 197 253 / var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:shadow-outline:focus{box-shadow:0 0 0 3px rgb(96 165 250 / .5);outline:none}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246 / var(--tw-ring-opacity))}@media (min-width: 768px){.md\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}} .h-6{height:1.5rem}.h-12{height:3rem}.h-20{height:5rem}.h-24{height:6rem}.w-6{width:1.5rem}.w-12{width:3rem}.w-20{width:5rem}.w-24{width:6rem}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-w:56rem}.flex-shrink-0{flex-shrink:0}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-16{margin-top:4rem}.inline-block{display:inline-block}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.list-disc{list-style-type:disc}.list-inside{list-style-position:inside}.min-h-screen{min-height:100vh}.max-h-60vh{max-height:60vh}.object-contain{object-fit:contain}.justify-center{justify-content:center}.items-center{align-items:center}.gap-8{gap:2rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.overflow-x-auto{overflow-x:auto}.overflow-hidden{overflow:hidden}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-6{padding-top:1.5rem}.text-center{text-align:center}.font-sans{font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}.text-xs{font-size:.75rem;line-height:1rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.font-mono{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}.text-white{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253 / var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128 / var(--tw-text-opacity))}.shadow-lg{--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.ring-blue-500{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246 / var(--tw-ring-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:text-blue-300:hover{--tw-text-opacity:1;color:rgb(147 197 253 / var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:shadow-outline:focus{box-shadow:0 0 0 3px rgb(96 165 250 / .5);outline:none}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246 / var(--tw-ring-opacity))}@media (min-width: 768px){.md\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,73 +1,73 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg <svg
version="1.1" version="1.1"
id="Layer_1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" x="0px" y="0px"
viewBox="0 135 1500 636" viewBox="0 135 1500 636"
style="enable-background:new 0 135 1500 636;" style="enable-background:new 0 135 1500 636;"
xml:space="preserve" xml:space="preserve"
> >
<style type="text/css"> <style type="text/css">
.st0{fill:#FFFFFF;} .st0{fill:#FFFFFF;}
.st1{fill:url(#SVGID_1_);} .st1{fill:url(#SVGID_1_);}
.st2{fill:url(#SVGID_2_);} .st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);} .st3{fill:url(#SVGID_3_);}
.st4{fill:url(#SVGID_4_);} .st4{fill:url(#SVGID_4_);}
.st5{fill:url(#SVGID_5_);} .st5{fill:url(#SVGID_5_);}
.st6{fill:#006BCC;} .st6{fill:#006BCC;}
</style> </style>
<g> <g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="378.2952" y1="456.0995" x2="378.2952" y2="131.6513"> <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="378.2952" y1="456.0995" x2="378.2952" y2="131.6513">
<stop offset="0" style="stop-color:#006FCD"/> <stop offset="0" style="stop-color:#006FCD"/>
<stop offset="1" style="stop-color:#00A9F2"/> <stop offset="1" style="stop-color:#00A9F2"/>
</linearGradient> </linearGradient>
<path class="st1" d="M600.3,324.7c-11.5-34.2-45.1-41.5-45.1-41.5c-13.7-62-73.4-64.4-73.4-64.4c-31.4-91.3-119.5-87.1-119.5-87.1 <path class="st1" d="M600.3,324.7c-11.5-34.2-45.1-41.5-45.1-41.5c-13.7-62-73.4-64.4-73.4-64.4c-31.4-91.3-119.5-87.1-119.5-87.1
C260.2,135,242.6,223,242.6,223c-92.5,15.4-90.1,94.4-90.1,94.4c3.5,93,81.8,100.3,81.8,100.3h33.5c1,18.1,8.6,27.7,15.9,32.8 C260.2,135,242.6,223,242.6,223c-92.5,15.4-90.1,94.4-90.1,94.4c3.5,93,81.8,100.3,81.8,100.3h33.5c1,18.1,8.6,27.7,15.9,32.8
c5.4,3.8,11.9,5.6,18.5,5.6h162c3.5,0,7-0.3,10.4-1.2c27.6-7.2,26.7-37.2,26.7-37.2h39.4c24.4-3.3,39.5-14.9,48.7-27 c5.4,3.8,11.9,5.6,18.5,5.6h162c3.5,0,7-0.3,10.4-1.2c27.6-7.2,26.7-37.2,26.7-37.2h39.4c24.4-3.3,39.5-14.9,48.7-27
C604,372,607.9,347.1,600.3,324.7z M489.4,415.4c0,15.4-12.5,27.8-27.8,27.8H304.2c-12.1,0-21.9-9.8-21.9-21.9V228.7 C604,372,607.9,347.1,600.3,324.7z M489.4,415.4c0,15.4-12.5,27.8-27.8,27.8H304.2c-12.1,0-21.9-9.8-21.9-21.9V228.7
c0-15.5,12.6-28.1,28.1-28.1H405c9.8,0,19.2,3.7,26.4,10.4l43.8,40.9c9.1,8.5,14.3,20.4,14.3,32.8V415.4z"/> c0-15.5,12.6-28.1,28.1-28.1H405c9.8,0,19.2,3.7,26.4,10.4l43.8,40.9c9.1,8.5,14.3,20.4,14.3,32.8V415.4z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="432.1597" y1="283.199" x2="432.1597" y2="227.7854"> <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="432.1597" y1="283.199" x2="432.1597" y2="227.7854">
<stop offset="0" style="stop-color:#008CE2"/> <stop offset="0" style="stop-color:#008CE2"/>
<stop offset="0.9017" style="stop-color:#0098E8"/> <stop offset="0.9017" style="stop-color:#0098E8"/>
</linearGradient> </linearGradient>
<path class="st2" d="M414.1,229.4l43.6,40.8c5,4.7,1.7,13.1-5.2,13.1h-39.4c-4.9,0-8.9-4-8.9-8.9v-40.6 <path class="st2" d="M414.1,229.4l43.6,40.8c5,4.7,1.7,13.1-5.2,13.1h-39.4c-4.9,0-8.9-4-8.9-8.9v-40.6
C404.2,228.5,410.4,225.9,414.1,229.4z"/> C404.2,228.5,410.4,225.9,414.1,229.4z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="380.3181" y1="417.6981" x2="380.3181" y2="294.9461"> <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="380.3181" y1="417.6981" x2="380.3181" y2="294.9461">
<stop offset="0" style="stop-color:#006FCE"/> <stop offset="0" style="stop-color:#006FCE"/>
<stop offset="0.9017" style="stop-color:#0082D9"/> <stop offset="0.9017" style="stop-color:#0082D9"/>
</linearGradient> </linearGradient>
<path class="st3" d="M425.9,376.3L385,415.6c-1.4,1.3-3.2,2.1-5.2,2.1c-2,0-3.9-0.8-5.3-2.2l-39.9-40.1c-1.8-1.8-2.6-4.1-2.6-6.4 <path class="st3" d="M425.9,376.3L385,415.6c-1.4,1.3-3.2,2.1-5.2,2.1c-2,0-3.9-0.8-5.3-2.2l-39.9-40.1c-1.8-1.8-2.6-4.1-2.6-6.4
c0-2.7,1.2-5.4,3.5-7.2c3.7-2.9,8.9-2.5,12.2,0.9l23.6,24.6v-82.7c0-5.3,4.3-9.6,9.6-9.6h0c5.3,0,9.6,4.3,9.6,9.6v80.6l23-22 c0-2.7,1.2-5.4,3.5-7.2c3.7-2.9,8.9-2.5,12.2,0.9l23.6,24.6v-82.7c0-5.3,4.3-9.6,9.6-9.6h0c5.3,0,9.6,4.3,9.6,9.6v80.6l23-22
c1.8-1.7,4-2.5,6.3-2.5c2.5,0,5,1,6.8,3C429.6,367.4,429.4,372.9,425.9,376.3z"/> c1.8-1.7,4-2.5,6.3-2.5c2.5,0,5,1,6.8,3C429.6,367.4,429.4,372.9,425.9,376.3z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="490.3123" y1="553.3344" x2="490.3123" y2="461.3674"> <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="490.3123" y1="553.3344" x2="490.3123" y2="461.3674">
<stop offset="4.469245e-02" style="stop-color:#007BD8"/> <stop offset="4.469245e-02" style="stop-color:#007BD8"/>
<stop offset="1" style="stop-color:#007DD8"/> <stop offset="1" style="stop-color:#007DD8"/>
</linearGradient> </linearGradient>
<path class="st4" d="M468.5,520.7l34.1-37.2c0.6-0.7,1.4-1.3,2.2-1.8c5.7-3.4,29.9-17.6,39.9-20.4c0,0-50,86.7-108.6,92 <path class="st4" d="M468.5,520.7l34.1-37.2c0.6-0.7,1.4-1.3,2.2-1.8c5.7-3.4,29.9-17.6,39.9-20.4c0,0-50,86.7-108.6,92
c0,0,21.3-5,30.6-29.4C467,522.7,467.7,521.6,468.5,520.7z"/> c0,0,21.3-5,30.6-29.4C467,522.7,467.7,521.6,468.5,520.7z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="327.0562" y1="771.1231" x2="327.0562" y2="426.3489"> <linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="327.0562" y1="771.1231" x2="327.0562" y2="426.3489">
<stop offset="0.1117" style="stop-color:#0067C7"/> <stop offset="0.1117" style="stop-color:#0067C7"/>
<stop offset="1" style="stop-color:#0082DA"/> <stop offset="1" style="stop-color:#0082DA"/>
</linearGradient> </linearGradient>
<path class="st5" d="M40.5,692.5c0,0,89.9,15,139.8,78.6c0,0,22.9-70.8,57.5-100.4c1.5-1.3,2.9-2.6,4.3-3.9 <path class="st5" d="M40.5,692.5c0,0,89.9,15,139.8,78.6c0,0,22.9-70.8,57.5-100.4c1.5-1.3,2.9-2.6,4.3-3.9
c5.8-5.6,27.6-22,79.3-17.5c0,0,121.1,15.3,198.9-60c2.6-2.6,5.4-5.1,8.1-7.5c9.3-8.3,33.6-32.7,54.2-76.2l29.8-61 c5.8-5.6,27.6-22,79.3-17.5c0,0,121.1,15.3,198.9-60c2.6-2.6,5.4-5.1,8.1-7.5c9.3-8.3,33.6-32.7,54.2-76.2l29.8-61
c2.6-5.3,0.8-11.7-4.2-14.9l0,0c-7.5-4.8-17.1-4.6-24.3,0.4c-8.1,5.6-19.1,16.2-32.6,36.3c-1.8,2.7-3.6,5.5-5.2,8.4 c2.6-5.3,0.8-11.7-4.2-14.9l0,0c-7.5-4.8-17.1-4.6-24.3,0.4c-8.1,5.6-19.1,16.2-32.6,36.3c-1.8,2.7-3.6,5.5-5.2,8.4
c-7.3,12.4-35.7,57.5-71.6,76.5c0,0-54.2,34-162.2-0.4H390c0,0,37.9,1.6,54.4-15.1c11-11,6.6-29.5-8.3-33.9 c-7.3,12.4-35.7,57.5-71.6,76.5c0,0-54.2,34-162.2-0.4H390c0,0,37.9,1.6,54.4-15.1c11-11,6.6-29.5-8.3-33.9
c-3-0.9-6.5-1.5-10.7-1.6l-55.7,0.9c-13.6,0.2-27.2-1.8-40-6.4c-2.5-0.9-5-1.9-7.4-3c-5.4-2.5-10.9-4.7-16.6-6.3 c-3-0.9-6.5-1.5-10.7-1.6l-55.7,0.9c-13.6,0.2-27.2-1.8-40-6.4c-2.5-0.9-5-1.9-7.4-3c-5.4-2.5-10.9-4.7-16.6-6.3
c-29-8.5-104.6-22.6-157.3,39.4c0,0-22.8,27.8-39.6,75.3C108.8,600.3,85.4,664.8,40.5,692.5z"/> c-29-8.5-104.6-22.6-157.3,39.4c0,0-22.8,27.8-39.6,75.3C108.8,600.3,85.4,664.8,40.5,692.5z"/>
<path class="st6" d="M758.1,315h-90.7v270H737c3.1,0,6.3,0,9.4,0.1c19.9,0.3,139.1-3.4,139.1-141.8 <path class="st6" d="M758.1,315h-90.7v270H737c3.1,0,6.3,0,9.4,0.1c19.9,0.3,139.1-3.4,139.1-141.8
C885.5,443.2,890.6,317.9,758.1,315z M827.7,461.1c0,41.1-33.3,74.5-74.5,74.5h-29.3V361.8h29.3c41.1,0,74.5,33.3,74.5,74.5V461.1z"/> C885.5,443.2,890.6,317.9,758.1,315z M827.7,461.1c0,41.1-33.3,74.5-74.5,74.5h-29.3V361.8h29.3c41.1,0,74.5,33.3,74.5,74.5V461.1z"/>
<path class="st6" d="M911.9,384.8v198.4h54.3v-98.1c0-11,2.8-21.9,8.6-31.3c8.2-13.3,23.6-26.4,51.6-19.3l8.5-49.7 <path class="st6" d="M911.9,384.8v198.4h54.3v-98.1c0-11,2.8-21.9,8.6-31.3c8.2-13.3,23.6-26.4,51.6-19.3l8.5-49.7
c0,0-44.1-15.5-68.7,29.7v-29.7H911.9z"/> c0,0-44.1-15.5-68.7,29.7v-29.7H911.9z"/>
<path class="st6" d="M1152.2,381.1c-8.3-1.3-16.7-1.4-24.9-0.4c-86.2,10.5-87.8,93-87.8,93c-3.4,117.7,94.8,115,94.8,115 <path class="st6" d="M1152.2,381.1c-8.3-1.3-16.7-1.4-24.9-0.4c-86.2,10.5-87.8,93-87.8,93c-3.4,117.7,94.8,115,94.8,115
c101.9-0.6,101.9-96.1,101.9-96.1C1240,405.4,1180.4,385.6,1152.2,381.1z M1137.4,545.1c-24.9,0-45.1-27.2-45.1-60.6 c101.9-0.6,101.9-96.1,101.9-96.1C1240,405.4,1180.4,385.6,1152.2,381.1z M1137.4,545.1c-24.9,0-45.1-27.2-45.1-60.6
c0-33.5,20.2-60.6,45.1-60.6s45.1,27.1,45.1,60.6C1182.5,518,1162.3,545.1,1137.4,545.1z"/> c0-33.5,20.2-60.6,45.1-60.6s45.1,27.1,45.1,60.6C1182.5,518,1162.3,545.1,1137.4,545.1z"/>
<path class="st6" d="M1441.8,431.3c0,0-12.4-40.5-60.7-49.6c-11.9-2.2-24.2-1.7-35.8,2c-10.5,3.3-23.4,9.6-33.8,21.3l-0.3-20.6 <path class="st6" d="M1441.8,431.3c0,0-12.4-40.5-60.7-49.6c-11.9-2.2-24.2-1.7-35.8,2c-10.5,3.3-23.4,9.6-33.8,21.3l-0.3-20.6
l-52.2,0.6l-0.6,262.3h53.5v-83.5c0,0,45.6,50.3,104.7,6.3C1416.5,570.2,1472.8,530.3,1441.8,431.3z M1363.4,543.6 l-52.2,0.6l-0.6,262.3h53.5v-83.5c0,0,45.6,50.3,104.7,6.3C1416.5,570.2,1472.8,530.3,1441.8,431.3z M1363.4,543.6
c-20.6,3.3-33.6-5.1-41.6-14.8c-7.7-9.3-11.5-21.2-11.5-33.2V470c0-7.3,1.4-14.5,4.3-21.1c12.1-27.1,35.9-25.1,35.9-25.1 c-20.6,3.3-33.6-5.1-41.6-14.8c-7.7-9.3-11.5-21.2-11.5-33.2V470c0-7.3,1.4-14.5,4.3-21.1c12.1-27.1,35.9-25.1,35.9-25.1
c43.7-2.7,47.5,48.3,47.5,48.3C1403.6,537.3,1363.4,543.6,1363.4,543.6z"/> c43.7-2.7,47.5,48.3,47.5,48.3C1403.6,537.3,1363.4,543.6,1363.4,543.6z"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,150 +1,149 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - I2P Secure Share</title> <title>Admin Dashboard - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}">
<style> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
body { background-color: #1a202c; color: #cbd5e0; } <style>
.content-container { background-color: #2d3748; border: 1px solid #4a5568; } body { background-color: #1a202c; color: #cbd5e0; }
.btn { background-color: #4299e1; transition: background-color 0.3s ease; } .content-container { background-color: #2d3748; border: 1px solid #4a5568; }
.btn:hover { background-color: #3182ce; } .btn { background-color: #4299e1; transition: background-color 0.3s ease; }
.btn-danger { background-color: #e53e3e; } .btn:hover { background-color: #3182ce; }
.btn-danger:hover { background-color: #c53030; } .btn-danger { background-color: #e53e3e; }
input { background-color: #4a5568; border: 1px solid #718096; } .btn-danger:hover { background-color: #c53030; }
table { width: 100%; border-collapse: collapse; } input { background-color: #4a5568; border: 1px solid #718096; }
th, td { border: 1px solid #4a5568; padding: 0.75rem; text-align: left; } table { width: 100%; border-collapse: collapse; }
th { background-color: #1a202c; } th, td { border: 1px solid #4a5568; padding: 0.75rem; text-align: left; }
tr:nth-child(even) { background-color: #2d3748; } th { background-color: #1a202c; }
.alert-error { background-color: #e53e3e; } tr:nth-child(even) { background-color: #2d3748; }
.announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; } .alert-error { background-color: #e53e3e; }
</style> .announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; }
</head> </style>
<body class="font-sans"> </head>
<body class="font-sans">
{% if announcement_enabled and announcement_message %}
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg"> {% if announcement_enabled and announcement_message %}
<span>{{ announcement_message }}</span> <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button> <span>{{ announcement_message }}</span>
</div> <button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
{% endif %} </div>
{% endif %}
<div class="flex items-center justify-center min-h-screen py-8">
<div class="w-full max-w-4xl mx-auto p-4"> <div class="flex items-center justify-center min-h-screen py-8">
<header class="text-center mb-8"> <a href="/" class="inline-block mb-4">
<a href="/" class="inline-block mb-4"> <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
<img src="{{ url_for('static', filename='images/stormycloud.svg') }}" alt="StormyCloud Logo" style="width: 550px; max-width: 100%;" class="mx-auto"> </a>
</a> <h1 class="text-4xl font-bold text-white">Admin Dashboard</h1>
<h1 class="text-4xl font-bold text-white">Admin Dashboard</h1> </header>
</header>
<main class="content-container rounded-lg p-8 shadow-lg">
<main class="content-container rounded-lg p-8 shadow-lg"> {% if auth_success %}
{% if auth_success %} <h2 class="text-2xl font-semibold text-white mb-6">Active Images</h2>
<h2 class="text-2xl font-semibold text-white mb-6">Active Images</h2> {% if images %}
{% if images %} <div class="overflow-x-auto">
<div class="overflow-x-auto"> <table>
<table> <thead>
<thead> <tr>
<tr> <th>Filename</th>
<th>Filename</th> <th>Expires On (UTC)</th>
<th>Expires On (UTC)</th> <th>Time Left</th>
<th>Time Left</th> <th>Action</th>
<th>Action</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> {% for image in images %}
{% for image in images %} <tr>
<tr> <td class="font-mono text-sm break-all">{{ image[0] }}</td>
<td class="font-mono text-sm break-all">{{ image[0] }}</td> <td class="font-mono text-sm">{{ image[1] }}</td>
<td class="font-mono text-sm">{{ image[1] }}</td> <td class="font-mono text-sm">{{ image[2] }}</td>
<td class="font-mono text-sm">{{ image[2] }}</td> <td>
<td> <form action="{{ url_for('delete_image', filename=image[0]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this image?');">
<form action="{{ url_for('delete_image', filename=image[0]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this image?');"> <button type="submit" class="btn-danger text-white font-bold py-1 px-3 rounded text-sm">Delete</button>
<button type="submit" class="btn-danger text-white font-bold py-1 px-3 rounded text-sm">Delete</button> </form>
</form> </td>
</td> </tr>
</tr> {% endfor %}
{% endfor %} </tbody>
</tbody> </table>
</table> </div>
</div> {% else %}
{% else %} <p class="text-gray-400">No active images.</p>
<p class="text-gray-400">No active images.</p> {% endif %}
{% endif %}
<h2 class="text-2xl font-semibold text-white mt-12 mb-6">Active Pastes</h2>
<h2 class="text-2xl font-semibold text-white mt-12 mb-6">Active Pastes</h2> {% if pastes %}
{% if pastes %} <div class="overflow-x-auto">
<div class="overflow-x-auto"> <table>
<table> <thead>
<thead> <tr>
<tr> <th>ID</th>
<th>ID</th> <th>Language</th>
<th>Language</th> <th>Expires On (UTC)</th>
<th>Expires On (UTC)</th> <th>Time Left</th>
<th>Time Left</th> <th>Action</th>
<th>Action</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> {% for paste in pastes %}
{% for paste in pastes %} <tr>
<tr> <td class="font-mono text-sm break-all">{{ paste[0] }}</td>
<td class="font-mono text-sm break-all">{{ paste[0] }}</td> <td class="font-mono text-sm">{{ paste[1] }}</td>
<td class="font-mono text-sm">{{ paste[1] }}</td> <td class="font-mono text-sm">{{ paste[2] }}</td>
<td class="font-mono text-sm">{{ paste[2] }}</td> <td class="font-mono text-sm">{{ paste[3] }}</td>
<td class="font-mono text-sm">{{ paste[3] }}</td> <td>
<td> <form action="{{ url_for('delete_paste', paste_id=paste[0]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this paste?');">
<form action="{{ url_for('delete_paste', paste_id=paste[0]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this paste?');"> <button type="submit" class="btn-danger text-white font-bold py-1 px-3 rounded text-sm">Delete</button>
<button type="submit" class="btn-danger text-white font-bold py-1 px-3 rounded text-sm">Delete</button> </form>
</form> </td>
</td> </tr>
</tr> {% endfor %}
{% endfor %} </tbody>
</tbody> </table>
</table> </div>
</div> {% else %}
{% else %} <p class="text-gray-400">No active pastes.</p>
<p class="text-gray-400">No active pastes.</p> {% endif %}
{% endif %}
{% else %}
{% else %} <h2 class="text-2xl font-semibold text-white mb-6">Admin Login</h2>
<h2 class="text-2xl font-semibold text-white mb-6">Admin Login</h2> {% with messages = get_flashed_messages(with_categories=true) %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
{% if messages %} <div class="mb-4">
<div class="mb-4"> {% for category, message in messages %}
{% for category, message in messages %} <div class="alert-{{ category }} text-white p-3 rounded-md shadow-lg" role="alert">
<div class="alert-{{ category }} text-white p-3 rounded-md shadow-lg" role="alert"> {{ message }}
{{ message }} </div>
</div> {% endfor %}
{% endfor %} </div>
</div> {% endif %}
{% endif %} {% endwith %}
{% endwith %} <form method="POST" action="{{ url_for('admin_dashboard') }}">
<form method="POST" action="{{ url_for('admin_dashboard') }}"> <div class="mb-6">
<div class="mb-6"> <label for="password" class="block text-gray-300 text-sm font-bold mb-2">Password:</label>
<label for="password" class="block text-gray-300 text-sm font-bold mb-2">Password:</label> <input type="password" name="password" id="password" class="w-full p-2 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500" required>
<input type="password" name="password" id="password" class="w-full p-2 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500" required> </div>
</div> <div><button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Login</button></div>
<div><button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Login</button></div> </form>
</form> {% endif %}
{% endif %} <div class="text-center mt-8 border-t border-gray-700 pt-6">
<div class="text-center mt-8 border-t border-gray-700 pt-6"> <a href="{{ url_for('index') }}" class="text-blue-400 hover:text-blue-300">Back to Homepage</a>
<a href="{{ url_for('index') }}" class="text-blue-400 hover:text-blue-300">Back to Homepage</a> </div>
</div> </main>
</main> <footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> <a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> </footer>
</footer> </div>
</div> </div>
</div> <script>
<script> const announcementBar = document.getElementById('announcement-bar');
const announcementBar = document.getElementById('announcement-bar'); const closeButton = document.getElementById('close-announcement');
const closeButton = document.getElementById('close-announcement'); if (closeButton) {
if (closeButton) { closeButton.addEventListener('click', () => {
closeButton.addEventListener('click', () => { announcementBar.style.display = 'none';
announcementBar.style.display = 'none'; });
}); }
} </script>
</script> </body>
</body>
</html> </html>

View File

@ -1,64 +1,66 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support the Project - I2P Secure Share</title> <title>Support the Project - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"> <link rel="stylesheet" href="/static/css/tailwind.css">
<style> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border: 1px solid #4a5568; } <style>
.announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; } body { background-color: #1a202c; color: #cbd5e0; }
.address-box { background-color: #1a202c; padding: 1rem; border-radius: 0.5rem; word-break: break-all; } .content-container { background-color: #2d3748; border: 1px solid #4a5568; }
</style> .announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; }
</head> .address-box { background-color: #1a202c; padding: 1rem; border-radius: 0.5rem; word-break: break-all; }
<body class="font-sans"> </style>
</head>
{% if announcement_enabled and announcement_message %} <body class="font-sans">
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span> {% if announcement_enabled and announcement_message %}
<button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button> <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
</div> <span>{{ announcement_message }}</span>
{% endif %} <button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
</div>
<div class="flex items-center justify-center min-h-screen py-8"> {% endif %}
<div class="w-full max-w-2xl mx-auto p-4">
<header class="text-center mb-8"> <div class="flex items-center justify-center min-h-screen py-8">
<a href="/" class="inline-block mb-4"> <div class="w-full max-w-2xl mx-auto p-4">
<img src="{{ url_for('static', filename='images/stormycloud.svg') }}" alt="StormyCloud Logo" style="width: 550px; max-width: 100%;" class="mx-auto"> <header class="text-center mb-8">
</a> <a href="/" class="inline-block mb-4">
<h1 class="text-3xl font-bold text-white">Support the Service</h1> <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
</header> </a>
<h1 class="text-3xl font-bold text-white">Support the Service</h1>
<main class="content-container rounded-lg p-8 shadow-lg"> </header>
<p class="text-center text-gray-400 mb-6">This service is developed and maintained for the I2P community free of charge. If you find it valuable, please consider a small donation to help cover server costs and support future development.</p>
<main class="content-container rounded-lg p-8 shadow-lg">
<div class="text-center"> <p class="text-center text-gray-400 mb-6">This service is developed and maintained for the I2P community free of charge. If you find it valuable, please consider a small donation to help cover server costs and support future development.</p>
<h2 class="text-2xl font-semibold text-white mb-2">Monero (XMR)</h2>
<div class="address-box font-mono text-sm"> <div class="text-center">
45Gtj5tkhs4EsbnV7kkhMCRpbZUdqCQqR5qmLFVLAvbFCYaPL4pFbBkEBLJ7beHqkiJxdTBkPwFsT5EMu5jDrYBHPjQzPuv <h2 class="text-2xl font-semibold text-white mb-2">Monero (XMR)</h2>
</div> <div class="address-box font-mono text-sm">
</div> 45Gtj5tkhs4EsbnV7kkhMCRpbZUdqCQqR5qmLFVLAvbFCYaPL4pFbBkEBLJ7beHqkiJxdTBkPwFsT5EMu5jDrYBHPjQzPuv
</div>
<div class="text-center mt-12 border-t border-gray-700 pt-6"> </div>
<a href="{{ url_for('index') }}" class="text-blue-400 hover:text-blue-300">Back to Uploader</a>
</div> <div class="text-center mt-12 border-t border-gray-700 pt-6">
</main> <a href="/" class="text-blue-400 hover:text-blue-300">Back to Uploader</a>
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> </div>
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> </main>
<span class="mx-2">|</span> <footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
<a href="{{ url_for('donate_page') }}" class="hover:text-gray-400">Donate</a> <a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
</footer> <span class="mx-2">|</span>
</div> <a href="/donate" class="hover:text-gray-400">Donate</a>
</div> </footer>
<script> </div>
const announcementBar = document.getElementById('announcement-bar'); </div>
const closeButton = document.getElementById('close-announcement'); <script>
if (closeButton) { const announcementBar = document.getElementById('announcement-bar');
closeButton.addEventListener('click', () => { const closeButton = document.getElementById('close-announcement');
announcementBar.style.display = 'none'; if (closeButton) {
}); closeButton.addEventListener('click', () => {
} announcementBar.style.display = 'none';
</script> });
</body> }
</html> </script>
</body>
</html>

View File

@ -1,331 +1,409 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>I2P Secure Share</title> <title>I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"> <link rel="stylesheet" href="/static/css/tailwind.css"/>
<style> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border: 1px solid #4a5568; } <style>
.tab { border-bottom: 2px solid transparent; cursor: pointer; } body { background-color: #1a202c; color: #cbd5e0; }
.tab.active { border-bottom-color: #63b3ed; color: #ffffff; } .content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; }
.btn { background-color: #4299e1; transition: background-color 0.3s ease; } .tab { border-bottom:2px solid transparent; cursor:pointer; }
.btn:hover { background-color: #3182ce; } .tab.active { border-bottom-color:#63b3ed; color:#ffffff; }
.btn:disabled { background-color: #2b6cb0; cursor: not-allowed; } .btn { background-color:#4299e1; transition:background-color .3s ease; }
input[type="file"]::file-selector-button { background-color: #4a5568; color: #cbd5e0; border: none; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; transition: background-color 0.3s ease; } .btn:hover { background-color:#3182ce; }
input[type="file"]::file-selector-button:hover { background-color: #2d3748; } select,textarea,input[type="text"],input[type="password"],input[type="number"] {
select, textarea, input { background-color: #4a5568; border: 1px solid #718096; } background-color:#4a5568; border:1px solid #718096; color:#cbd5e0;
.alert-success { background-color: #38a169; } }
.alert-error { background-color: #e53e3e; } .alert-success { background-color:#38a169; }
.feature-card { background-color: #2d3748; } .alert-error { background-color:#e53e3e; }
.docs-container h3, .tos-container h2, #stats-content h2 { font-size: 1.5rem; font-weight: 600; color: #ffffff; margin-bottom: 1rem; } .announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; }
.docs-container h3, .tos-container h2 { border-bottom: 1px solid #4a5568; padding-bottom: 0.5rem; margin-top: 2rem; } .reveal { display:none; }
.docs-container p, .docs-container li, .tos-container p, .tos-container li { color: #a0aec0; } input[type="file"] {
.docs-container code { background-color: #1a202c; color: #f7fafc; padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: 'Courier New', Courier, monospace; } width:100%; padding:0.5rem 1rem; border-radius:0.375rem;
.docs-container pre { background-color: #1a202c; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; } background-color:#4a5568; border:1px solid #718096; color:#cbd5e0; cursor:pointer;
.tos-container ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; } }
.stat-card { background-color: #2d3748; border: 1px solid #4a5568; } input[type="file"]::file-selector-button {
.stat-value { color: #63b3ed; } background-color:#2d3748; color:#cbd5e0; border:none;
.announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; } padding:0.5rem 1rem; margin-right:1rem; border-radius:0.375rem; cursor:pointer;
#main-container { transition: max-width 0.3s ease-in-out; } transition:background-color .3s ease;
</style> }
<noscript> input[type="file"]::file-selector-button:hover { background-color:#3a4a5a; }
<style> .stat-card {
.tab-nav-container { display: none; } background-color:#2d3748; border:1px solid #4a5568; border-radius:0.5rem;
.tab-content { display: none !important; } }
#image-form, #paste-form { display: block !important; margin-bottom: 2rem; } .stat-value { color:#63b3ed; }
#api-docs, #stats-content, #tos-content, .features-section { display: none !important; } .label-with-icon {
@media (min-width: 1024px) { display:inline-flex; align-items:center; gap:0.5rem; font-size:0.875rem; color:#cbd5e0;
.noscript-forms-container { display: flex; gap: 1.5rem; } }
.noscript-forms-container > div { flex: 1; } .label-with-icon svg {
} width:1rem; height:1rem; color:#4299e1; flex-shrink:0;
</style> }
</noscript> .feature-card {
</head> background-color: #2d3748;
<body class="font-sans"> border: 1px solid #4a5568;
border-radius: 0.5rem;
{% if announcement_enabled and announcement_message %} }
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg"> /* API Docs Styling */
<span>{{ announcement_message }}</span> .docs-container h3 {
<button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button> font-size: 1.5rem;
</div> font-weight: 600;
{% endif %} color: #ffffff;
margin-top: 2rem;
<div class="flex items-center justify-center min-h-screen py-8"> margin-bottom: 1rem;
<div id="main-container" class="w-full max-w-2xl mx-auto p-4"> padding-bottom: 0.5rem;
<header class="text-center mb-8"> border-bottom: 1px solid #4a5568;
<a href="/" class="inline-block mb-4"> }
<img src="{{ url_for('static', filename='images/stormycloud.svg') }}" alt="StormyCloud Logo" style="width: 550px; max-width: 100%;" class="mx-auto"> .docs-container p { margin-bottom: 1rem; color: #a0aec0; }
</a> .docs-container ul { list-style-position: inside; margin-bottom: 1rem; }
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1> .docs-container li { margin-bottom: 0.5rem; color: #cbd5e0;}
<p class="text-gray-400">Anonymously share images and text pastes.</p> .docs-container code {
</header> background-color:#1a202c;
color:#f7fafc;
<main> padding:0.2rem 0.4rem;
<div id="js-error-container" class="hidden alert-error text-white p-3 rounded-md shadow-lg mb-4" role="alert"></div> border-radius:0.25rem;
font-family:monospace;
{% with messages = get_flashed_messages(with_categories=true) %} font-size: 0.875rem;
{% if messages %} }
<div class="mb-4"> .docs-container pre {
{% for category, message in messages %} background-color:#1a202c;
<div class="alert-{{ category }} text-white p-3 rounded-md shadow-lg" role="alert"> padding:1rem;
{{ message }} border-radius:0.5rem;
</div> overflow-x:auto;
{% endfor %} color:#f7fafc;
</div> font-family:monospace;
{% endif %} }
{% endwith %} </style>
<noscript>
<div class="mb-4 border-b border-gray-700 tab-nav-container"> <style>
<nav class="flex flex-wrap -mb-px" id="tab-nav"> .tab-nav-container{display:none;}
<a href="#image" class="tab active text-gray-300 py-4 px-6 block hover:text-white focus:outline-none">Image Uploader</a> .tab-content{display:none!important;}
<a href="#paste" class="tab text-gray-300 py-4 px-6 block hover:text-white focus:outline-none">Pastebin</a> #image-form,#paste-form{display:block!important;margin-bottom:2rem;}
<a href="#api" class="tab text-gray-300 py-4 px-6 block hover:text-white focus:outline-none">API</a> #api-docs,#stats-content,#tos-content,.features-section{display:none!important;}
<a href="#stats" class="tab text-gray-300 py-4 px-6 block hover:text-white focus:outline-none">Stats</a> @media(min-width:1024px){.noscript-forms-container{display:flex;gap:1.5rem;} .noscript-forms-container>div{flex:1;}}
<a href="#tos" class="tab text-gray-300 py-4 px-6 block hover:text-white focus:outline-none">Terms of Service</a> .reveal{display:block!important;}
</nav> </style>
</div> </noscript>
</head>
<div class="noscript-forms-container"> <body class="font-sans">
<div id="image-form" class="content-container rounded-lg p-8 shadow-lg tab-content">
<form id="image-upload-form" action="{{ url_for('upload_image') }}" method="POST" enctype="multipart/form-data"> {% if announcement_enabled and announcement_message %}
<h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2> <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<div class="mb-6"> <span>{{ announcement_message }}</span>
<label for="image-file" class="block text-gray-300 text-sm font-bold mb-2">Image File:</label> <button id="close-announcement"
<input type="file" name="file" id="image-file" class="w-full text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
<p class="text-xs text-gray-500 mt-1">Max file size: 10MB. Images are converted to WebP.</p> </div>
</div> {% endif %}
<div class="mb-6"> <div class="flex items-center justify-center min-h-screen py-8">
<label for="image-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label> <div id="main-container" class="w-full max-w-2xl mx-auto p-4">
<select name="expiry" id="image-expiry" class="w-full p-2 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> <header class="text-center mb-8">
<option value="15m">15 Minutes</option> <a href="/" class="inline-block mb-4">
<option value="1h" selected>1 Hour</option> <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
<option value="2h">2 Hours</option> </a>
<option value="4h">4 Hours</option> <h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<option value="8h">8 Hours</option> <p class="text-gray-400">Anonymously share images and text pastes.</p>
<option value="12h">12 Hours</option> </header>
<option value="24h">24 Hours</option>
<option value="48h">48 Hours</option> <main>
</select> <div id="js-error-container"
</div> class="hidden alert-error text-white p-3 rounded-md shadow-lg mb-4"
<div><button type="submit" id="upload-button" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Upload Image</button></div> role="alert"></div>
</form> {% with messages = get_flashed_messages(with_categories=true) %}
</div> {% if messages %}
<div class="mb-4">
<div id="paste-form" class="content-container rounded-lg p-8 shadow-lg hidden tab-content"> {% for category,message in messages %}
<form action="/upload/paste" method="POST"> <div class="alert-{{category}} text-white p-3 rounded-md shadow-lg"
<h2 class="text-2xl font-semibold mb-6 text-white">Create a Paste</h2> role="alert">{{ message }}</div>
<div class="mb-6"><label for="paste-content" class="block text-gray-300 text-sm font-bold mb-2">Paste Content:</label><textarea name="content" id="paste-content" rows="10" class="w-full p-2 rounded-md text-white font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" required></textarea></div> {% endfor %}
<div class="mb-6"> </div>
<label for="paste-language" class="block text-gray-300 text-sm font-bold mb-2">Language:</label> {% endif %}
<select name="language" id="paste-language" class="w-full p-2 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> {% endwith %}
<option value="text">Plain Text</option>
{% for lang in languages %} <div class="mb-4 border-b border-gray-700 tab-nav-container">
<option value="{{ lang|capitalize }}">{{ lang|capitalize }}</option> <nav class="flex -mb-px" id="tab-nav">
{% endfor %} <a href="#image"
</select> class="tab active text-gray-300 py-4 px-6 block hover:text-white">Image Uploader</a>
</div> <a href="#paste"
<div class="mb-6"> class="tab text-gray-300 py-4 px-6 block hover:text-white">Pastebin</a>
<label for="paste-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label> <a href="#api"
<select name="expiry" id="paste-expiry" class="w-full p-2 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500"> class="tab text-gray-300 py-4 px-6 block hover:text-white">API</a>
<option value="15m">15 Minutes</option> <a href="#stats"
<option value="1h" selected>1 Hour</option> class="tab text-gray-300 py-4 px-6 block hover:text-white">Stats</a>
<option value="2h">2 Hours</option> <a href="#tos"
<option value="4h">4 Hours</option> class="tab text-gray-300 py-4 px-6 block hover:text-white">Terms</a>
<option value="8h">8 Hours</option> </nav>
<option value="12h">12 Hours</option> </div>
<option value="24h">24 Hours</option>
<option value="48h">48 Hours</option> <div class="noscript-forms-container">
</select> <div id="image-form" class="content-container rounded-lg p-8 shadow-lg tab-content">
</div> <form action="/upload/image" method="POST" enctype="multipart/form-data">
<div><button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Create Paste</button></div> <h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2>
</form>
</div> <div class="mb-6">
</div> <label for="image-file" class="block text-gray-300 text-sm font-bold mb-2">Image File:</label>
<input type="file" name="file" id="image-file" required>
<div id="api-docs" class="content-container docs-container rounded-lg p-8 shadow-lg hidden tab-content"> <p class="text-xs text-gray-500 mt-1">Max 10MB; WebP conversion.</p>
<h3>Introduction</h3> </div>
<p>The API allows programmatic uploads. All endpoints are rate-limited. No API key is required.</p>
<h3>Uploading an Image</h3> <div class="mb-6">
<p>Send a `POST` request with `multipart/form-data`.</p> <label for="image-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<ul class="list-disc list-inside my-4 space-y-2"> <select name="expiry" id="image-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<li><strong>Endpoint:</strong> <code>POST /api/upload/image</code></li> <option value="15m">15 minutes</option>
<li><strong>Parameter <code>file</code>:</strong> (Required) The image file.</li> <option value="1h" selected>1 hour</option>
<li><strong>Parameter <code>expiry</code>:</strong> (Optional) Values: <code>15m</code>, <code>1h</code>, <code>2h</code>, <code>4h</code>, <code>8h</code>, <code>12h</code>, <code>24h</code>, <code>48h</code>.</li> <option value="2h">2 hours</option>
</ul> <option value="4h">4 hours</option>
<pre><code>curl -X POST -F "file=@/path/to/image.jpg" http:{{ url_for('api_upload_image', _external=True, _scheme='') }}</code></pre> <option value="8h">8 hours</option>
<h3>Creating a Paste</h3> <option value="12h">12 hours</option>
<p>Send a `POST` request with a JSON payload.</p> <option value="24h">24 hours</option>
<ul class="list-disc list-inside my-4 space-y-2"> <option value="48h">48 hours</option>
<li><strong>Endpoint:</strong> <code>POST /api/upload/paste</code></li> </select>
<li><strong>JSON Field <code>content</code>:</strong> (Required) The paste text.</li> </div>
</ul>
<pre><code>curl -X POST -H "Content-Type: application/json" -d '{"content": "Hello, World!"}' http:{{ url_for('api_upload_paste', _external=True, _scheme='') }}</code></pre> <div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;align-items:start;">
</div> <label class="label-with-icon">
<input type="checkbox" name="keep_exif">
<div id="stats-content" class="content-container rounded-lg p-8 shadow-lg hidden tab-content"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6M7 7h10M4 6h16M4 6a2 2 0 012-2h8l2 2h6a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6z"/></svg>
<h2 class="text-3xl font-bold text-white text-center mb-8">Service Statistics</h2> <span>Keep EXIF Data</span>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> </label>
<div class="stat-card p-6 rounded-lg text-center"><h3 class="text-xl font-semibold text-white mb-2">Total Image Uploads</h3><p class="text-5xl font-bold stat-value">{{ stats.get('total_images', 0) }}</p></div> <label class="label-with-icon">
<div class="stat-card p-6 rounded-lg text-center"><h3 class="text-xl font-semibold text-white mb-2">Total Paste Uploads</h3><p class="text-5xl font-bold stat-value">{{ stats.get('total_pastes', 0) }}</p></div> <input type="checkbox" id="image-pw-protect" name="password_protect">
<div class="stat-card p-6 rounded-lg text-center md:col-span-2 lg:col-span-1"><h3 class="text-xl font-semibold text-white mb-2">Total API Uploads</h3><p class="text-5xl font-bold stat-value">{{ stats.get('total_api_uploads', 0) }}</p></div> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
</div> <span>Password</span>
</div> </label>
<label class="label-with-icon" title="Removed after this many successful views">
<div id="tos-content" class="content-container tos-container rounded-lg p-8 shadow-lg hidden tab-content"> <input type="checkbox" id="image-views-protect" name="views_protect">
<h2 class="text-3xl font-bold text-white mb-4">Terms of Service</h2> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<p><strong>Last Updated: June 13, 2025</strong></p> <span>Max Views</span>
<p>Welcome! By using this service, you agree to these terms. The service is provided "as-is" with a focus on privacy.</p> </label>
<h3>1. Privacy and Data Handling</h3> </div>
<ul>
<li><strong>No Logs:</strong> We do not store personally identifiable information.</li> <div id="image-pw-options" class="reveal mb-6">
<li><strong>Encryption:</strong> All uploads are encrypted on our servers.</li> <label for="image-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
<li><strong>Data Deletion:</strong> Your data is permanently deleted after the selected expiration time.</li> <input type="password" name="password" id="image-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
<li><strong>Metadata Stripping:</strong> EXIF metadata is removed from all images.</li> </div>
</ul> <div id="image-views-options" class="reveal mb-6">
<h3>2. Acceptable Use</h3> <label for="image-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
<p>You agree not to upload illegal or harmful content. We reserve the right to remove content that violates these terms.</p> <input type="number" name="max_views" id="image-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
<h3>3. Limitation of Liability</h3> </div>
<p>This service is provided for free and without warranties. We are not responsible for any data loss.</p>
</div> <button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Upload Image</button>
</form>
</main> </div>
<section class="mt-16 text-center features-section"> <div id="paste-form" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white mb-8">Features</h2> <form action="/upload/paste" method="POST">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <h2 class="text-2xl font-semibold mb-6 text-white">Create a Paste</h2>
<div class="feature-card p-6 rounded-lg"><div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg></div><h3 class="text-xl font-semibold text-white mb-2">Encrypted at Rest</h3><p class="text-gray-400">All uploaded files and pastes are fully encrypted on the server, ensuring your data is protected.</p></div> <div class="mb-6">
<div class="feature-card p-6 rounded-lg"><div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.786-2.673 9.356m-2.673-9.356C6.673 8.214 9.327 5.5 12 5.5c2.673 0 5.327 2.714 5.327 5.5s-1.009 6.786-2.673 9.356m-2.673-9.356h0z" /></svg></div><h3 class="text-xl font-semibold text-white mb-2">Anonymous by Design</h3><p class="text-gray-400">Image metadata (EXIF) is stripped and no unnecessary logs are kept. Built for the I2P network.</p></div> <label for="paste-content" class="block text-gray-300 text-sm font-bold mb-2">Paste Content:</label>
<div class="feature-card p-6 rounded-lg"><div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" /></svg></div><h3 class="text-xl font-semibold text-white mb-2">StormyCloud Infrastructure</h3><p class="text-gray-400">A fast, reliable, and secure platform dedicated to the privacy of the I2P community.</p></div> <textarea name="content" id="paste-content" rows="10" class="w-full p-2 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" required></textarea>
</div> </div>
</section> <div class="mb-6">
<label for="paste-language" class="block text-gray-300 text-sm font-bold mb-2">Language:</label>
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> <select name="language" id="paste-language" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> <option value="text">Plain Text</option>
<span class="mx-2">|</span> {% for lang in languages %}
<a href="{{ url_for('donate_page') }}" class="hover:text-gray-400">Donate</a> <option value="{{ lang }}">{{ lang|capitalize }}</option>
</footer> {% endfor %}
</select>
</div> </div>
</div> <div class="mb-6">
<label for="paste-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<script> <select name="expiry" id="paste-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
// Make variables from backend available to JavaScript <option value="15m">15 minutes</option>
const allowedExtensions = {{ allowed_extensions|tojson }}; <option value="1h" selected>1 hour</option>
const maxFileSize = {{ config.MAX_CONTENT_LENGTH }}; <option value="2h">2 hours</option>
<option value="4h">4 hours</option>
document.addEventListener('DOMContentLoaded', () => { <option value="8h">8 hours</option>
const contentDivs = { <option value="12h">12 hours</option>
'#image': document.getElementById('image-form'), <option value="24h">24 hours</option>
'#paste': document.getElementById('paste-form'), <option value="48h">48 hours</option>
'#api': document.getElementById('api-docs'), </select>
'#stats': document.getElementById('stats-content'), </div>
'#tos': document.getElementById('tos-content') <div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(2,1fr);gap:2rem;align-items:start;">
}; <label class="label-with-icon">
const tabs = document.querySelectorAll('#tab-nav a'); <input type="checkbox" id="paste-pw-protect" name="password_protect">
const announcementBar = document.getElementById('announcement-bar'); <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
const closeButton = document.getElementById('close-announcement'); <span>Password</span>
</label>
const showTab = (hash) => { <label class="label-with-icon" title="Removed after this many successful views">
if (!hash || !contentDivs[hash]) { <input type="checkbox" id="paste-views-protect" name="views_protect">
hash = '#image'; <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
} <span>Max Views</span>
</label>
Object.values(contentDivs).forEach(div => { if(div) div.classList.add('hidden'); }); </div>
tabs.forEach(t => t.classList.remove('active')); <div id="paste-pw-options" class="reveal mb-6">
<label for="paste-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
if (contentDivs[hash]) { <input type="password" name="password" id="paste-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
contentDivs[hash].classList.remove('hidden'); </div>
document.querySelector(`a[href="${hash}"]`).classList.add('active'); <div id="paste-views-options" class="reveal mb-6">
} <label for="paste-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
}; <input type="number" name="max_views" id="paste-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
tabs.forEach(tab => { <button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Create Paste</button>
tab.addEventListener('click', (e) => { </form>
e.preventDefault(); </div>
const hash = e.target.hash;
window.history.replaceState(null, null, ' ' + hash); <div id="api-docs" class="content-container docs-container rounded-lg p-8 shadow-lg hidden tab-content">
showTab(hash); <h3>Introduction</h3>
}); <p>The API allows programmatic uploads. All endpoints are rate-limited. No API key is required.</p>
});
<h3>Uploading an Image</h3>
if (closeButton) { <p>Send a <code>POST</code> request with <code>multipart/form-data</code>.</p>
closeButton.addEventListener('click', () => { <ul>
announcementBar.style.display = 'none'; <li><strong>Endpoint:</strong> <code>POST /api/upload/image</code></li>
}); <li><strong>Parameter <code>file</code>:</strong> (Required) The image file.</li>
} <li><strong>Parameter <code>expiry</code>:</strong> (Optional) Values: <code>15m</code>, <code>1h</code>, <code>2h</code>, <code>4h</code>, <code>8h</code>, <code>12h</code>, <code>24h</code>, <code>48h</code>.</li>
<li><strong>Parameter <code>password</code>:</strong> (Optional) A password to protect the content.</li>
showTab(window.location.hash); <li><strong>Parameter <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
</ul>
// --- Upload Logic with Client-Side Validation --- <pre>curl -X POST -F "file=@/path/to/image.jpg" http://{{ request.host }}/api/upload/image</pre>
const imageForm = document.getElementById('image-upload-form');
const imageFileInput = document.getElementById('image-file'); <h3>Creating a Paste</h3>
const uploadButton = document.getElementById('upload-button'); <p>Send a <code>POST</code> request with a JSON payload.</p>
const errorContainer = document.getElementById('js-error-container'); <ul>
<li><strong>Endpoint:</strong> <code>POST /api/upload/paste</code></li>
imageForm.addEventListener('submit', function(event) { <li><strong>JSON Field <code>content</code>:</strong> (Required) The paste text.</li>
event.preventDefault(); <li><strong>JSON Field <code>language</code>:</strong> (Optional) A valid language for syntax highlighting. Defaults to 'text'.</li>
<li><strong>JSON Field <code>expiry</code>:</strong> (Optional) Same values as image expiry.</li>
const file = imageFileInput.files[0]; <li><strong>JSON Field <code>password</code>:</strong> (Optional) A password to protect the content.</li>
<li><strong>JSON Field <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
// --- Start Validation --- </ul>
errorContainer.classList.add('hidden'); // Hide old errors <pre>curl -X POST -H "Content-Type: application/json" \
-d '{"content":"Hello World", "expiry":"1h"}' \
if (!file) { http://{{ request.host }}/api/upload/paste</pre>
errorContainer.textContent = 'Please select a file to upload.'; </div>
errorContainer.classList.remove('hidden');
return; <div id="stats-content" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
} <h2 class="text-3xl font-bold text-white text-center mb-8">Service Statistics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
const fileExtension = file.name.split('.').pop().toLowerCase(); <div class="stat-card p-6 text-center">
if (!allowedExtensions.includes(fileExtension)) { <h3 class="text-xl font-semibold text-white mb-2">Total Image Uploads</h3>
errorContainer.textContent = `Invalid file type. Please upload one of the following: ${allowedExtensions.join(', ')}`; <p class="text-5xl font-bold stat-value">{{ stats.total_images }}</p>
errorContainer.classList.remove('hidden'); </div>
return; <div class="stat-card p-6 text-center">
} <h3 class="text-xl font-semibold text-white mb-2">Total Paste Uploads</h3>
<p class="text-5xl font-bold stat-value">{{ stats.total_pastes }}</p>
if (file.size > maxFileSize) { </div>
errorContainer.textContent = `File is too large. Maximum size is ${maxFileSize / 1024 / 1024}MB.`; <div class="stat-card p-6 text-center">
errorContainer.classList.remove('hidden'); <h3 class="text-xl font-semibold text-white mb-2">Total API Uploads</h3>
return; <p class="text-5xl font-bold stat-value">{{ stats.total_api_uploads }}</p>
} </div>
// --- End Validation --- </div>
</div>
uploadButton.textContent = 'Uploading...';
uploadButton.disabled = true; <div id="tos-content" class="content-container tos-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white mb-4">Terms of Service</h2>
const formData = new FormData(imageForm); <p><strong>Last Updated: June 20, 2025</strong></p>
<p>By using this service you agree to these terms. The service is provided “as-is” with a focus on privacy.</p>
fetch("{{ url_for('api_upload_image') }}", { <h3 class="mt-6 text-white text-xl font-semibold mb-2">1. Privacy & Data</h3>
method: 'POST', <ul class="list-disc list-inside text-gray-300 mb-4">
body: formData <li>No logs of your identity or IP.</li>
}) <li>All uploads are encrypted at rest.</li>
.then(response => { <li>Data auto-deletes after expiry or view limit.</li>
return response.json().then(data => ({ ok: response.ok, status: response.status, data })); <li>EXIF metadata only kept if opted-in.</li>
}) </ul>
.then(({ ok, status, data }) => { <h3 class="mt-6 text-white text-xl font-semibold mb-2">2. Acceptable Use</h3>
if (ok) { <p class="text-gray-300 mb-4">Do not upload illegal or harmful content. We reserve the right to remove content that violates these terms.</p>
const urlParts = data.url.split('/'); <h3 class="mt-6 text-white text-xl font-semibold mb-2">3. Liability</h3>
const filename = urlParts[urlParts.length - 1]; <p class="text-gray-300">This free service comes with no warranties. We are not responsible for data loss.</p>
window.location.href = `/image/${filename}`; </div>
} else { </div>
errorContainer.textContent = data.error || `Server responded with status: ${status}`;
errorContainer.classList.remove('hidden'); <div class="text-center text-xs text-gray-500 mt-6 mb-8">
uploadButton.textContent = 'Upload Image'; <a href="https://github.com/your-username/your-repo-name" target="_blank" rel="noopener noreferrer" class="hover:text-gray-400 transition-colors">
uploadButton.disabled = false; Version 1.1
} </a>
}) </div>
.catch(error => {
errorContainer.textContent = `An error occurred during the upload: ${error.message}`; <section class="text-center features-section">
errorContainer.classList.remove('hidden'); <h2 class="text-3xl font-bold text-white mb-8">Features</h2>
uploadButton.textContent = 'Upload Image'; <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
uploadButton.disabled = false; <div class="feature-card p-6 rounded-lg">
console.error('Upload Error:', error); <div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
}); <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
}); </div>
}); <h3 class="text-xl font-semibold text-white mb-2">Encrypted at Rest</h3>
</script> <p class="text-gray-400">All uploaded files and pastes are fully encrypted on the server, ensuring your data is protected.</p>
</body> </div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.786-2.673 9.356 m-2.673-9.356C6.673 8.214 9.327 5.5 12 5.5 c2.673 0 5.327 2.714 5.327 5.5s-1.009 6.786-2.673 9.356m-2.673-9.356h0z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Anonymous by Design</h3>
<p class="text-gray-400">Image metadata (EXIF) is stripped and no unnecessary logs are kept. Built for the I2P network.</p>
</div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">StormyCloud Infrastructure</h3>
<p class="text-gray-400">A fast, reliable, and secure platform dedicated to the privacy of the I2P community.</p>
</div>
</div>
</section>
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
<span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a>
</footer>
</main>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const contentDivs = {
'#image': document.querySelector('#image-form'),
'#paste': document.querySelector('#paste-form'),
'#api': document.querySelector('#api-docs'),
'#stats': document.querySelector('#stats-content'),
'#tos': document.querySelector('#tos-content')
};
const tabs = document.querySelectorAll('#tab-nav a');
function showTab(hash) {
if (!hash || !contentDivs[hash]) hash = '#image';
Object.values(contentDivs).forEach(d => {
if(d) d.classList.add('hidden');
});
tabs.forEach(t => t.classList.remove('active'));
if(contentDivs[hash]) {
contentDivs[hash].classList.remove('hidden');
}
const activeTab = document.querySelector(`#tab-nav a[href="${hash}"]`);
if(activeTab) {
activeTab.classList.add('active');
}
}
tabs.forEach(tab => tab.addEventListener('click', e => {
e.preventDefault();
const h = e.target.hash;
window.history.replaceState(null, null, ' ' + h);
showTab(h);
}));
showTab(window.location.hash);
function toggle(cbId, tgtId) {
const cb = document.getElementById(cbId), tgt = document.getElementById(tgtId);
if (!cb||!tgt) return;
cb.addEventListener('change', () => tgt.style.display = cb.checked ? 'block' : 'none');
tgt.style.display = cb.checked ? 'block' : 'none';
}
toggle('image-pw-protect','image-pw-options');
toggle('image-views-protect','image-views-options');
toggle('paste-pw-protect','paste-pw-options');
toggle('paste-views-protect','paste-views-options');
const closeBtn = document.getElementById('close-announcement');
if (closeBtn) closeBtn.addEventListener('click', ()=>
document.getElementById('announcement-bar').style.display = 'none'
);
});
</script>
</body>
</html> </html>

View File

@ -1,98 +1,120 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Image Uploaded - I2P Secure Share</title> <title>View Image - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"> <link rel="stylesheet" href="/static/css/tailwind.css"/>
<style> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
body { background-color: #1a202c; color: #cbd5e0; }
.link-box { background-color: #2d3748; border: 1px solid #4a5568; word-break: break-all; } <style>
.btn { background-color: #4299e1; transition: background-color 0.3s ease; } body { background-color: #1a202c; color: #cbd5e0; }
.btn:hover { background-color: #3182ce; } .content-container { background-color: #2d3748; border: 1px solid #4a5568; }
.thumbnail-container { border: 2px dashed #4a5568; max-width: 100%; } .link-box { background-color: #2d3748; border:1px solid #4a5568; word-break:break-all; }
.thumbnail { max-width: 100%; max-height: 60vh; object-fit: contain; } .btn { background-color:#4299e1; transition:background-color .3s ease; }
.announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; } .btn:hover { background-color:#3182ce; }
</style> .thumbnail-container { border:2px dashed #4a5568; max-width:100%; }
</head> .thumbnail { max-width:100%; max-height:60vh; object-fit:contain; }
<body class="font-sans"> .announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; }
.alert-success { background-color:#38a169; }
<!-- Announcement Bar --> .alert-error { background-color:#e53e3e; }
{% if announcement_enabled and announcement_message %} </style>
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg"> </head>
<span>{{ announcement_message }}</span> <body class="font-sans">
<button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
</div> {% if announcement_enabled and announcement_message %}
{% endif %} <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span>
<div class="flex items-center justify-center min-h-screen py-8"> <button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
<div class="w-full max-w-2xl mx-auto p-4"> </div>
<header class="text-center mb-8"> {% endif %}
<a href="/" class="inline-block mb-4">
<img src="{{ url_for('static', filename='images/stormycloud.svg') }}" alt="StormyCloud Logo" style="width: 550px; max-width: 100%;" class="mx-auto"> <div class="flex items-center justify-center min-h-screen py-8">
</a> <div class="w-full max-w-2xl mx-auto p-4">
<h1 class="text-3xl font-bold text-white">Image Uploaded Successfully</h1> {% if password_required %}
<p class="text-gray-400 mt-2">Share the link below. The file will be deleted based on the expiration you selected.</p> <div class="content-container rounded-lg p-8 shadow-lg">
</header> <h2 class="text-2xl font-semibold text-white mb-6">Enter Password to View Image</h2>
<form method="POST">
<main class="flex-grow"> <div class="mb-6">
<!-- Image Thumbnail --> <label for="password" class="block text-gray-300 text-sm font-bold mb-2">Password:</label>
<div class="thumbnail-container rounded-lg p-4 mb-6 flex justify-center items-center"> <input type="password" name="password" id="password"
<img src="{{ url_for('get_upload', filename=filename) }}" alt="Uploaded Image" class="thumbnail rounded-md"> class="w-full p-2 rounded-md text-white bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
</div> required>
</div>
<!-- Share Link --> <button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md">Unlock Image</button>
<div class="link-box rounded-lg p-4"> </form>
<label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Direct Image Link:</label> </div>
<div class="flex items-center space-x-2"> {% else %}
<input type="text" id="share-link" class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md" value="{{ url_for('get_upload', filename=filename, _external=True) }}" readonly> <header class="text-center mb-8">
<button id="copy-button" class="btn text-white font-bold py-3 px-5 rounded-md focus:shadow-outline flex-shrink-0">Copy</button> <a href="/" class="inline-block mb-4">
</div> <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
</div> </a>
<h1 class="text-3xl font-bold text-white">View Image</h1>
<div class="text-center mt-8"> <p class="text-gray-400 mt-2 text-xl">Expires in: {{ time_left }}</p>
<a href="{{ url_for('index') }}" class="text-blue-400 hover:text-blue-300">Upload another file</a> </header>
</div>
<main>
<!-- Subtle Donation Link --> {% with messages = get_flashed_messages(with_categories=true) %}
<div class="text-center mt-4 text-sm"> {% if messages %}
<p class="text-gray-500">Find this service useful? <a href="{{ url_for('donate_page') }}" class="text-blue-400 hover:underline">Consider supporting its future.</a></p> <div class="mb-4">
</div> {% for category, message in messages %}
</main> <div class="alert-{{category}} text-white p-3 rounded-md shadow-lg"
role="alert">{{ message }}</div>
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> {% endfor %}
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> </div>
<span class="mx-2">|</span> {% endif %}
<a href="{{ url_for('donate_page') }}" class="hover:text-gray-400">Donate</a> {% endwith %}
</footer>
<div class="thumbnail-container rounded-lg p-4 mb-6 flex justify-center items-center">
</div> <img src="/uploads/{{ filename }}"
</div> alt="Uploaded Image" class="thumbnail rounded-md"/>
</div>
<script>
document.getElementById('copy-button').addEventListener('click', () => { <div class="link-box rounded-lg p-4 mb-4">
const linkInput = document.getElementById('share-link'); <label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Direct Image Link:</label>
const button = document.getElementById('copy-button'); <div class="flex items-center space-x-2">
linkInput.select(); <input type="text" id="share-link"
linkInput.setSelectionRange(0, 99999); class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md"
try { value="{{ request.host_url }}uploads/{{ filename }}" readonly>
document.execCommand('copy'); <button id="copy-button"
button.textContent = 'Copied!'; class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
setTimeout(() => { button.textContent = 'Copy'; }, 2000); Copy
} catch (err) { </button>
console.error('Failed to copy text: ', err); </div>
button.textContent = 'Error'; </div>
}
window.getSelection().removeAllRanges(); <div class="text-center mt-8">
}); <a href="/" class="text-blue-400 hover:text-blue-300">Upload another file</a>
</div>
const announcementBar = document.getElementById('announcement-bar'); <div class="text-center mt-4 text-sm">
const closeButton = document.getElementById('close-announcement'); <p class="text-gray-500">Find this service useful? <a href="/donate" class="text-blue-400 hover:underline">Consider supporting its future.</a></p>
if (closeButton) { </div>
closeButton.addEventListener('click', () => {
announcementBar.style.display = 'none'; </main>
}); {% endif %}
}
</script> <footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
</body> <a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
</html> <span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a>
</footer>
</div>
</div>
<script>
const copyBtn = document.getElementById('copy-button');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const inp = document.getElementById('share-link');
inp.select(); document.execCommand('copy');
copyBtn.textContent = 'Copied!';
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
});
}
const closeBtn = document.getElementById('close-announcement');
if (closeBtn) closeBtn.addEventListener('click', () =>
document.getElementById('announcement-bar').style.display = 'none'
);
</script>
</body>
</html>

View File

@ -1,111 +1,214 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Paste Created - I2P Secure Share</title> <title>View Paste - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"> <link rel="stylesheet" href="/static/css/tailwind.css"/>
<style> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
body { background-color: #1a202c; color: #cbd5e0; }
.link-box { background-color: #2d3748; border: 1px solid #4a5568; word-break: break-all; } <style>
.btn { background-color: #4299e1; transition: background-color 0.3s ease; } body { background-color: #1a202c; color: #cbd5e0; }
.btn:hover { background-color: #3182ce; } .link-box { background-color: #2d3748; border:1px solid #4a5568; word-break:break-all; }
.btn { background-color:#4299e1; transition:background-color .3s ease; }
{{ css_styles|safe }} .btn:hover { background-color:#3182ce; }
.code-container {
.code-container { background-color:#2d3748; border-radius:0.5rem; border:1px solid #4a5568; overflow:hidden;
background-color: #2d3748; }
border-radius: 0.5rem; .syntax pre {
border: 1px solid #4a5568; margin:0; padding:1rem; white-space:pre-wrap; word-wrap:break-word;
overflow: hidden; }
} .announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; }
.wide-container { max-width: 85rem; }
.syntax pre { .alert-success { background-color:#38a169; }
margin: 0; .alert-error { background-color:#e53e3e; }
padding: 1rem;
white-space: pre-wrap; /* Pygments 'monokai' theme CSS */
word-wrap: break-word; .syntax .hll { background-color: #49483e }
} .syntax { background: #272822; color: #f8f8f2 }
.announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; } .syntax .c { color: #75715e } /* Comment */
</style> .syntax .err { color: #960050; background-color: #1e0010 } /* Error */
</head> .syntax .k { color: #66d9ef } /* Keyword */
<body class="font-sans"> .syntax .l { color: #ae81ff } /* Literal */
.syntax .n { color: #f8f8f2 } /* Name */
<!-- Announcement Bar --> .syntax .o { color: #f92672 } /* Operator */
{% if announcement_enabled and announcement_message %} .syntax .p { color: #f8f8f2 } /* Punctuation */
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg"> .syntax .ch { color: #75715e } /* Comment.Hashbang */
<span>{{ announcement_message }}</span> .syntax .cm { color: #75715e } /* Comment.Multiline */
<button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button> .syntax .cp { color: #75715e } /* Comment.Preproc */
</div> .syntax .cpf { color: #75715e } /* Comment.PreprocFile */
{% endif %} .syntax .c1 { color: #75715e } /* Comment.Single */
.syntax .cs { color: #75715e } /* Comment.Special */
<div class="flex items-center justify-center min-h-screen py-8"> .syntax .gd { color: #f92672 } /* Generic.Deleted */
<div class="w-full max-w-2xl mx-auto p-4"> .syntax .ge { font-style: italic } /* Generic.Emph */
<header class="text-center mb-8"> .syntax .gi { color: #a6e22e } /* Generic.Inserted */
<a href="/" class="inline-block mb-4"> .syntax .gs { font-weight: bold } /* Generic.Strong */
<img src="{{ url_for('static', filename='images/stormycloud.svg') }}" alt="StormyCloud Logo" style="width: 550px; max-width: 100%;" class="mx-auto"> .syntax .gu { color: #75715e } /* Generic.Subheading */
</a> .syntax .kc { color: #66d9ef } /* Keyword.Constant */
<h1 class="text-3xl font-bold text-white">Paste Created Successfully</h1> .syntax .kd { color: #66d9ef } /* Keyword.Declaration */
<p class="text-gray-400 mt-2">Share the link below. The paste will be deleted based on the expiration you selected.</p> .syntax .kn { color: #f92672 } /* Keyword.Namespace */
</header> .syntax .kp { color: #66d9ef } /* Keyword.Pseudo */
.syntax .kr { color: #66d9ef } /* Keyword.Reserved */
<main> .syntax .kt { color: #66d9ef } /* Keyword.Type */
<!-- Paste Content --> .syntax .ld { color: #e6db74 } /* Literal.Date */
<div class="code-container mb-6"> .syntax .m { color: #ae81ff } /* Literal.Number */
{{ highlighted_content|safe }} .syntax .s { color: #e6db74 } /* Literal.String */
</div> .syntax .na { color: #a6e22e } /* Name.Attribute */
.syntax .nb { color: #f8f8f2 } /* Name.Builtin */
<!-- Share Link --> .syntax .nc { color: #a6e22e } /* Name.Class */
<div class="link-box rounded-lg p-4"> .syntax .no { color: #66d9ef } /* Name.Constant */
<label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Shareable Link:</label> .syntax .nd { color: #a6e22e } /* Name.Decorator */
<div class="flex items-center space-x-2"> .syntax .ni { color: #f8f8f2 } /* Name.Entity */
<input type="text" id="share-link" class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md" value="{{ request.url }}" readonly> .syntax .ne { color: #a6e22e } /* Name.Exception */
<button id="copy-button" class="btn text-white font-bold py-3 px-5 rounded-md focus:shadow-outline flex-shrink-0">Copy</button> .syntax .nf { color: #a6e22e } /* Name.Function */
</div> .syntax .nl { color: #f8f8f2 } /* Name.Label */
</div> .syntax .nn { color: #f8f8f2 } /* Name.Namespace */
.syntax .nx { color: #a6e22e } /* Name.Other */
<div class="text-center mt-8"> .syntax .py { color: #f8f8f2 } /* Name.Property */
<a href="{{ url_for('index') }}" class="text-blue-400 hover:text-blue-300">Create another paste</a> .syntax .nt { color: #f92672 } /* Name.Tag */
</div> .syntax .nv { color: #f8f8f2 } /* Name.Variable */
.syntax .ow { color: #f92672 } /* Operator.Word */
<!-- Subtle Donation Link --> .syntax .w { color: #f8f8f2 } /* Text.Whitespace */
<div class="text-center mt-4 text-sm"> .syntax .mb { color: #ae81ff } /* Literal.Number.Bin */
<p class="text-gray-500">Find this service useful? <a href="{{ url_for('donate_page') }}" class="text-blue-400 hover:underline">Consider supporting its future.</a></p> .syntax .mf { color: #ae81ff } /* Literal.Number.Float */
</div> .syntax .mh { color: #ae81ff } /* Literal.Number.Hex */
</main> .syntax .mi { color: #ae81ff } /* Literal.Number.Integer */
.syntax .mo { color: #ae81ff } /* Literal.Number.Oct */
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> .syntax .sa { color: #e6db74 } /* Literal.String.Affix */
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> .syntax .sb { color: #e6db74 } /* Literal.String.Backtick */
<span class="mx-2">|</span> .syntax .sc { color: #e6db74 } /* Literal.String.Char */
<a href="{{ url_for('donate_page') }}" class="hover:text-gray-400">Donate</a> .syntax .dl { color: #e6db74 } /* Literal.String.Delimiter */
</footer> .syntax .sd { color: #e6db74 } /* Literal.String.Doc */
</div> .syntax .s2 { color: #e6db74 } /* Literal.String.Double */
</div> .syntax .se { color: #ae81ff } /* Literal.String.Escape */
.syntax .sh { color: #e6db74 } /* Literal.String.Heredoc */
<script> .syntax .si { color: #e6db74 } /* Literal.String.Interpol */
document.getElementById('copy-button').addEventListener('click', () => { .syntax .sx { color: #e6db74 } /* Literal.String.Other */
const linkInput = document.getElementById('share-link'); .syntax .sr { color: #e6db74 } /* Literal.String.Regex */
const button = document.getElementById('copy-button'); .syntax .s1 { color: #e6db74 } /* Literal.String.Single */
linkInput.select(); .syntax .ss { color: #e6db74 } /* Literal.String.Symbol */
linkInput.setSelectionRange(0, 99999); .syntax .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
try { .syntax .fm { color: #a6e22e } /* Name.Function.Magic */
document.execCommand('copy'); .syntax .vc { color: #f8f8f2 } /* Name.Variable.Class */
button.textContent = 'Copied!'; .syntax .vg { color: #f8f8f2 } /* Name.Variable.Global */
setTimeout(() => { button.textContent = 'Copy'; }, 2000); .syntax .vi { color: #f8f8f2 } /* Name.Variable.Instance */
} catch (err) { .syntax .vm { color: #f8f8f2 } /* Name.Variable.Magic */
console.error('Failed to copy text: ', err); .syntax .il { color: #ae81ff } /* Literal.Number.Integer.Long */
button.textContent = 'Error'; </style>
} </head>
window.getSelection().removeAllRanges(); <body class="font-sans">
});
{% if announcement_enabled and announcement_message %}
const announcementBar = document.getElementById('announcement-bar'); <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
const closeButton = document.getElementById('close-announcement'); <span>{{ announcement_message }}</span>
if (closeButton) { <button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
closeButton.addEventListener('click', () => { </div>
announcementBar.style.display = 'none'; {% endif %}
});
} <div class="flex items-center justify-center min-h-screen py-8">
</script> <div class="w-full wide-container mx-auto p-6">
</body> {% if password_required %}
</html> <div class="content-container rounded-lg p-10 shadow-lg"></div>
<h2 class="text-2xl font-semibold text-white mb-6">Enter Password to View Paste</h2>
<form method="POST">
<div class="mb-6">
<label for="password" class="block text-gray-300 text-sm font-bold mb-2">Password:</label>
<input type="password" name="password" id="password"
class="w-full p-2 rounded-md text-white bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md">Unlock Paste</button>
</form>
</div>
{% else %}
<header class="text-center mb-8">
<a href="/" class="inline-block mb-4">
<img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
</a>
<h1 class="text-3xl font-bold text-white">View Paste</h1>
<p class="text-gray-400 mt-2 text-xl">Expires in: {{ time_left }}</p>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mb-4">
{% for category, message in messages %}
<div class="alert-{{category}} text-white p-3 rounded-md shadow-lg"
role="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="flex justify-end mb-2">
<form method="GET" action="" class="flex items-center space-x-2">
<label for="language-switcher" class="text-sm text-gray-400">Syntax:</label>
<select name="lang" id="language-switcher" onchange="this.form.submit()" class="text-sm rounded-md p-1 bg-gray-700 border border-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer">
{% for lang in languages %}
<option value="{{ lang }}" {% if lang == selected_language %}selected{% endif %}>
{{ lang|capitalize }}
</option>
{% endfor %}
</select>
<noscript><button type="submit" class="btn text-sm py-1 px-3">Update</button></noscript>
</form>
</div>
<div class="code-container mb-6 syntax">
{{ highlighted_content|safe }}
</div>
<div class="link-box rounded-lg p-4 mb-4">
<label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Shareable Link:</label>
<div class="flex items-center space-x-2">
<input type="text" id="share-link"
class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md"
value="{{ request.url }}" readonly>
<button id="copy-button"
class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
Copy
</button>
<a href="/paste/{{ paste_id }}/raw"
target="_blank"
class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
Raw
</a>
</div>
</div>
<div class="text-center mt-8">
<a href="/" class="text-blue-400 hover:text-blue-300">Create another paste</a>
</div>
<div class="text-center mt-4 text-sm">
<p class="text-gray-500">Find this service useful? <a href="/donate" class="text-blue-400 hover:underline">Consider supporting its future.</a></p>
</div>
</main>
{% endif %}
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
<span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a>
</footer>
</div>
</div>
<script>
const copyBtn = document.getElementById('copy-button');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const inp = document.getElementById('share-link');
inp.select(); document.execCommand('copy');
copyBtn.textContent = 'Copied!';
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
});
}
const closeBtn = document.getElementById('close-announcement');
if (closeBtn) closeBtn.addEventListener('click', () =>
document.getElementById('announcement-bar').style.display = 'none'
);
</script>
</body>
</html>

View File

@ -1,8 +0,0 @@
# wsgi.py
# This file is the entry point for the Gunicorn WSGI server.
# It imports the 'app' object from your main application file.
from app import app
if __name__ == "__main__":
app.run()