├── .gitignore ├── Discovery.md ├── LICENSE ├── README.md ├── Spacefile ├── main.py ├── models.py ├── requirements.txt ├── static ├── images │ ├── copy.png │ ├── discovery.png │ ├── icon.png │ └── upload.png ├── scripts │ ├── dashboard.js │ ├── info.js │ ├── script.js │ └── upload.js └── styles │ └── style.css └── templates ├── base.html ├── dashboard.html ├── embed.html ├── info.html └── upload.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom ignores 2 | __pycache__/ 3 | .vscode/ 4 | .vs/ 5 | .idea/ 6 | .deta/ 7 | .space/ 8 | .env 9 | .venv/ 10 | env/ 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | cover/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | .pybuilder/ 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | # For a library or package, you might want to ignore these files since the code is 98 | # intended to run in multiple environments; otherwise, check them in: 99 | # .python-version 100 | 101 | # pipenv 102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 105 | # install all needed dependencies. 106 | #Pipfile.lock 107 | 108 | # poetry 109 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 110 | # This is especially recommended for binary packages to ensure reproducibility, and is more 111 | # commonly ignored for libraries. 112 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 113 | #poetry.lock 114 | 115 | # pdm 116 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 117 | #pdm.lock 118 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 119 | # in version control. 120 | # https://pdm.fming.dev/#use-with-ide 121 | .pdm.toml 122 | 123 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 124 | __pypackages__/ 125 | 126 | # Celery stuff 127 | celerybeat-schedule 128 | celerybeat.pid 129 | 130 | # SageMath parsed files 131 | *.sage.py 132 | 133 | # Environments 134 | .env 135 | .venv 136 | env/ 137 | venv/ 138 | ENV/ 139 | env.bak/ 140 | venv.bak/ 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | .spyproject 145 | 146 | # Rope project settings 147 | .ropeproject 148 | 149 | # mkdocs documentation 150 | /site 151 | 152 | # mypy 153 | .mypy_cache/ 154 | .dmypy.json 155 | dmypy.json 156 | 157 | # Pyre type checker 158 | .pyre/ 159 | 160 | # pytype static type analyzer 161 | .pytype/ 162 | 163 | # Cython debug symbols 164 | cython_debug/ 165 | 166 | # PyCharm 167 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 168 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 169 | # and can be added to the global gitignore or merged into this file. For a more nuclear 170 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 171 | #.idea/ 172 | -------------------------------------------------------------------------------- /Discovery.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "SpaceShuttle" 3 | tagline: "Your personal image cloud" 4 | theme_color: "#FF13AC" 5 | git: "https://github.com/SpaceShuttleApp/SpaceShuttle" 6 | homepage: "https://spaceshuttle.deta.dev" 7 | --- 8 | 9 | # The **first** ever deta space image hosting 10 | 11 | ![dashboard](https://sleep.deta.dev/cdn/SCR-20221019-3zk.png) 12 | 13 | ### Guide 14 | 15 | - [Read The Guide](https://github.com/SpaceShuttleApp/Guide) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SpaceShuttleApp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpaceShuttle 2 | 3 | Your personal image cloud 4 | 5 | ### Dashboard 6 | 7 | ![dashboard](https://sleep.deta.dev/cdn/SCR-20221019-3zk.png) 8 | 9 | ### Installation 10 | 11 | 12 | 13 | ### Links 14 | 15 | - [Website](https://spaceshuttle.deta.dev/) 16 | - [Guide](https://github.com/SpaceShuttleApp/Guide) 17 | 18 | ## Special Thanks 19 | 20 | - [jnsougata](https://github.com/jnsougata) for helping me with the image uploading and teaching me html layouts 21 | - [LemonPi](https://github.com/LemonPi314) for creating the dark/light mode toggle and teaching me html ui's 22 | - [Xeust](https://github.com/xeust) for giving feedback and helping me setup deta space 23 | - [Mustafa](https://github.com/abdelhai) for the new name (SpaceShuttle) 24 | -------------------------------------------------------------------------------- /Spacefile: -------------------------------------------------------------------------------- 1 | v: 0 2 | icon: ./static/images/discovery.png 3 | micros: 4 | - name: spaceshuttle 5 | src: . 6 | engine: python3.9 7 | run: uvicorn main:app 8 | primary: true 9 | public_routes: 10 | - "/cdn/*" 11 | - "/embed/*" -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from models import Image 3 | from deta import Base, Drive 4 | from fastapi import FastAPI, Request 5 | from fastapi.templating import Jinja2Templates 6 | from fastapi.middleware.cors import CORSMiddleware 7 | from fastapi.responses import HTMLResponse, FileResponse, Response 8 | 9 | app = FastAPI() 10 | 11 | app.add_middleware(CORSMiddleware, allow_origins=["*"]) 12 | pages = Jinja2Templates(directory="templates") 13 | 14 | cdn = Base("images") 15 | images = Drive("images") 16 | 17 | 18 | class NoCacheFileResponse(FileResponse): 19 | def __init__(self, path: str, **kwargs): 20 | super().__init__(path, **kwargs) 21 | self.headers["Cache-Control"] = "no-cache" 22 | 23 | 24 | @app.get("/", response_class=HTMLResponse) 25 | async def dashboard(request: Request): 26 | res = cdn.fetch() 27 | items = res.items 28 | while res.last: 29 | res = cdn.fetch(last=res.last) 30 | items += res.items 31 | return pages.TemplateResponse( 32 | "dashboard.html", 33 | {"request": request, "items": items}, 34 | ) 35 | 36 | 37 | @app.get("/image", response_class=HTMLResponse) 38 | async def image_upload_page(request: Request): 39 | return pages.TemplateResponse("upload.html", {"request": request}) 40 | 41 | 42 | @app.post("/upload") 43 | async def image_upload(request: Request, image: Image): 44 | extension = image.filename.split(".")[-1] 45 | image_data = image.content.split(",")[1].encode("utf-8") 46 | item = cdn.put( 47 | { 48 | # Why non-US spelling? Might come as a suprise to users of the API. 49 | # Just the way it is, not trying to say US is superior. -- LemonPi314 50 | # Don't take the ou out of colour --SlumberDemon 51 | "ext": extension, 52 | "visibility": False, 53 | "embed": [{"title": "", "colour": "000000"}], 54 | }, 55 | ) 56 | id = item["key"] 57 | images.put(f"{id}.{extension}", base64.b64decode(image_data)) 58 | url = f"{request.url.scheme}://{request.url.hostname}/cdn/{id}.{extension}" 59 | return {"image": url, "id": id} 60 | 61 | 62 | @app.get("/info/{id}", response_class=HTMLResponse) 63 | async def image_info(request: Request, id: str): 64 | info = cdn.get(id) 65 | return pages.TemplateResponse( 66 | "info.html", 67 | {"request": request, "data": info}, 68 | ) 69 | 70 | 71 | @app.get("/data/{id}") 72 | async def image_data(id: str): 73 | info = cdn.get(id) 74 | return info 75 | 76 | 77 | # to deliver static files without caching 78 | # (Done by jnsougata... smh -- LemonPi314) 79 | @app.get("/static/{path:path}") 80 | async def static(path: str): 81 | return NoCacheFileResponse(f"./static/{path}") 82 | 83 | 84 | @app.patch("/update/{id}") 85 | async def image_update( 86 | id: str, 87 | embed_title: str = None, 88 | embed_colour_hex: str = None, 89 | visibility: bool = None, 90 | ): 91 | data = cdn.get(id) 92 | embed_title = ( 93 | embed_title if not data["embed"][0]["title"] else data["embed"][0]["title"] 94 | ) 95 | embed_colour_hex = ( 96 | embed_colour_hex 97 | if not data["embed"][0]["colour"] 98 | else data["embed"][0]["colour"] 99 | ) 100 | visibility = visibility if not data["visibility"] else data["visibility"] 101 | cdn.update( 102 | { 103 | "visibility": visibility, 104 | "embed": [{"title": embed_title, "colour": f"{embed_colour_hex}"}], 105 | }, 106 | key=id, 107 | ) 108 | return { 109 | "id": id, 110 | "visibility": visibility, 111 | "title": embed_title, 112 | "colour": embed_colour_hex, 113 | } 114 | 115 | 116 | @app.delete("/delete/{id}") 117 | async def image_delete(id: str): 118 | data = cdn.get(id) 119 | images.delete(f"{data['key']}.{data['ext']}") 120 | cdn.delete(id) 121 | return {"id": id} 122 | 123 | 124 | @app.get("/cdn/{image}") 125 | async def image_cdn(image: str): 126 | img = images.get(image) 127 | info = cdn.get(image.split(".")[0]) 128 | if info["visibility"] == True: 129 | item = Response( 130 | img.read(), 131 | media_type=f"image/{image.split('.')[1]}", 132 | ) 133 | else: 134 | item = {"error": 404} 135 | return item 136 | 137 | 138 | @app.get("/embed/{image}") 139 | async def image_cdn_embed(request: Request, image: str): 140 | embed = cdn.get(image.split(".")[0]) 141 | return pages.TemplateResponse( 142 | "embed.html", 143 | {"request": request, "embed": embed}, 144 | ) 145 | 146 | 147 | @app.get("/assets/{image}") 148 | async def image_assets(image: str): 149 | img = images.get(image) 150 | return Response( 151 | img.read(), 152 | media_type=f"image/{image.split('.')[1]}", 153 | ) 154 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Image(BaseModel): 5 | content: str 6 | filename: str 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-multipart 2 | uvicorn 3 | fastapi 4 | jinja2 5 | deta -------------------------------------------------------------------------------- /static/images/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaceShuttleApp/SpaceShuttle/7b39f31814d8a568b70fcd074bda7b845d547612/static/images/copy.png -------------------------------------------------------------------------------- /static/images/discovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaceShuttleApp/SpaceShuttle/7b39f31814d8a568b70fcd074bda7b845d547612/static/images/discovery.png -------------------------------------------------------------------------------- /static/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaceShuttleApp/SpaceShuttle/7b39f31814d8a568b70fcd074bda7b845d547612/static/images/icon.png -------------------------------------------------------------------------------- /static/images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaceShuttleApp/SpaceShuttle/7b39f31814d8a568b70fcd074bda7b845d547612/static/images/upload.png -------------------------------------------------------------------------------- /static/scripts/dashboard.js: -------------------------------------------------------------------------------- 1 | let shareButtons = document.getElementsByClassName("share-button"); 2 | 3 | for (let button of shareButtons) { 4 | button.addEventListener("click", () => { 5 | let url = `${window.location.origin}/cdn/${button.id}` 6 | fetch(`/data/${button.id.split(".")[0]}`) 7 | .then((res) => res.json()) 8 | .then((data) => { 9 | if (data.visibility == false) { 10 | alert("Image is private. You can't share it."); 11 | } else { 12 | navigator.clipboard.writeText(url) 13 | .then(() => { 14 | alert("Link copied to clipboard."); 15 | }); 16 | } 17 | }) 18 | }); 19 | } -------------------------------------------------------------------------------- /static/scripts/info.js: -------------------------------------------------------------------------------- 1 | let imginfo = document.getElementById("imginfo"); 2 | let imgId = imginfo.innerHTML.split(".")[0]; 3 | let deleteButton = document.getElementById("delete"); 4 | let visibilityToggle = document.getElementById("visibility"); 5 | let esaveButton = document.getElementsByClassName("esave-button")[0]; 6 | 7 | deleteButton.addEventListener("click", () => { 8 | fetch(`/delete/${imgId}`, { method: "DELETE" }).then(() => { 9 | window.location.href = "/"; 10 | }); 11 | }); 12 | 13 | visibilityToggle.addEventListener("click", () => { 14 | fetch(`/data/${imgId}`) 15 | .then((res) => res.json()) 16 | .then((data) => { 17 | if (data.visibility == false) { 18 | fetch(`/update/${imgId}?visibility=${true}`, { method: "PATCH" }) 19 | .then(() => { 20 | visibilityToggle.innerHTML = ` Public`; 21 | }); 22 | } else { 23 | fetch(`/update/${imgId}?visibility=${false}`, { method: "PATCH" }) 24 | .then(() => { 25 | visibilityToggle.innerHTML = ` Private`; 26 | }); 27 | } 28 | }) 29 | }); 30 | 31 | let intialColor = "000000" 32 | let colourInput = document.getElementById("ecolour") 33 | colourInput.addEventListener("change", () => { 34 | intialColor = colourInput.value 35 | }) 36 | 37 | esaveButton.addEventListener("click", () => { 38 | let title = document.getElementById("einput") 39 | let url = `${window.location.origin}/embed/${esaveButton.id}` 40 | fetch(`/update/${imgId}?visibility=${true}&embed_title=${title.value}&embed_colour_hex=${intialColor.replace("#", " ")}`, { 41 | method: "PATCH" 42 | }).then(() => { 43 | navigator.clipboard.writeText(url) 44 | .then(() => { 45 | alert("Embed saved and link copied to clipboard."); 46 | }); 47 | }) 48 | }); -------------------------------------------------------------------------------- /static/scripts/script.js: -------------------------------------------------------------------------------- 1 | function toggleColorMode() { 2 | setColorMode(document.documentElement.getAttribute("color-mode") === "dark" ? "light" : "dark"); 3 | } 4 | 5 | function setColorMode(mode) { 6 | document.documentElement.setAttribute("color-mode", mode); 7 | localStorage.setItem("color-mode", mode); 8 | colorToggleButton.innerText = mode === "dark" ? "🌙" : "☀️"; 9 | } 10 | 11 | const colorToggleButton = document.getElementById("color-mode-toggle"); 12 | colorToggleButton.addEventListener("click", toggleColorMode); 13 | // Initialize the color mode. 14 | if (localStorage.getItem("color-mode")) { 15 | setColorMode(localStorage.getItem("color-mode")); 16 | } else { 17 | const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)"); 18 | if (colorSchemeQueryList.matches) { 19 | setColorMode("dark"); 20 | } 21 | } -------------------------------------------------------------------------------- /static/scripts/upload.js: -------------------------------------------------------------------------------- 1 | let uploadButton = document.getElementById("upload-button"); 2 | let hiddenInput = document.getElementById("files"); 3 | let image = document.getElementById("placeholder"); 4 | let viewButton = document.getElementById("view-button"); 5 | let matrix = document.getElementById("matrix"); 6 | let contextImageId = null; 7 | 8 | function handleFile(file) { 9 | let reader = new FileReader(); 10 | reader.readAsDataURL(file); 11 | reader.onloadend = () => { 12 | uploadButton.disabled = true; 13 | image.src = reader.result; 14 | let data = { content: reader.result, filename: file.name }; 15 | fetch(`/upload`, { 16 | method: "POST", 17 | headers: { "Content-Type": "application/json" }, 18 | body: JSON.stringify(data), 19 | }) 20 | .then((response) => response.json()) 21 | .then((resp) => { 22 | contextImageId = resp.id; 23 | matrix.hidden = false; 24 | }); 25 | }; 26 | } 27 | 28 | uploadButton.addEventListener("click", () => { 29 | hiddenInput.click(); 30 | }); 31 | 32 | hiddenInput.addEventListener("change", () => { 33 | let file = hiddenInput.files[0]; 34 | if (file) { 35 | handleFile(file); 36 | } 37 | }); 38 | 39 | viewButton.addEventListener("click", () => { 40 | window.location.href = `/info/${contextImageId}`; 41 | }); 42 | 43 | // detect file paste 44 | document.addEventListener("paste", (event) => { 45 | let file = event.clipboardData.files[0]; 46 | if (file && file.type.startsWith("image/")) { 47 | handleFile(file); 48 | } 49 | }); 50 | 51 | // file drop callback 52 | function dropHandler(event) { 53 | event.preventDefault(); 54 | if (event.dataTransfer.items) { 55 | let file = event.dataTransfer.items[0].getAsFile(); 56 | if (file.type.startsWith("image/")) { 57 | handleFile(file); 58 | } 59 | } 60 | } 61 | 62 | // overrided default file drop event 63 | function dragOverHandler(event) { 64 | event.preventDefault(); 65 | } -------------------------------------------------------------------------------- /static/styles/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --gray-50: #fafafa; 3 | --gray-100: #f5f5f5; 4 | --gray-200: #eeeeee; 5 | --gray-300: #e0e0e0; 6 | --gray-400: #bdbdbd; 7 | --gray-500: #9e9e9e; 8 | --gray-600: #757575; 9 | --gray-700: #616161; 10 | --gray-800: #424242; 11 | --gray-900: #212121; 12 | --pink-light: #ef6bbc; 13 | --pink-dark: #ea3aaa; 14 | } 15 | 16 | :root[color-mode="light"] { 17 | --surface1: var(--gray-50); 18 | --surface2: var(--gray-100); 19 | --surface3: var(--gray-200); 20 | --element1: var(--gray-900); 21 | --element2: var(--gray-800); 22 | --element3: var(--gray-700); 23 | } 24 | 25 | :root[color-mode="dark"] { 26 | --surface1: var(--gray-900); 27 | --surface2: var(--gray-800); 28 | --surface3: var(--gray-700); 29 | --element1: var(--gray-50); 30 | --element2: var(--gray-100); 31 | --element3: var(--gray-200); 32 | } 33 | 34 | body { 35 | font-family: "Open Sans", sans-serif; 36 | color: var(--element1); 37 | background-color: var(--surface1); 38 | display: flex; 39 | flex-direction: column; 40 | align-items: center; 41 | } 42 | 43 | footer { 44 | position: fixed; 45 | bottom: 0; 46 | font-size: 12px; 47 | color: #9e9e9e; 48 | text-align: center; 49 | } 50 | 51 | hr { 52 | visibility: hidden; 53 | } 54 | 55 | .button { 56 | color: var(--gray-900) !important; 57 | background-color: var(--gray-400) !important; 58 | text-decoration: none; 59 | font-size: 16px; 60 | padding: 6px 20px; 61 | border-radius: 10px; 62 | transition: 0.1s linear; 63 | user-select: none; 64 | cursor: pointer; 65 | border: none; 66 | } 67 | 68 | .button img { 69 | object-fit: contain; 70 | } 71 | 72 | .button:hover { 73 | background-color: var(--pink-light) !important; 74 | } 75 | 76 | .button:active { 77 | background-color: var(--pink-dark) !important; 78 | } 79 | 80 | .main-container { 81 | width: 500px; 82 | display: flex; 83 | flex-direction: column; 84 | } 85 | 86 | .header-container { 87 | display: flex; 88 | flex-direction: row; 89 | align-items: center; 90 | } 91 | 92 | .header-container .button { 93 | margin-left: auto; 94 | font-size: 20px; 95 | font-weight: bold; 96 | } 97 | 98 | .card-container { 99 | background-color: var(--surface2); 100 | border: 2px solid var(--surface3); 101 | transition: 0.1s linear; 102 | border-radius: 10px; 103 | padding: 4px; 104 | display: flex; 105 | } 106 | 107 | .card-container:hover { 108 | background-color: var(--surface3); 109 | } 110 | 111 | .card-details { 112 | font-size: larger; 113 | font-weight: bold; 114 | margin: 10px; 115 | display: flex; 116 | flex-direction: column; 117 | align-items: flex-start; 118 | justify-content: space-around; 119 | } 120 | 121 | .card-image { 122 | border-radius: 10px; 123 | height: 100px; 124 | margin-left: auto; 125 | } 126 | 127 | .container { 128 | display: flex; 129 | flex-direction: column; 130 | align-items: center; 131 | justify-content: center; 132 | } 133 | 134 | .container code { 135 | font-size: 20px; 136 | margin: 20px; 137 | } 138 | 139 | .info-container { 140 | display: flex; 141 | font-size: 20px; 142 | } 143 | 144 | .embed-creator { 145 | border-radius: 10px; 146 | text-decoration: none; 147 | background-color: var(--surface1); 148 | border: 2px solid var(--surface3) !important; 149 | padding: 4px 24px; 150 | text-align: center; 151 | } 152 | 153 | .embed-left { 154 | float: left; 155 | align-items: left; 156 | vertical-align: middle; 157 | } 158 | 159 | .embed-right { 160 | float: right; 161 | align-items: right; 162 | vertical-align: middle; 163 | } 164 | 165 | .embed-input { 166 | float: left; 167 | width: 150px; 168 | height: 20px; 169 | padding: 4px 24px; 170 | border-radius: 10px; 171 | color: var(--element1); 172 | background-color: var(--surface2); 173 | border: 2px solid var(--surface3) !important; 174 | } 175 | 176 | .embed-input ::placeholder { 177 | color: var(--element1); 178 | font-style: italic; 179 | } 180 | 181 | .embed-colour { 182 | float: left; 183 | width: 200px; 184 | height: 150px; 185 | background-color: var(--surface2); 186 | border: 1px solid var(--surface3) !important; 187 | border-radius: 10px; 188 | padding: 6px 20px; 189 | } -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block head %} 6 | SpaceShuttle 7 | 8 | 9 | 10 | 11 | 12 | {% endblock %} 13 | 14 | 15 | 16 | {% block body %} {% endblock 17 | %} 18 | 19 | 20 | {% block scripts %} 21 | 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block body %} 2 |
3 |
4 |

Your Images

5 | 6 | 7 | 8 |
9 | {% for item in items %} 10 |
11 |
12 | {{ item["key"] }}.{{ item["ext"] }} 13 | 19 |
20 | 21 |
22 |
{% endfor %} 23 |
24 | {% endblock %} {% block scripts %} {{ super() }} 25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /templates/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image -------------------------------------------------------------------------------- /templates/info.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |
5 |
6 |

{{ data["key"] }}.{{ data["ext"] }}

7 | Back 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 |
18 | 20 |
21 | 23 |
24 |
25 |
26 |

Embed Builder

27 |
28 | 29 | Save and Share 30 | 31 |
32 |
33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | {% endblock %} 45 | 46 | {% block scripts %} 47 | {{ super() }} 48 | 49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block body %} 2 |
3 |
4 |

Upload Images

5 | Back 6 |
7 |
8 | 12 | Drop - Paste - Upload 13 | 16 |
17 |
18 | {% endblock %} {% block scripts %} {{ super() }} 19 | 20 | {% endblock %} --------------------------------------------------------------------------------