Your Images
5 | 6 | 7 | 8 |{% endfor %} 23 |
├── .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 | 
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 | 
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 |
Drop - Paste - Upload
13 |