├── .env ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── app.py ├── dazzle-docs.md ├── dazzle.py ├── install_reqs.py ├── requirements.txt ├── static ├── Jost.ttf ├── PlayFairDisplay.ttf ├── VT323.ttf ├── linux-users-installing-browser.gif ├── master-styles.css ├── scanlines.png ├── styles-aurora.css ├── styles-choco.css ├── styles-gruvbox.css ├── styles-hackerman.css ├── styles-ice.css ├── styles-newspaper.css ├── styles-nord.css └── styles-voidzero.css └── templates ├── _banner.html ├── _base.html ├── _dl_mockup.html ├── _download.html ├── _error.html ├── _footer.html ├── dlm.html ├── download.html ├── editor.html ├── forum-topic.html ├── forum-topics.html ├── forums.html ├── img.html ├── index.html ├── loggedin.html ├── login.html ├── profile.html ├── projects-scratch.html ├── projects.html ├── scratchapi-error.html ├── scratchdb-error.html ├── search.html ├── settings.html ├── studio.html ├── suggestions-post.html └── trending.html /.env: -------------------------------------------------------------------------------- 1 | # change this to change where Dazzle stores its archive 2 | DAZZLE_DIR=".dazzle-archive" 3 | 4 | # change SERVER_MODE to "yes" if running a public server 5 | SERVER_MODE="no" 6 | 7 | # Running in Replit? 8 | REPLIT_MODE="no" 9 | 10 | 11 | USE_SCRATCHDB="yes" 12 | 13 | # change this to the IP:PORT that you're going to expose 14 | SERVER_HOST="localhost:3000" 15 | 16 | # Debug mode 17 | DEBUG="yes" 18 | 19 | # Run flask in debug mode 20 | FLASK_DEBUG="yes" 21 | 22 | DB_LOCATION="users.sqlite" 23 | DB_TABLE="users" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS moment 2 | .DS_Store 3 | 4 | # Dazzle archive files 5 | .dazzle-archive/ 6 | users.sqlite 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/#use-with-ide 117 | .pdm.toml 118 | 119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 120 | __pypackages__/ 121 | 122 | # Celery stuff 123 | celerybeat-schedule 124 | celerybeat.pid 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | # .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | 162 | # PyCharm 163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 165 | # and can be added to the global gitignore or merged into this file. For a more nuclear 166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 167 | #.idea/ 168 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, SnarpleDev 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snazzle 2 | 3 | A better frontend for Scratch, built by the community, for the community 4 | 5 | Snazzle is the original attempt at a better Scratch website. It aims to be feature-rich and easy and quick to use, incorporating many things that the Scratch community has been wanting for years. 6 | 7 | Basically, this is a Scratch website just for power users. 8 | 9 | > If you're more than a casual user of the Scratch website for whatever reason, then you'll like Snazzle. 10 | > It's like a giant vat of coffee brewed with ingredients from the Scratch community. 11 | -- Snazzle's homepage 12 | 13 | ## Contributing 14 | 15 | Format your code with [Black](https://github.com/psf/black) and make a pull request. If there is a feature branch for what you are changing, make the PR to that branch instead of main. 16 | 17 | ## Running your own instance locally 18 | 19 | Since Snazzle is very much still in development, this won't be representative of the final product's build steps. 20 | 21 | But for now, this is how you do it: 22 | 23 | 1. Clone the repo 24 | 2. (optional but recommended) Create a Python virtual environment. Snazzle requires Python 3.8+. However, we recommend 3.11+ as this version has better error messages that will let us diagnose issues better if you submit a bug report. 25 | 3. If you are on an Arch(-based) linux distro, you will have to run `sudo pacman -S python-flask` to install Flask. 26 | > Deps are installed automatically since [commit `822d869`](https://github.com/SnarpleDev/Snazzle/commit/822d869fc80f9a7275c2aaf85f0b084f1572db63). 27 | 4. Once deps are installed, run `flask run --with-threads`. This will set up a Flask server at `127.0.0.1:5000`. If you find any bugs, please report them. 28 | > The `--with-threads` option is basically required if you want Snazzle to run fast. 29 | 5. Go to `127.0.0.1:5000` in your favourite browser and play around with it! 30 | 31 | ## Hosting on Replit 32 | We've discontinued the repl you used to have to fork a while ago because of updates to replit. Please host an instance somewhere else. (Sorry, it's not our fault.) 33 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Our current version support policy aims to maintain the latest application and web versions while ensuring financial sustainability. We support older versions until 85% of active users migrate to the new version. After this threshold, the old version will be deprecated and cease to function. 6 | 7 | Internal minor updates to the application or web app will be automatically applied during launch. Major updates to the application will require a fresh installation of the new version and will only be available for current versions. Experimental versions will be immediately deprecated upon the release of a stable version. 8 | 9 | Please note that Snazzle, Snarple, members of the Snarple team, and any affiliates are not liable for any damage or failure to property resulting from the use or misuse of our software. 10 | 11 | ## Reporting a Vulnerability 12 | 13 | **Do not report vulnerabilities in public GitHub issues.** 14 | 15 | Please report vulnerabilities through the GitHub security panel by navigating to the Advisories section. We will respond to your report within 24 hours. 16 | 17 | ### Preferred Language 18 | 19 | We recommend using English to report vulnerabilities. If necessary, please use a translation service to convert your report from your preferred language to English. 20 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """ 2 | **** Snazzle Server Code **** 3 | Made in Flask by Snarple over at 4 | https://github.com/SnarpleDev/Snazzle/ 5 | """ 6 | 7 | # automatically install deps 8 | import install_reqs 9 | 10 | from os import listdir 11 | from sys import version_info, exit 12 | from datetime import timedelta 13 | from flask import ( 14 | Flask, 15 | render_template, 16 | stream_template, 17 | make_response, 18 | request, 19 | redirect, 20 | ) 21 | import requests 22 | import webbrowser 23 | 24 | from werkzeug import exceptions as werkexcept 25 | 26 | import dazzle 27 | 28 | REPLIT_MODE = True if dazzle.env["REPLIT_MODE"] == "yes" else False 29 | USE_SCRATCHDB = True if dazzle.env["USE_SCRATCHDB"] == "yes" else False 30 | HOST, PORT = dazzle.env["SERVER_HOST"].split(":") 31 | DEBUG = True if dazzle.env["DEBUG"] == "yes" else False 32 | FLASK_DEBUG = True if dazzle.env["FLASK_DEBUG"] == "yes" else False 33 | 34 | app = Flask(__name__) 35 | 36 | if not (version_info.major == 3 and version_info.minor >= 8): 37 | print( 38 | """Snazzle does not work on Python 3.7 or lower. Please upgrade. 39 | 3.8 is old now anyways, and you should be using the latest version of Python unless you absolutely cannot.""" 40 | ) 41 | exit(0) 42 | 43 | user_data = dict( 44 | theme="choco", 45 | user_name="CoolScratcher123", 46 | pinned_subforums=[], 47 | saved_posts=[], 48 | max_topic_posts=20, 49 | show_deleted_posts=True, 50 | ocular_ov=True, # for 'ocular override' 51 | signed_in=True, 52 | use_sb2=False, 53 | sb_scale=1, 54 | use_old_layout=False, 55 | ) 56 | 57 | 58 | dazzle.use_scratchdb(True) 59 | 60 | global get_status 61 | get_status = dazzle.get_ocular 62 | 63 | 64 | def get_themes(): 65 | return [ 66 | item[7 : item.index(".css")] 67 | for item in listdir("static") 68 | if item.startswith("styles-") and item.endswith(".css") 69 | ] 70 | 71 | 72 | # https://stackoverflow.com/questions/34066804/disabling-caching-in-flask 73 | @app.after_request 74 | def add_header(r): 75 | """ 76 | Stops forum pages from being cached so that they actually work correctly 77 | """ 78 | if "/forums" in request.url or "/settings" in request.url: 79 | r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 80 | r.headers["Pragma"] = "no-cache" 81 | r.headers["Expires"] = "0" 82 | return r 83 | 84 | 85 | subforums_data = ( 86 | ("Welcome", ["Announcements", "New Scratchers"]), 87 | ( 88 | "Making Scratch Projects", 89 | [ 90 | "Help with Scripts", 91 | "Show and Tell", 92 | "Project Ideas", 93 | "Collaboration", 94 | "Requests", 95 | "Project Save & Level Codes", 96 | ], 97 | ), 98 | ( 99 | "About Scratch", 100 | [ 101 | "Questions about Scratch", 102 | "Suggestions", 103 | "Bugs and Glitches", 104 | "Advanced Topics", 105 | "Connecting to the Physical World", 106 | "Scratch Extensions", 107 | "Open Source Projects", 108 | ], 109 | ), 110 | ( 111 | "Interests Beyond Scratch", 112 | ["Things I'm Making and Creating", "Things I'm Reading and Playing"], 113 | ), 114 | ) 115 | 116 | 117 | @app.context_processor 118 | def context(): 119 | # Play with this and the user_data dict to manipulate app state 120 | username, signed = None, False 121 | if request.cookies.get("snazzle-token"): 122 | matched = dazzle.token_matches_user(request.cookies.get("snazzle-token")) 123 | signed = len(matched) == 1 124 | username = matched[0][0] if signed else None 125 | return dict( 126 | theme=user_data["theme"], 127 | username=username or user_data["user_name"], 128 | signed_in=signed, 129 | to_str=str, 130 | get_author_of=dazzle.get_author_of, 131 | len=len, 132 | host=HOST, 133 | get_status=get_status, 134 | sb_scale=user_data["sb_scale"], 135 | use_sb2=user_data["use_sb2"], 136 | ) 137 | 138 | 139 | @app.get("/") 140 | def index(): 141 | """ 142 | The home page of the app 143 | """ 144 | projects = dazzle.get_featured_projects() 145 | trending = dazzle.get_trending_projects() 146 | return stream_template( 147 | "index.html", 148 | featured_projects=projects["community_featured_projects"], 149 | featured_studios=projects["community_featured_studios"], 150 | trending_projects=trending, 151 | loving=projects["community_most_loved_projects"], 152 | remixed=projects["community_most_remixed_projects"], 153 | ) 154 | 155 | 156 | @app.get("/trending") 157 | def trending(): 158 | """ 159 | Explore page. 160 | """ 161 | projects = dazzle.get_trending_projects() 162 | if filter := request.args.get("filter"): 163 | return render_template("trending.html", filter=filter) 164 | return stream_template("trending.html") 165 | 166 | @app.get("/api/trending/") 167 | def get_trending(): 168 | """ 169 | Gets trending projects and returns them as json 170 | 171 | Requires a `page` argument in url 172 | """ 173 | return dazzle.get_trending_projects(40, int(request.args.get("page"))) 174 | 175 | @app.get("/forums") 176 | def categories(): 177 | """ 178 | A page that lists all the subforums in the forums 179 | """ 180 | return render_template( 181 | "forums.html", 182 | data=subforums_data, 183 | pinned_subforums=user_data["pinned_subforums"], 184 | legacy_layout=user_data["use_old_layout"], 185 | ) 186 | 187 | 188 | @app.get("/forums/") 189 | def topics(subforum): 190 | """ 191 | A page that lists all the topics in the subforum 192 | """ 193 | 194 | sf_page = request.args.get("page") 195 | 196 | try: 197 | response = dazzle.get_topics(subforum, sf_page) 198 | except requests.exceptions.ReadTimeout: 199 | return render_template( 200 | "_error.html", 201 | errdata={ 202 | "code": 500, 203 | "name": "Internal server error", 204 | "description": "A request took too long and has been aborted. Please try again later, or check your internet connection.", 205 | }, 206 | ) 207 | 208 | if response["error"]: 209 | ErrorMessage = response["message"] if "message" in response.keys() else response["error"] 210 | print("Error when loading", subforum, sf_page, ErrorMessage) 211 | return render_template("scratchdb-error.html", err=ErrorMessage) 212 | return stream_template( 213 | "forum-topics.html", 214 | subforum=subforum, 215 | topics=response["topics"], 216 | pinned_subforums=user_data["pinned_subforums"], 217 | url=request.url, 218 | str=str, 219 | topic_page=int(sf_page), 220 | len=len, 221 | ) 222 | 223 | 224 | @app.get("/forums/topic/") 225 | def topic(topic_id): 226 | """ 227 | Shows all posts in a topic. 228 | """ 229 | 230 | if post_to_save := request.args.get("save"): 231 | user_data["saved_posts"].append((topic_id, post_to_save)) 232 | show_deleted_posts = user_data["show_deleted_posts"] 233 | 234 | topic_page = request.args.get("page") 235 | 236 | topic_data = dazzle.get_topic_data(topic_id) 237 | topic_posts = dazzle.get_topic_posts(topic_id, page=topic_page) 238 | 239 | if topic_data["error"]: 240 | ErrorMessage = topic_data["message"] if "message" in topic_data.keys() else topic_data["error"] 241 | print("Error when loading", topic_id, ErrorMessage) 242 | return render_template("scratchdb-error.html", err=ErrorMessage) 243 | if topic_posts["error"]: 244 | ErrorMessage = topic_posts["message"] if "message" in topic_posts.keys() else topic_posts["error"] 245 | print("Error when loading", topic_id, ErrorMessage) 246 | return render_template("scratchdb-error.html", err=ErrorMessage) 247 | data_dict = { 248 | "topic_id": topic_id, 249 | "topic_title": topic_data["data"]["title"], 250 | "topic_posts": [ 251 | {"author_status": get_status(post["author"]), **post} 252 | for post in topic_posts["posts"] 253 | ], 254 | "topic_page": int(topic_page), 255 | "max_posts": user_data["max_topic_posts"], 256 | "show_deleted": show_deleted_posts, 257 | "list": list, 258 | "len": len, 259 | "get_pfp": dazzle.get_pfp_url, 260 | "get_status": get_status, 261 | } 262 | 263 | # if topic_id == 'Suggestions': 264 | # return stream_template( 265 | # "suggestions-post.html", 266 | # **data_dict 267 | # ) 268 | # else: 269 | return stream_template("forum-topic.html", **data_dict) 270 | 271 | 272 | @app.get("/img-fullscreen") 273 | def img(): 274 | return render_template("img.html", img_url=request.args.get("url")) 275 | 276 | 277 | @app.get("/projects/scratch/") 278 | def scratchproject(project_id): 279 | return stream_template("projects-scratch.html", project_id=project_id) 280 | 281 | 282 | @app.get("/projects/") 283 | def project(project_id): 284 | global user_data 285 | project_info = dazzle.get_project_info(project_id) 286 | if "error" in project_info.keys(): 287 | ErrorMessage = project_info["message"] if "message" in project_info.keys() else project_info["error"] 288 | print("Error when loading", project_id, ErrorMessage) 289 | return render_template("scratchdb-error.html", err=ErrorMessage) 290 | try: 291 | project_name = project_info["title"] 292 | creator_name = project_info["username"] 293 | except Exception: 294 | try: 295 | project_name = project_info["title"] 296 | creator_name = project_info["author"]["username"] 297 | except Exception: 298 | project_name = "A scratch project..." 299 | creator_name = "A scratch user..." 300 | theme = user_data["theme"] 301 | if theme == "choco": # add new elif at the end 302 | colour = "%23282320" 303 | elif theme == "hackerman": 304 | colour = "%23212820" 305 | elif theme == "ice": 306 | colour = "%23202d38" 307 | elif theme == "newspaper": 308 | colour = "%23c8c8c8" 309 | elif theme == "nord": 310 | colour = "%232e3440" 311 | elif theme == "gruvbox": 312 | colour = "%23282828" 313 | # elif theme == "new theme": 314 | # colour = "%23[hex colour]" 315 | ocular = dazzle.get_ocular(creator_name) 316 | ocular_colour = ocular["color"] 317 | if ocular_colour in (None, "null"): 318 | ocular_colour = "#999999" 319 | else: 320 | creator_name = str(creator_name) + " ●" # add the dot 321 | ocular_colour = f"color:{ocular_colour}" 322 | if not DEBUG: 323 | return stream_template( 324 | "projects.html", 325 | project_id=project_id, 326 | colour=colour, 327 | name=project_name, 328 | creator_name=creator_name, 329 | ocularcolour=ocular_colour, 330 | ) 331 | else: 332 | # scomments = scratchdb.get_comments(project_id) 333 | return stream_template( 334 | "projects.html", 335 | project_id=project_id, 336 | colour=colour, 337 | name=project_name, 338 | creator_name=creator_name, 339 | # comments=[{"username": comment["author"]["username"], "content": comment["content"], "visibility": comment["visibility"]} for comment in scomments], 340 | ocularcolour=ocular_colour, 341 | ) 342 | 343 | 344 | @app.route("/settings", methods=["GET"]) 345 | def settings(): 346 | """ 347 | Settings page. 348 | Change theme, status, link github account 349 | """ 350 | for key, value in request.args.items(): 351 | user_data[key.replace("-", "_")] = value 352 | return render_template( 353 | "settings.html", 354 | themes=get_themes(), 355 | str_title=str.title, 356 | values=[ 357 | user_data["theme"], 358 | user_data["max_topic_posts"], 359 | user_data["show_deleted_posts"], 360 | user_data["ocular_ov"], 361 | user_data["use_sb2"], 362 | user_data["use_old_layout"], 363 | ], 364 | ) 365 | 366 | 367 | @app.get("/downloads") 368 | def downloads(): 369 | """old download page""" 370 | return render_template("download.html") 371 | 372 | 373 | @app.get("/secret/dl_mockup") 374 | def dl_mockup(): 375 | return render_template("dlm.html") 376 | 377 | 378 | @app.get("/pin-subforum/") 379 | def pin_sub(sf): 380 | """route that pins a subforum""" 381 | 382 | def flatten_comprehension(matrix): 383 | return [item for row in matrix for item in row] 384 | 385 | if sf not in flatten_comprehension( 386 | [subforum for subforum in [subforums for _, subforums in subforums_data]] 387 | ): 388 | return '' 389 | 390 | if sf not in user_data["pinned_subforums"]: 391 | arr = user_data["pinned_subforums"].copy() 392 | arr.append(sf) 393 | user_data["pinned_subforums"] = arr 394 | return "" 395 | else: 396 | return '' 397 | 398 | 399 | @app.get("/unpin-subforum/") 400 | def unpin_sub(sf): 401 | """route that unpins a subforum""" 402 | if sf in user_data["pinned_subforums"]: 403 | arr = user_data["pinned_subforums"].copy() 404 | arr.remove(sf) 405 | user_data["pinned_subforums"] = arr 406 | return "" 407 | 408 | 409 | @app.get("/handle-scratch-auth") 410 | def scratch_auth(): 411 | if not request.args: 412 | try: 413 | if sa_login := dazzle.get_redirect_url(): 414 | return redirect(sa_login) 415 | except SyntaxError: 416 | return render_template("") 417 | return "" 418 | code = request.args.get("privateCode") 419 | session_id = dazzle.login(code) 420 | response = make_response( 421 | "

Login successful

" 422 | ) 423 | response.set_cookie("snazzle-token", session_id, timedelta(days=30)) 424 | 425 | return response 426 | 427 | 428 | @app.route("/search", methods=["POST", "GET"]) # search feature 429 | def search(): 430 | query = request.form["query"] 431 | result = dazzle.search_for_projects(query) 432 | return stream_template("search.html", result=result, query=query) 433 | 434 | 435 | # Studio pages 436 | @app.get("/studios//") 437 | def studios(id, tab): 438 | data = dazzle.get_studio_data(id) 439 | if "error" in data.keys(): 440 | ErrorMessage = data["message"] if "message" in data.keys() else data["error"] 441 | print("Error when loading", id, ErrorMessage) 442 | return render_template("scratchapi-error.html", message=ErrorMessage) 443 | 444 | return render_template( 445 | "studio.html", 446 | studio_name=data["title"], 447 | studio_description=data["description"], 448 | studio_id=id, 449 | studio_tab=tab, 450 | studio_banner=data["image"], 451 | studio_stats=data["stats"], 452 | name_len=len(data["title"]), 453 | ) 454 | 455 | 456 | @app.errorhandler(werkexcept.NotFound) 457 | def err404(e: Exception): 458 | # route for error 459 | return render_template("_error.html", errdata=e), 404 460 | 461 | 462 | # CHANGE THIS IF YOU'RE RUNNING A PUBLIC SERVER 463 | if __name__ == "__main__": 464 | dazzle.init_db() 465 | webbrowser.open(f"{HOST}:{PORT}", 2) 466 | app.run(host=HOST, port=PORT, debug=FLASK_DEBUG) 467 | -------------------------------------------------------------------------------- /dazzle-docs.md: -------------------------------------------------------------------------------- 1 | # Dazzle library 2 | 3 | Dazzle is a helper library for Snazzle that helps get things from ScratchDB and the Scratch API. 4 | 5 | ## Functions 6 | 7 | ### fmt_time(timestamp) 8 | Formats a timestamp to a human-readable format. 9 | 10 | - `timestamp` (str): The timestamp to format. 11 | 12 | ### set_server_host(host) 13 | Sets the server host for the application. 14 | 15 | - `host` (str): The host to set. 16 | 17 | ### use_scratchdb(value) 18 | Force ScratchDB usage. 19 | 20 | - `value` (bool): True to force ScratchDB usage, False otherwise. 21 | 22 | ### replit_mode(value) 23 | Enable Replit mode for Snazzle usage on Replit. 24 | 25 | - `value` (bool): True to enable Replit mode, False otherwise. 26 | 27 | ### use_proxy(value) 28 | Force proxy usage for Snazzle. 29 | 30 | - `value` (bool): True to force proxy usage, False otherwise. 31 | 32 | ### remove_duplicates(input_list) 33 | Removes duplicates from a list. 34 | 35 | - `input_list` (list): The list to remove duplicates from. 36 | 37 | Returns: 38 | - list: A list that is a copy of `input_list`, but with duplicates removed. 39 | 40 | ### get_topics(category, page) 41 | Gets topics in a subforum from ScratchDB. 42 | 43 | - `category` (str): The category to retrieve topics from. 44 | - `page` (int): The page number of topics to retrieve. 45 | 46 | Returns: 47 | - dict: A dictionary containing the error status and topics. 48 | 49 | ### get_post_info(post_id) 50 | Gets information about a forum post from ScratchDB. 51 | 52 | - `post_id` (str): The ID of the post to retrieve information for. 53 | 54 | Returns: 55 | - dict: Information about the forum post. 56 | 57 | ### get_author_of(post_id) 58 | Gets the author of a forum topic. 59 | 60 | - `post_id` (str): The ID of the post to retrieve the author for. 61 | 62 | Returns: 63 | - str: The author of the forum topic. 64 | 65 | ### get_project_info(project_id) 66 | Gets information about a project from ScratchDB. 67 | 68 | - `project_id` (str): The ID of the project to retrieve information for. 69 | 70 | Returns: 71 | - dict: Information about the project. 72 | 73 | ### get_comments(project_id) 74 | Gets comments for a project. 75 | 76 | - `project_id` (str): The ID of the project to retrieve comments for. 77 | 78 | Returns: 79 | - dict: Comments for the project. 80 | 81 | ### get_ocular(username) 82 | Gets a user's status from Ocular. 83 | 84 | - `username` (str): The username of the user. 85 | 86 | Returns: 87 | - dict: User's status information. 88 | 89 | ### get_aviate(username) 90 | Gets a user's status from Aviate. 91 | 92 | - `username` (str): The username of the user. 93 | 94 | Returns: 95 | - str: User's status. 96 | 97 | ### init_db() 98 | Initializes the database. 99 | 100 | ### get_featured_projects() 101 | Retrieves the featured projects from the Scratch API. 102 | 103 | Returns: 104 | - dict: Featured projects. 105 | 106 | ### get_topic_data(topic_id) 107 | Gets data about a topic from ScratchDB. 108 | 109 | - `topic_id` (str): The ID of the topic to retrieve data for. 110 | 111 | Returns: 112 | - dict: Topic data. 113 | 114 | ### get_trending_projects() 115 | Gets trending projects from the Scratch API. 116 | 117 | Returns: 118 | - dict: Trending projects. 119 | 120 | ### get_topic_posts(topic_id, page=0, order="oldest") 121 | Gets posts for a topic. 122 | 123 | - `topic_id` (str): The ID of the topic to retrieve posts for. 124 | - `page` (int): The page number of posts. 125 | - `order` (str): The order of posts (oldest or newest). 126 | 127 | Returns: 128 | - dict: Topic posts. 129 | 130 | ### get_pfp_url(username, size=90) 131 | Gets the profile picture URL for a user. 132 | 133 | - `username` (str): The username of the user. 134 | - `size` (int): The size of the profile picture. 135 | 136 | Returns: 137 | - str: Profile picture URL. 138 | 139 | ### get_redirect_url() 140 | Gets the redirect URL for Scratch Auth. 141 | 142 | Returns: 143 | - str: Redirect URL. 144 | 145 | ### login(code) 146 | Logs in a user with the provided code. 147 | 148 | - `code` (str): The authentication code. 149 | 150 | Returns: 151 | - str: Session ID. 152 | 153 | ### token_matches_user(token) 154 | Checks if the token matches the user. 155 | 156 | - `token` (str): The token to check. 157 | 158 | Returns: 159 | - list: List of matching users. 160 | 161 | ### search_for_projects(q) 162 | Searches for projects. 163 | 164 | - `q` (str): The query string. 165 | 166 | Returns: 167 | - dict: Search results. 168 | 169 | ### get_studio_data(id) 170 | Gets data about a studio. 171 | 172 | - `id` (str): The ID of the studio. 173 | 174 | Returns: 175 | - dict: Studio data. 176 | 177 | ### get_studio_comments(id) 178 | Gets comments for a studio. 179 | 180 | - `id` (str): The ID of the studio. 181 | 182 | Returns: 183 | - None 184 | -------------------------------------------------------------------------------- /dazzle.py: -------------------------------------------------------------------------------- 1 | # Dazzle library 2 | 3 | """ 4 | Dazzle is a helper library for Snazzle that helps get things from ScratchDB and the Scratch API. 5 | """ 6 | 7 | from functools import lru_cache 8 | import base64 9 | 10 | from datetime import datetime 11 | from uuid import uuid4 12 | import sqlite3 13 | 14 | from dotenv import dotenv_values 15 | import requests 16 | 17 | env = dotenv_values(".env") 18 | 19 | SCRATCHDB = "https://scratchdb.lefty.one/v3/" 20 | DAZZLE_DIR = env["DAZZLE_DIR"] if "DAZZLE_DIR" in env else ".dazzle-archive" 21 | ENABLE_SCRATCH_AUTH = True 22 | 23 | useDB = True # False will fetch data from ScratchDB 24 | REPLIT_MODE = False 25 | USE_PROXY = False 26 | 27 | def fmt_time(timestamp: str) -> str: 28 | """ 29 | Formats a timestamp to a human-readable format. 30 | - `timestamp` (str): The timestamp to format. 31 | 32 | Returns: 33 | - str: The formatted timestamp. 34 | """ 35 | return datetime.fromisoformat(timestamp.replace("Z", "+00:00")).strftime("%B %d, %Y at %I:%M %p UTC") 36 | 37 | # TODO: deprecate the config functions because the .env file supersedes them 38 | def set_server_host(host) -> None: 39 | """ 40 | Sets the server host for the application. 41 | 42 | - `host` (str): The host to set. 43 | 44 | NOTE: May be removed in the future as it is not used. 45 | """ 46 | global SERVER_HOST 47 | SERVER_HOST = host 48 | 49 | def use_scratchdb(value: bool) -> None: 50 | """ 51 | Force ScratchDB usage. 52 | 53 | - `value` (bool): True to force ScratchDB usage, False otherwise. 54 | """ 55 | global USE_SDB 56 | USE_SDB = value 57 | 58 | def remove_duplicates(input_list) -> list: 59 | """ 60 | Removes duplicates from a list. 61 | 62 | - `input_list` (list): The list to remove duplicates from. 63 | 64 | Returns: 65 | - list: A list that is a copy of `input_list`, but with duplicates removed. 66 | """ 67 | result_list = [] 68 | for d in input_list: 69 | if d not in result_list: 70 | result_list.append(d) 71 | return result_list 72 | 73 | 74 | @lru_cache(maxsize=15) 75 | def get_topics(category, page) -> dict: 76 | """ 77 | Gets topics in a subforum from ScratchDB. 78 | 79 | - `category` (str): The category to retrieve topics from. 80 | - `page` (int): The amount of pages of topics to retrieve. 81 | 82 | Returns: 83 | - dict: A dictionary containing the error status and topics. 84 | """ 85 | r = requests.get( 86 | f"{SCRATCHDB}forum/category/topics/{category}/{page}?detail=0&filter=1", 87 | timeout=10, 88 | ) 89 | try: 90 | if type(r.json()) != list: 91 | return {"error": True, "message": "sdb_" + r.json()["error"].lower()} 92 | return {"error": False, "topics": remove_duplicates(r.json())} 93 | except requests.exceptions.JSONDecodeError: 94 | return {"error": True, "message": "lib_scratchdbdown"} 95 | 96 | 97 | @lru_cache(maxsize=15) 98 | def get_post_info(post_id) -> dict: 99 | """ 100 | Gets information about a forum post from ScratchDB. 101 | 102 | - `post_id` (str): The ID of the post to retrieve information for. 103 | 104 | Returns: 105 | - dict: Information about the forum post. 106 | """ 107 | r = requests.get(f"{SCRATCHDB}forum/post/info/{post_id}", timeout=10) 108 | return r.json() 109 | 110 | 111 | def get_author_of(post_id) -> str: 112 | """ 113 | Gets the author of a forum topic. 114 | 115 | - `post_id` (str): The ID of the post to retrieve the author for. 116 | 117 | Returns: 118 | - str: The author of the forum topic. 119 | """ 120 | return "user" 121 | # r = requests.get(f'{SCRATCHDB}forum/post/info/{post_id}') 122 | # return r.json()['username'] 123 | 124 | 125 | @lru_cache(maxsize=15) 126 | def get_project_info(project_id) -> dict: 127 | """ 128 | Gets information about a project from ScratchDB. 129 | 130 | - `project_id` (str): The ID of the project to retrieve information for. 131 | 132 | Returns: 133 | - dict: Information about the project. 134 | """ 135 | try: 136 | if not useDB: 137 | r = requests.get( 138 | f"https://scratchdb.lefty.one/v2/project/info/id/{project_id}", timeout=10 139 | ) 140 | else: 141 | r = requests.get( 142 | f"https://api.scratch.mit.edu/projects/{project_id}", timeout=10 143 | ) 144 | return r.json() 145 | except Exception: 146 | return {"error": True, "message": "lib_scratchdbtimeout"} 147 | 148 | @lru_cache(maxsize=15) 149 | def get_comments(project_id: str) -> dict: 150 | """ 151 | Gets comments for a project. 152 | 153 | - `project_id` (str): The ID of the project to retrieve comments for. 154 | 155 | Returns: 156 | - dict: Comments for the project. 157 | """ 158 | if not REPLIT_MODE: 159 | return None # i'll do this later 160 | try: 161 | project_creator = requests.get( 162 | f"https://api.scratch.mit.edu/projects/{project_id}", timeout=10 163 | ).json()["author"]["username"] 164 | except Exception: 165 | return Exception 166 | r = requests.get( 167 | f"https://api.scratch.mit.edu/users/{project_creator}/projects/{project_id}/comments?limit=40", 168 | timeout=10, 169 | ) 170 | return r.json() 171 | 172 | 173 | @lru_cache(maxsize=5) 174 | def get_ocular(username) -> dict: 175 | """ 176 | Gets a user's status from Ocular. 177 | 178 | - `username` (str): The username of the user. 179 | 180 | Returns: 181 | - dict: User's status information. 182 | """ 183 | try: 184 | info = requests.get( 185 | f"https://my-ocular.jeffalo.net/api/user/{username}", timeout=10 186 | ) 187 | info.json()["name"] 188 | except KeyError: 189 | return { 190 | "name": None, 191 | "status": None, 192 | "color": None, 193 | } # i had to spell it the 'murican way for it to work 194 | return info.json() 195 | 196 | def init_db() -> None: 197 | """ 198 | Initializes the database. 199 | """ 200 | conn = sqlite3.connect(env["DB_LOCATION"]) 201 | conn.cursor().execute( 202 | f"CREATE TABLE IF NOT EXISTS {env['DB_TABLE']}( username, token )" 203 | ) 204 | conn.close() 205 | 206 | def get_featured_projects() -> dict: 207 | """ 208 | Retrieves the featured projects from the Scratch API. 209 | 210 | Returns: 211 | - dict: Featured projects. 212 | """ 213 | r = requests.get("https://api.scratch.mit.edu/proxy/featured", timeout=10) 214 | return r.json() 215 | 216 | 217 | @lru_cache(maxsize=15) 218 | def get_topic_data(topic_id) -> dict: 219 | """ 220 | Gets data about a topic from ScratchDB. 221 | 222 | - `topic_id` (str): The ID of the topic to retrieve data for. 223 | 224 | Returns: 225 | - dict: Topic data. 226 | """ 227 | r = requests.get(f"{SCRATCHDB}forum/topic/info/{topic_id}", timeout=10) 228 | try: 229 | if "error" in r.json().keys(): 230 | return {"error": True, "message": "sdb_" + r.json()["error"].lower()} 231 | return {"error": False, "data": r.json()} 232 | except requests.exceptions.JSONDecodeError: 233 | return {"error": True, "message": "lib_scratchdbdown"} 234 | 235 | def get_topic_posts(topic_id, page=0, order="oldest") -> dict: 236 | """ 237 | Gets posts for a topic. 238 | 239 | - `topic_id` (str): The ID of the topic to retrieve posts for. 240 | - `page` (int): The page number of posts. 241 | - `order` (str): The order of posts (oldest or newest). 242 | 243 | Returns: 244 | - dict: Topic posts. 245 | """ 246 | r = requests.get( 247 | f"{SCRATCHDB}forum/topic/posts/{topic_id}/{page}?o={order}", timeout=10 248 | ) 249 | # post['author'], post['time'], post['html_content'], post['index'], post['is_deleted'] 250 | try: 251 | if not isinstance(r.json(), list): 252 | return {"error": True, "message": "sdb_" + r.json()["error"].lower()} 253 | # Time formatting thanks to ChatGPT 254 | return { 255 | "error": False, 256 | "posts": [ 257 | { 258 | "id": post["id"], 259 | "author": post["username"], 260 | "time": fmt_time(post["time"]["first_checked"]), 261 | "html_content": post["content"]["html"], 262 | "bb_content": post["content"]["bb"], 263 | "is_deleted": post["deleted"], 264 | "index": i, 265 | } 266 | for i, post in enumerate(r.json()) 267 | ], 268 | } 269 | except requests.exceptions.JSONDecodeError: 270 | return {"error": True, "message": "lib_scratchdbdown"} 271 | 272 | 273 | def get_pfp_url(username, size=90) -> str: 274 | """ 275 | Gets the profile picture URL for a user. 276 | 277 | - `username` (str): The username of the user. 278 | - `size` (int): The size of the requested image. 279 | 280 | Returns: 281 | - str: Profile picture URL.""" 282 | r = requests.get(f"https://api.scratch.mit.edu/users/{username}", timeout=10) 283 | 284 | return r.json()["profile"]["images"][str(size) + "x" + str(size)] 285 | 286 | 287 | def get_redirect_url() -> str: 288 | """ 289 | Gets the redirect URL for Scratch Auth. 290 | 291 | Returns: 292 | - str: Redirect URL. 293 | """ 294 | assert not ( 295 | not env["SERVER_MODE"] or not env["SERVER_MODE"] 296 | ), "Snazzle must be run in server mode for Scratch Auth to work. See https://tinyurl.com/snazzle-server" 297 | redir_loc = base64.b64encode( 298 | f"http://{env['SERVER_HOST']}/handle-scratch-auth".encode() 299 | ).decode() 300 | return f"https://auth.itinerary.eu.org/auth?name=snazzle&redirect={redir_loc}" 301 | 302 | 303 | def login(code: str) -> str: 304 | """ 305 | Logs in a user with the provided code. 306 | 307 | - `code` (str): The authentication code. 308 | 309 | Returns: 310 | - str: Session ID. 311 | """ 312 | data = requests.get( 313 | f"https://auth.itinerary.eu.org/api/auth/verifyToken?privateCode={code}", 314 | timeout=10, 315 | ).json() 316 | print(data) 317 | 318 | if ( 319 | data["valid"] 320 | and data["redirect"] == f"http://{env['SERVER_HOST']}/handle-scratch-auth" 321 | ): 322 | session_id = str(uuid4()) 323 | conn = sqlite3.connect(env["DB_LOCATION"]) 324 | cursor = conn.cursor() 325 | cursor.execute( 326 | f"CREATE TABLE IF NOT EXISTS {env['DB_TABLE']}( username, token )" 327 | ) 328 | cursor.execute( 329 | f"INSERT OR IGNORE INTO {env['DB_TABLE']} VALUES (?, ?)", 330 | (data["username"], session_id), 331 | ) 332 | conn.commit() 333 | conn.close() 334 | return session_id 335 | 336 | 337 | def token_matches_user(token: str) -> list: 338 | """ 339 | Checks if the token matches the user. 340 | 341 | - `token` (str): The token to check. 342 | 343 | Returns: 344 | - list: List of matching users. 345 | """ 346 | conn = sqlite3.connect(env["DB_LOCATION"]) 347 | cursor = conn.cursor() 348 | rows = cursor.execute("SELECT * from users WHERE token=?", (token,)) 349 | return rows.fetchall() 350 | 351 | 352 | def search_for_projects(q) -> dict: 353 | """ 354 | Searches for projects. 355 | 356 | - `q` (str): The query string. 357 | 358 | Returns: 359 | - dict: Search results. 360 | """ 361 | r = requests.get( 362 | f"https://api.scratch.mit.edu/search/projects?q={q}&mode=popular&language=en", 363 | timeout=10 364 | ) 365 | return r.json() 366 | 367 | def get_studio_data(id) -> dict: 368 | """ 369 | Gets data about a studio. 370 | 371 | - `id` (str): The ID of the studio. 372 | 373 | Returns: 374 | - dict: Studio data. 375 | """ 376 | r = requests.get(f'https://api.scratch.mit.edu/studios/{id}') 377 | return {"error": True, "message": "api_notfound"} if 'code' in r.json().keys() else r.json() 378 | 379 | def get_studio_comments(id): 380 | pass 381 | 382 | def get_trending_projects(limit: int = 20, page: int = 1): 383 | """ 384 | Gets a list of trending projects for the explore page 385 | 386 | - `page` (int): The page of results 387 | 388 | Returns: 389 | - list: Projects 390 | """ 391 | offset = limit * (page - 1) 392 | r = requests.get( 393 | f"https://api.scratch.mit.edu/search/projects?q=*&mode=trending&language=en&limit={limit}&offset={offset}", 394 | timeout=10 395 | ) 396 | return r.json() 397 | 398 | # Below this line is all stuff used for the REPL debugging mode 399 | # Generally, don't touch this, unless there's a severe flaw or something 400 | 401 | 402 | def parse_token(token: str, i: int): 403 | if isinstance(token, str) and i > 0 and "." in token: 404 | return token.replace(".", " ") 405 | return token 406 | 407 | 408 | def parse_cmd(cmd: str): 409 | if not isinstance(cmd, str): 410 | return None 411 | return_cmd = [parse_token(token, i) for i, token in enumerate(cmd.split(" "))] 412 | 413 | return return_cmd[0] + "(" + ", ".join(return_cmd[1:]) + ")" 414 | 415 | 416 | if __name__ == "__main__": 417 | cmd = None 418 | while True: 419 | cmd = input("input> ") 420 | parsed_cmd = parse_cmd(cmd) 421 | print("running>", parsed_cmd) 422 | print("output>", eval(parsed_cmd)) 423 | -------------------------------------------------------------------------------- /install_reqs.py: -------------------------------------------------------------------------------- 1 | # Script to install requirements. Use this if "pip install -r requirements.txt" doesn't work for some reason. 2 | import subprocess 3 | import os 4 | from os.path import exists, join 5 | from sys import exit 6 | 7 | were_deps_installed = True 8 | 9 | if not exists(join("logs", "install_reqs.log")): 10 | if not exists("logs"): os.mkdir("logs") 11 | open(join("logs", "install_reqs.log"), "x") 12 | were_deps_installed = False 13 | 14 | if were_deps_installed: 15 | print("[Snazzle] [AutoDep] Dependencies already installed. If there were errors in the first run, running this again won't fix anything.") 16 | else: 17 | try: 18 | subprocess.run(["pip", "install", "-r", "requirements.txt"]).check_returncode() 19 | except(subprocess.CalledProcessError): 20 | print("[Snazzle] [AutoDep] Option 1 failed, switching to backup...") 21 | with open("requirements.txt", "rt") as f: 22 | file = f.read() 23 | for requirement in file.splitlines(): 24 | try: 25 | proc = subprocess.run(["pip", "install", requirement], 26 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 27 | proc.check_returncode() 28 | if b"Requirement already satisfied" in proc.stdout: 29 | print(f"[Snazzle] [AutoDep] {requirement} already installed...") 30 | else: 31 | print(f"[Snazzle] [AutoDep] Installed {requirement}") 32 | except subprocess.CalledProcessError as err: 33 | print(f"[Snazzle] [AutoDep] An error occurred trying to install {requirement}. See logs/install_reqs.log for details.") 34 | with open("logs/install_reqs.log", "xt+") as log: 35 | log.writelines(err.stderr) 36 | exit(1) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.6.2 2 | certifi==2024.7.4 3 | charset-normalizer==3.2.0 4 | click==8.1.6 5 | Flask==2.3.2 6 | idna==3.7 7 | importlib-metadata==6.8.0 8 | itsdangerous==2.1.2 9 | Jinja2==3.1.4 10 | MarkupSafe==2.1.3 11 | numpy==1.26.4 12 | pandas==2.0.3 13 | python-dateutil==2.8.2 14 | python-dotenv==1.0.0 15 | pytz==2023.3 16 | requests==2.32.2 17 | six==1.16.0 18 | tzdata==2023.3 19 | urllib3==2.2.2 20 | Werkzeug==3.0.6 21 | zipp==3.19.1 22 | -------------------------------------------------------------------------------- /static/Jost.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnarpleDev/Snazzle/5eed72796303a52aafa5abdafd81280a1cc67c41/static/Jost.ttf -------------------------------------------------------------------------------- /static/PlayFairDisplay.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnarpleDev/Snazzle/5eed72796303a52aafa5abdafd81280a1cc67c41/static/PlayFairDisplay.ttf -------------------------------------------------------------------------------- /static/VT323.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnarpleDev/Snazzle/5eed72796303a52aafa5abdafd81280a1cc67c41/static/VT323.ttf -------------------------------------------------------------------------------- /static/linux-users-installing-browser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnarpleDev/Snazzle/5eed72796303a52aafa5abdafd81280a1cc67c41/static/linux-users-installing-browser.gif -------------------------------------------------------------------------------- /static/master-styles.css: -------------------------------------------------------------------------------- 1 | /* Default styles for the "desktop-only" class */ 2 | .desktop-only { 3 | display: none; 4 | } 5 | 6 | .mobile-only { 7 | display: block; 8 | } 9 | 10 | /* Media query for screens with a minimum width of 1024px */ 11 | @media (min-width: 1024px) { 12 | .desktop-only { 13 | display: block; 14 | } 15 | 16 | span.desktop-only { 17 | display: inline; 18 | } 19 | 20 | .mobile-only { 21 | display: none; 22 | } 23 | } 24 | 25 | @media (max-width: 1024px) { 26 | [class^="width-"] { 27 | width: 100% !important; 28 | } 29 | 30 | .flex-cont * { 31 | display: block; 32 | } 33 | } 34 | 35 | div#studio-description { 36 | white-space: pre-wrap; 37 | } 38 | 39 | ::-webkit-scrollbar { 40 | width: 10px; 41 | height: 10px; 42 | } 43 | 44 | ::-webkit-scrollbar-track { 45 | box-shadow: inset 0 0 2px black; 46 | border-radius: 10px; 47 | } 48 | 49 | input, 50 | textarea { 51 | transition: all 0.3s ease; 52 | padding: 2.5px; 53 | border-radius: 10px; 54 | width: calc(100% - 10px); 55 | } 56 | 57 | textarea { 58 | resize: none; 59 | } 60 | 61 | button:hover { 62 | backdrop-filter: blur(5px); 63 | cursor: pointer; 64 | } 65 | 66 | select:hover { 67 | cursor: pointer; 68 | } 69 | 70 | .scrollpane { 71 | 72 | max-width: 100%; 73 | border-radius: 10px; 74 | display: flex; 75 | } 76 | 77 | .scrollpane.sp-horiz { 78 | overflow-x: scroll; 79 | overflow-y: hidden; 80 | flex-direction: row; 81 | } 82 | 83 | .scrollpane.sp-vert { 84 | overflow-x: hidden; 85 | overflow-y: scroll; 86 | flex-direction: column; 87 | } 88 | 89 | .scrollpane.sp-horiz_reverse { 90 | overflow-x: scroll; 91 | overflow-y: hidden; 92 | flex-direction: row-reverse; 93 | } 94 | 95 | .scrollpane.sp-vert_reverse { 96 | overflow-x: hidden; 97 | overflow-y: scroll; 98 | flex-direction: column-reverse; 99 | } 100 | 101 | /* Projects scrollpane */ 102 | .scrollpane.sp-projects { 103 | min-height: 50px; 104 | max-height: 250px; 105 | } 106 | 107 | .scrollpane.sp-projects .sp-item, 108 | .project-grid .pg-project { 109 | margin: 5px; 110 | min-height: 70px; 111 | max-height: 180px; 112 | min-width: 140px; 113 | border-radius: 10px; 114 | padding: .5rem; 115 | transition: 0.5s all; 116 | } 117 | 118 | .scrollpane.sp-projects .sp-item:hover, 119 | 120 | .scrollpane .sp-item:hover, 121 | .project-grid .pg-project:hover { 122 | transform: none; 123 | cursor: pointer; 124 | } 125 | 126 | /* Studios scrollpane */ 127 | .scrollpane.sp-studios { 128 | min-height: 60px; 129 | max-height: 210px; 130 | } 131 | 132 | .scrollpane.sp-studios .sp-item { 133 | margin: 5px; 134 | min-height: 80px; 135 | max-height: 200px; 136 | min-width: 180px; 137 | border-radius: 10px; 138 | padding: .5rem; 139 | transition: 0.5s all; 140 | 141 | } 142 | 143 | .scrollpane .sp-item { 144 | flex-flow: column nowrap; 145 | display: flex; 146 | } 147 | 148 | /* Filters scrollpane */ 149 | 150 | .scrollpane.sp-filters .sp-item { 151 | min-width: 100px; 152 | max-width: 200px; 153 | } 154 | 155 | .scrollpane.sp-filters .sp-spacer { 156 | min-width: 10px; 157 | } 158 | 159 | /* Forums scrollpane */ 160 | .scrollpane.sp-forum { 161 | max-height: 250px; 162 | } 163 | 164 | .scrollpane.sp-forum .sp-item { 165 | transition: 0.5s all; 166 | margin: 0.5em; 167 | } 168 | 169 | .flex-cont { 170 | display: flex; 171 | } 172 | 173 | .flex-cont.fc-vert { 174 | flex-direction: column; 175 | } 176 | 177 | .width-70 { 178 | width: 70%; 179 | } 180 | 181 | .width-60 { 182 | width: 60%; 183 | } 184 | 185 | .width-50 { 186 | width: 50%; 187 | } 188 | 189 | .width-40 { 190 | width: 40%; 191 | } 192 | 193 | .width-30 { 194 | width: 30%; 195 | } 196 | 197 | .width-third { 198 | width: calc(100% / 3) !important; 199 | margin: 0px 5px; 200 | } 201 | 202 | .width-full { 203 | width: 100%; 204 | } 205 | 206 | section { 207 | margin: 0.05em; 208 | padding: 5px; 209 | border-radius: 10px; 210 | margin-top: 1%; 211 | } 212 | 213 | #banner { 214 | margin: 0.05em; 215 | border-radius: 10px; 216 | /* height: calc(75vh + 100px) !important; */ 217 | 218 | padding: 2.5rem; 219 | /* z-index: 999; */ 220 | } 221 | 222 | 223 | .update { 224 | animation: jiggle 2s forwards; 225 | } 226 | 227 | @keyframes jiggle { 228 | 0% { 229 | transform: rotateZ(10deg) 230 | } 231 | 232 | 5% { 233 | transform: rotateZ(-9deg) 234 | } 235 | 236 | 10% { 237 | transform: rotateZ(8deg) 238 | } 239 | 240 | 15% { 241 | transform: rotateZ(-7deg) 242 | } 243 | 244 | 20% { 245 | transform: rotateZ(6deg) 246 | } 247 | 248 | 25% { 249 | transform: rotateZ(-5deg) 250 | } 251 | 252 | 30% { 253 | transform: rotateZ(4deg) 254 | } 255 | 256 | 35% { 257 | transform: rotateZ(-3deg) 258 | } 259 | 260 | 40% { 261 | transform: rotateZ(2deg) 262 | } 263 | 264 | 45% { 265 | transform: rotateZ(-1deg) 266 | } 267 | 268 | 50% { 269 | transform: rotateZ(0deg) 270 | } 271 | } 272 | 273 | section.project-grid { 274 | display: flex; 275 | flex-wrap: wrap; 276 | padding: none; 277 | margin: none; 278 | } 279 | 280 | .project-grid .pg-project { 281 | margin: 5px; 282 | min-height: 70px; 283 | max-height: 180px; 284 | max-width: 130px; 285 | min-width: 130px; 286 | border-radius: 10px; 287 | padding: 0.5rem; 288 | transition: 0.5s all; 289 | } 290 | 291 | img { 292 | border-radius: 10px; 293 | } 294 | 295 | span.bb-small { 296 | font-size: 0.8rem; 297 | } 298 | 299 | span.bb-strikethrough { 300 | text-decoration: line-through; 301 | } 302 | 303 | .width-80 { 304 | width: 80%; 305 | } 306 | 307 | .width-20 { 308 | width: 20%; 309 | } 310 | 311 | .width-25 { 312 | width: 25%; 313 | } 314 | 315 | .forum-post * { 316 | max-width: 100% !important; 317 | 318 | } 319 | 320 | .forum-post blockquote img { 321 | max-width: 50%; 322 | max-height: 50%; 323 | } 324 | 325 | .sp-forum div.sp-item { 326 | transition: all 0.3s ease; 327 | border-radius: 10px; 328 | border: 1px solid #ffffff; 329 | background-color: #00000067; 330 | color: white; 331 | padding: 0.5em !important; 332 | } 333 | 334 | .sp-item a>i.pin-btn, 335 | .sp-item a>span.pin-btn { 336 | padding: 0.5em !important; 337 | display: none; 338 | } 339 | 340 | .sp-item:hover a>i.pin-btn, 341 | .sp-item:hover a>span.pin-btn { 342 | display: inline; 343 | } 344 | 345 | blockquote, 346 | .forum-aside, 347 | pre.blocks { 348 | background-color: rgba(0, 0, 0, 0.56); 349 | box-shadow: 0px 0px 10px rgb(0, 0, 0) inset; 350 | border-radius: 10px; 351 | padding: 5px; 352 | margin: 2.5px; 353 | overflow-x: scroll; 354 | } 355 | 356 | .bb-big { 357 | font-size: 1.8rem; 358 | } 359 | 360 | section, 361 | #banner { 362 | margin-bottom: 5px; 363 | } 364 | 365 | .pfp { 366 | width: 100px; 367 | } 368 | 369 | div.code { 370 | overflow-x: scroll; 371 | } 372 | 373 | #replit-logo { 374 | width: 1em; 375 | } 376 | 377 | .topic-author { 378 | color: rgba(255, 255, 255, 0.5); 379 | font-weight: bold; 380 | } 381 | 382 | nav { 383 | background: rgba(50, 48, 47, 0.5); 384 | backdrop-filter: blur(8px); 385 | border-bottom: #504945; 386 | padding: 5px 0; 387 | position: sticky; 388 | top: 0px; 389 | z-index: 1000; 390 | backdrop-filter: blur(10px); 391 | border-bottom-left-radius: 10px; 392 | border-bottom-right-radius: 10px; 393 | margin: 0px 100px; 394 | } 395 | 396 | .logo a { 397 | font-size: 24px; 398 | font-weight: bold; 399 | margin-left: 10px; 400 | transition: 0.5s all cubic-bezier(0.455, 0.03, 0.515, 0.955); 401 | text-decoration: none; 402 | } 403 | 404 | .logo a:hover { 405 | cursor: pointer; 406 | } 407 | 408 | .navbar-container { 409 | max-width: 1200px; 410 | margin: 0 auto; 411 | display: flex; 412 | align-items: center; 413 | justify-content: space-between; 414 | max-height: 2em; 415 | } 416 | 417 | 418 | .nav-links { 419 | list-style: none; 420 | display: flex; 421 | margin-right: 10px; 422 | } 423 | 424 | .nav-links li { 425 | margin-left: 10px; 426 | } 427 | 428 | .nav-links li a { 429 | text-decoration: none; 430 | font-size: 18px; 431 | font-weight: bold; 432 | } 433 | 434 | button, 435 | select { 436 | transition: all 0.3s ease; 437 | padding: 5px; 438 | border-radius: 2.5px; 439 | } 440 | 441 | input { 442 | transition: all 0.3s ease; 443 | padding: 2.5px; 444 | border-radius: 10px; 445 | } 446 | 447 | button:hover { 448 | backdrop-filter: blur(5px); 449 | border-radius: 10px; 450 | cursor: pointer; 451 | } 452 | 453 | select:hover { 454 | cursor: pointer; 455 | } 456 | 457 | .no-bullets { 458 | list-style: none; 459 | } 460 | 461 | textarea { 462 | height: 200px; 463 | } 464 | 465 | .sp-vert.sp-forum.sp-any-height { 466 | height: 100% !important; 467 | } 468 | 469 | #menu { 470 | display: none; 471 | margin: 10px; 472 | align-items: center; 473 | justify-content: center; 474 | } 475 | 476 | .omni { 477 | margin: 0px 100px; 478 | } 479 | 480 | body { 481 | margin: 0px; 482 | } 483 | 484 | #load { 485 | display: flex; 486 | align-items: center; 487 | justify-content: center; 488 | width: 130px; 489 | min-height: 70px; 490 | max-height: 180px; 491 | margin: 13px; 492 | } 493 | 494 | #loadMoreButton { 495 | width: 150px; 496 | height: 100px; 497 | font-size: larger; 498 | font-weight: bold; 499 | } 500 | 501 | .title, 502 | .author { 503 | overflow: hidden; 504 | display: block; 505 | text-overflow: ellipsis; 506 | white-space: nowrap; 507 | } -------------------------------------------------------------------------------- /static/scanlines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnarpleDev/Snazzle/5eed72796303a52aafa5abdafd81280a1cc67c41/static/scanlines.png -------------------------------------------------------------------------------- /static/styles-aurora.css: -------------------------------------------------------------------------------- 1 | @import url("master-styles.css"); 2 | 3 | @font-face { 4 | font-family: "Jost"; 5 | src: url("Jost.ttf") format("truetype"); 6 | font-weight: 400; 7 | } 8 | 9 | body { 10 | background-color: #0d0e18; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | div, 21 | button, 22 | input, 23 | article, 24 | textarea { 25 | font-family: sans-serif; 26 | } 27 | 28 | input::placeholder { 29 | color: #B2BFFF; 30 | } 31 | 32 | nav { 33 | background: rgba(0, 0, 0, 0.544); 34 | } 35 | 36 | .logo a { 37 | color: #fff; 38 | } 39 | 40 | .logo a:hover { 41 | text-shadow: 0px 0px 3px #8699FF; 42 | } 43 | 44 | .nav-links li a { 45 | color: #fff; 46 | } 47 | 48 | button, 49 | select { 50 | border: 1px solid #6977c7; 51 | background-color: #6977c7; 52 | color: white; 53 | } 54 | 55 | input, textarea { 56 | border: 1px solid #8699FF; 57 | background-color: transparent; 58 | color: white; 59 | } 60 | 61 | input:focus, textarea:focus { 62 | outline: 1px solid #fff; 63 | } 64 | 65 | button:hover { 66 | background-color: transparent; 67 | color: white; 68 | } 69 | 70 | .scrollpane { 71 | 72 | background-color: #0D0E18; 73 | box-shadow: 0px 0px 10px rgb(0, 0, 0) inset; 74 | } 75 | 76 | .scrollpane.sp-projects .sp-item, 77 | .project-grid .pg-project { 78 | background-color: #6977c7; 79 | color: rgb(255, 255, 255); 80 | font-weight: bold; 81 | } 82 | 83 | .scrollpane.sp-projects .sp-item:hover, 84 | .project-grid .pg-project:hover { 85 | background-color: #8699FF; 86 | color: white; 87 | font-weight: bold; 88 | box-shadow: 5px 5px 3px rgba(0, 0, 0, 0.643); 89 | } 90 | 91 | .scrollpane.sp-studios .sp-item { 92 | background-color: #6977c7; 93 | color: white; 94 | font-weight: bold; 95 | } 96 | 97 | .scrollpane.sp-studios .sp-item:hover { 98 | background-color: #8699FF; 99 | color: white; 100 | } 101 | 102 | span.stats { 103 | color: rgba(255, 255, 255, 0.4); 104 | } 105 | 106 | .scrollpane .sp-header { 107 | background-color: rgba(0, 0, 0, 0.544); 108 | } 109 | 110 | section { 111 | background: #262845; 112 | box-shadow: 0px 0px 10px #262845; 113 | color: white; 114 | } 115 | 116 | #banner { 117 | background: linear-gradient(90deg, #6977c7 0%, #69c7be 100%); 118 | color: white; 119 | } 120 | 121 | #banner button.purple, 122 | button.secondary { 123 | background-color: transparent; 124 | color: white; 125 | } 126 | 127 | #banner button.purple:hover, 128 | button.secondary:hover { 129 | background-color: #6977c7; 130 | color: white; 131 | } 132 | 133 | a, 134 | a:visited { 135 | color: #8699FF; 136 | } 137 | 138 | button a, 139 | button a:visited { 140 | color: white; 141 | } 142 | 143 | #studio-banner section { 144 | background-color: rgba(0, 0, 0, 0.25); 145 | } -------------------------------------------------------------------------------- /static/styles-choco.css: -------------------------------------------------------------------------------- 1 | @import url("master-styles.css"); 2 | 3 | @font-face { 4 | font-family: "Jost"; 5 | src: url("Jost.ttf") format("truetype"); 6 | font-weight: 400; 7 | } 8 | 9 | body { 10 | background-color: #282320; 11 | } 12 | 13 | ::-webkit-scrollbar-thumb { 14 | background: #b75d24; 15 | border-radius: 10px; 16 | } 17 | 18 | ::-webkit-scrollbar-thumb:hover { 19 | background: #91491d; 20 | } 21 | 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5, 27 | h6, 28 | p, 29 | div, 30 | button, 31 | input, 32 | article, 33 | textarea { 34 | font-family: 'Jost', sans-serif; 35 | } 36 | 37 | input::placeholder { 38 | color: #ca6120; 39 | } 40 | 41 | nav { 42 | background: rgba(0, 0, 0, 0.544); 43 | } 44 | 45 | .logo a { 46 | color: #fff; 47 | } 48 | 49 | .logo a:hover { 50 | text-shadow: 0px 0px 3px orange; 51 | } 52 | 53 | .nav-links li a { 54 | color: #fff; 55 | } 56 | 57 | button, 58 | select { 59 | border: 1px solid #ca6120; 60 | background-color: #ca6120; 61 | color: white; 62 | } 63 | 64 | input, 65 | textarea { 66 | border: 1px solid #ca6120; 67 | background-color: transparent; 68 | color: white; 69 | } 70 | 71 | input:focus, 72 | textarea:focus { 73 | outline: 1px solid #fff; 74 | } 75 | 76 | button:hover { 77 | background-color: transparent; 78 | color: white; 79 | } 80 | 81 | .scrollpane { 82 | 83 | background-color: #803d14; 84 | box-shadow: 0px 0px 10px rgb(0, 0, 0) inset; 85 | } 86 | 87 | .scrollpane.sp-projects .sp-item, 88 | .project-grid .pg-project { 89 | background-color: #ca6120; 90 | color: rgb(255, 255, 255); 91 | 92 | } 93 | 94 | .scrollpane.sp-projects .sp-item:hover, 95 | .project-grid .pg-project:hover { 96 | background-color: #ff9654; 97 | color: black; 98 | box-shadow: 5px 5px 3px rgba(0, 0, 0, 0.643); 99 | } 100 | 101 | .scrollpane.sp-studios .sp-item { 102 | background-color: #ca6120; 103 | color: white; 104 | 105 | } 106 | 107 | .scrollpane.sp-studios .sp-item:hover { 108 | background-color: #ff9654; 109 | color: black; 110 | } 111 | 112 | span.stats { 113 | color: lightgrey; 114 | } 115 | 116 | .scrollpane .sp-header { 117 | background-color: rgba(0, 0, 0, 0.544); 118 | } 119 | 120 | section { 121 | background: #724326; 122 | box-shadow: 0px 0px 10px #724326; 123 | color: white; 124 | } 125 | 126 | #banner { 127 | background: linear-gradient(90deg, rgb(193, 114, 64) 0%, rgb(103, 61, 35) 100%); 128 | box-shadow: 0px 0px 10px linear-gradient(180deg, rgba(139, 82, 46, 1) 0%, rgba(114, 67, 38, 1) 100%); 129 | color: white; 130 | } 131 | 132 | #banner button.purple, 133 | button.secondary { 134 | background-color: transparent; 135 | color: white; 136 | } 137 | 138 | #banner button.purple:hover, 139 | button.secondary:hover { 140 | background-color: #ca6120; 141 | color: white; 142 | } 143 | 144 | a, 145 | a:visited { 146 | color: #ffbb93; 147 | } 148 | 149 | button a, 150 | button a:visited { 151 | color: white; 152 | } 153 | 154 | #studio-banner section { 155 | background-color: rgba(0, 0, 0, 0.25); 156 | } -------------------------------------------------------------------------------- /static/styles-gruvbox.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | A Gruvbox theme for Snazzle, updated by @redstone-scratch 4 | Original author: @ajskateboarder 5 | Modified by: @redstone-scratch, @DarthNess 6 | 7 | */ 8 | 9 | @import url("master-styles.css"); 10 | @font-face { 11 | font-family: "Jost"; 12 | src: url("Jost.ttf") format("truetype"); 13 | font-weight: 400; 14 | } 15 | body { 16 | 17 | background-color: #282828; 18 | } 19 | 20 | ::-webkit-scrollbar-thumb { 21 | background: #4a423f; 22 | border-radius: 10px; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb:hover { 26 | background: #3f3836; 27 | } 28 | 29 | .omni { 30 | margin-left: 2vw; 31 | margin-right: 2vw; 32 | } 33 | 34 | 35 | h1, 36 | h2, 37 | h3, 38 | h4, 39 | h5, 40 | h6, 41 | p, 42 | div, 43 | button, 44 | input, 45 | article { 46 | font-family: 'Jost', sans-serif; 47 | } 48 | 49 | h1, 50 | h2, 51 | h3 { 52 | color: #ede3be; 53 | } 54 | 55 | input::placeholder { 56 | color: #6f6259; 57 | } 58 | 59 | body { 60 | margin: 0; 61 | font-family: 'Jost', Arial, sans-serif; 62 | } 63 | 64 | nav { 65 | background: rgba(50, 48, 47, 0.5); 66 | backdrop-filter: blur(8px); 67 | border-bottom: #504945; 68 | padding: 10px 0; 69 | position: sticky; 70 | top: 0px; 71 | z-index: 1000; 72 | backdrop-filter: blur(10px); 73 | border-bottom-left-radius: 10px; 74 | border-bottom-right-radius: 10px; 75 | } 76 | 77 | .logo a { 78 | font-size: 24px; 79 | font-weight: bold; 80 | margin-left: 20px; 81 | color: #fe8019 !important; 82 | transition: 0.5s all cubic-bezier(0.455, 0.03, 0.515, 0.955); 83 | text-decoration: none; 84 | } 85 | 86 | .logo a:hover { 87 | text-shadow: 0px 0px 3px #ff963f; 88 | cursor: pointer; 89 | } 90 | 91 | .navbar-container { 92 | max-width: 1200px; 93 | margin: 0 auto; 94 | display: flex; 95 | align-items: center; 96 | justify-content: space-between; 97 | max-height: 2em; 98 | } 99 | 100 | 101 | .nav-links { 102 | list-style: none; 103 | display: flex; 104 | margin-right: 20px; 105 | } 106 | 107 | .nav-links li { 108 | margin-left: 20px; 109 | } 110 | 111 | .nav-links li a { 112 | color: #fbf1c7; 113 | text-decoration: none; 114 | font-size: 18px; 115 | font-weight: bold; 116 | } 117 | 118 | button, 119 | select { 120 | transition: all 0.3s ease; 121 | padding: 10px; 122 | border-radius: 10px; 123 | border: 1px solid #fe8019; 124 | background-color: #3c3836; 125 | color: #f8efcc; 126 | } 127 | 128 | input { 129 | transition: all 0.3s ease; 130 | padding: 5px; 131 | border-radius: 10px; 132 | border: 1px solid #ca6120; 133 | background-color: transparent; 134 | color: white; 135 | } 136 | 137 | input:focus { 138 | outline: 1px solid #fff; 139 | } 140 | 141 | button:hover { 142 | background-color: #e38242; 143 | border: 1px solid #3c3836; 144 | backdrop-filter: blur(5px); 145 | border-radius: 10px; 146 | cursor: pointer; 147 | } 148 | 149 | select:hover { 150 | cursor: pointer; 151 | } 152 | 153 | .scrollpane { 154 | background-color: #32302f; 155 | max-width: 100%; 156 | border-radius: 10px; 157 | display: flex; 158 | box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2) inset; 159 | } 160 | 161 | .scrollpane.sp-horiz { 162 | overflow-x: scroll; 163 | overflow-y: hidden; 164 | flex-direction: row; 165 | } 166 | 167 | .scrollpane.sp-vert { 168 | overflow-x: hidden; 169 | overflow-y: scroll; 170 | flex-direction: column; 171 | } 172 | 173 | .scrollpane.sp-horiz_reverse { 174 | overflow-x: scroll; 175 | overflow-y: hidden; 176 | flex-direction: row-reverse; 177 | } 178 | 179 | .scrollpane.sp-vert_reverse { 180 | overflow-x: hidden; 181 | overflow-y: scroll; 182 | flex-direction: column-reverse; 183 | } 184 | 185 | /* Projects scrollpane */ 186 | .scrollpane.sp-projects { 187 | min-height: 50px; 188 | max-height: 250px; 189 | } 190 | 191 | .scrollpane.sp-projects .sp-item, 192 | .project-grid .pg-project { 193 | background-color: #433e38; 194 | color: #fbf1c7; 195 | margin: 10px; 196 | min-height: 70px; 197 | max-height: 180px; 198 | min-width: 140px; 199 | border-radius: 10px; 200 | padding: 1rem; 201 | position: relative; 202 | bottom: 0px; 203 | transition: 0.5s all; 204 | } 205 | 206 | .scrollpane.sp-projects .sp-item:hover, 207 | .project-grid .pg-project:hover { 208 | background-color: #eb8c3e; 209 | color: black; 210 | } 211 | 212 | .sp-projects .sp-item:hover, 213 | .project-grid .pg-project:hover { 214 | box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2); 215 | rotate: 5deg; 216 | bottom: 5px; 217 | cursor: pointer; 218 | } 219 | 220 | /* Studios scrollpane */ 221 | .scrollpane.sp-studios { 222 | min-height: 60px; 223 | max-height: 210px; 224 | } 225 | 226 | .scrollpane.sp-studios .sp-item { 227 | background-color: #433e38; 228 | color: #f8efcc; 229 | margin: 10px; 230 | min-height: 80px; 231 | max-height: 200px; 232 | min-width: 180px; 233 | border-radius: 10px; 234 | padding: 1rem; 235 | transition: 0.5s all; 236 | 237 | } 238 | 239 | .scrollpane.sp-studios .sp-item:hover { 240 | background-color: #ff9654; 241 | color: black; 242 | } 243 | 244 | .scrollpane .sp-item { 245 | flex-flow: column nowrap; 246 | display: flex; 247 | } 248 | 249 | /* Filters scrollpane */ 250 | 251 | .scrollpane.sp-filters .sp-item { 252 | min-width: 100px; 253 | max-width: 200px; 254 | } 255 | 256 | .scrollpane.sp-filters .sp-spacer { 257 | min-width: 10px; 258 | } 259 | 260 | /* Forums scrollpane */ 261 | .scrollpane.sp-forum { 262 | max-height: 300px; 263 | } 264 | 265 | .scrollpane.sp-forum .sp-item { 266 | transition: 0.5s all; 267 | margin: 0.25em; 268 | padding: 5px; 269 | border: 3px solid #eb8c3e; 270 | border-radius: 10px; 271 | } 272 | 273 | .scrollpane.sp-forum .sp-item:hover { 274 | box-shadow: none; 275 | rotate: none !important; 276 | border: 3px solid #fbf1c7; 277 | } 278 | 279 | .scrollpane.sp-forum .sp-item a { 280 | color: #ca6120; 281 | transition: 0.5s color; 282 | } 283 | 284 | .scrollpane.sp-forum .sp-item:hover a { 285 | color: #ff9654; 286 | } 287 | 288 | span.stats { 289 | color: #f8efcc; 290 | } 291 | 292 | .flex-cont { 293 | display: flex; 294 | } 295 | 296 | .flex-cont.fc-vert { 297 | flex-direction: column; 298 | } 299 | 300 | .width-70 { 301 | width: 70%; 302 | } 303 | 304 | .width-60 { 305 | width: 60%; 306 | } 307 | 308 | .width-50 { 309 | width: 50%; 310 | } 311 | 312 | .width-40 { 313 | width: 40%; 314 | } 315 | 316 | .width-30 { 317 | width: 30%; 318 | } 319 | 320 | .width-third { 321 | width: calc(100% / 3) 322 | } 323 | 324 | .scrollpane .sp-header { 325 | background-color: rgba(0, 0, 0, 0.544); 326 | } 327 | 328 | section { 329 | margin: 0.1em; 330 | background: #3c3836; 331 | padding: 10px; 332 | border-radius: 20px; 333 | color: #fbf1c7; 334 | border: 6px solid #3c3836; 335 | } 336 | 337 | section:first-child:not(.width-third, .gb_no-T){ 338 | background: transparent; 339 | } 340 | 341 | #banner { 342 | margin: 0.1em; 343 | background: linear-gradient(90deg, #3c3836, 0%, #504945, 100%); 344 | border-radius: 20px; 345 | box-shadow: 0px 0px 10px linear-gradient(180deg, rgba(139, 82, 46, 1) 0%, rgba(114, 67, 38, 1) 100%); 346 | color: #ebdbb2; 347 | height: 75vh; 348 | padding: 2.5rem; 349 | padding-bottom: 0px; 350 | border: 6px solid #3c3836; 351 | /* z-index: 999; */ 352 | } 353 | 354 | 355 | .update { 356 | animation: jiggle 2s forwards; 357 | } 358 | 359 | @keyframes jiggle { 360 | 0% { 361 | transform: rotateZ(10deg) 362 | } 363 | 364 | 5% { 365 | transform: rotateZ(-9deg) 366 | } 367 | 368 | 10% { 369 | transform: rotateZ(8deg) 370 | } 371 | 372 | 15% { 373 | transform: rotateZ(-7deg) 374 | } 375 | 376 | 20% { 377 | transform: rotateZ(6deg) 378 | } 379 | 380 | 25% { 381 | transform: rotateZ(-5deg) 382 | } 383 | 384 | 30% { 385 | transform: rotateZ(4deg) 386 | } 387 | 388 | 35% { 389 | transform: rotateZ(-3deg) 390 | } 391 | 392 | 40% { 393 | transform: rotateZ(2deg) 394 | } 395 | 396 | 45% { 397 | transform: rotateZ(-1deg) 398 | } 399 | 400 | 50% { 401 | transform: rotateZ(0deg) 402 | } 403 | } 404 | 405 | section.project-grid { 406 | display: flex; 407 | flex-wrap: wrap; 408 | padding: none; 409 | margin: none; 410 | } 411 | 412 | .project-grid .pg-project { 413 | margin: 10px; 414 | min-height: 70px; 415 | max-height: 180px; 416 | max-width: 130px; 417 | min-width: 130px; 418 | border-radius: 10px; 419 | padding: 1rem; 420 | transition: 0.5s all; 421 | } 422 | 423 | a, 424 | a:visited { 425 | color: #f8efcc; 426 | transition: 0.5s color; 427 | } 428 | 429 | button a, 430 | button a:visited { 431 | color: white; 432 | } 433 | 434 | a:hover { 435 | color: #ca6120; 436 | } 437 | 438 | img { 439 | border-radius: 10px; 440 | } 441 | 442 | .bb-quote-author { 443 | color: #ca6120; 444 | } 445 | 446 | #studio-banner section { 447 | background-color: rgba(40, 40, 40, 0.25); 448 | } -------------------------------------------------------------------------------- /static/styles-hackerman.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | "I'm in." 4 | Author: @redstone-scratch 5 | 6 | */ 7 | 8 | @import url('/static/master-styles.css'); 9 | @font-face { 10 | font-family: "VT323"; 11 | src: url("VT323.ttf") format("truetype"); 12 | font-weight: 400; 13 | } 14 | body { 15 | background-color: hsl(112, 11%, 14%); 16 | color: lime; 17 | } 18 | 19 | ::-webkit-scrollbar-thumb { 20 | background: #083d00; 21 | border-radius: 10px; 22 | } 23 | 24 | ::-webkit-scrollbar-thumb:hover { 25 | background: #062e00; 26 | } 27 | 28 | * { 29 | border-radius: 0px !important 30 | } 31 | 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6, 38 | p, 39 | div, 40 | button, 41 | input, 42 | select, 43 | article { 44 | font-family: 'VT323', monospace; 45 | } 46 | 47 | input::placeholder { 48 | color: hsl(112, 100%, 25%); 49 | } 50 | 51 | nav { 52 | background: rgba(0, 0, 0, 1); 53 | } 54 | 55 | .logo a { 56 | color: #fff; 57 | } 58 | 59 | .logo a:hover { 60 | text-shadow: 0px 0px 3px hsl(112, 100%, 29%); 61 | } 62 | 63 | .nav-links li a { 64 | color: #fff; 65 | } 66 | 67 | button, 68 | input, 69 | select { 70 | border: 1px solid hsl(112, 100%, 25%); 71 | background-color: hsl(112, 100%, 12%); 72 | color: white; 73 | } 74 | 75 | button:hover { 76 | background-color: black; 77 | color: white; 78 | } 79 | 80 | .scrollpane { 81 | 82 | background-color: hsl(112, 2%, 43%); 83 | box-shadow: 0px 0px 10px rgb(0, 0, 0) inset; 84 | } 85 | 86 | .scrollpane.sp-projects .sp-item { 87 | background-color: hsl(112, 100%, 21%); 88 | color: rgb(255, 255, 255); 89 | transition: unset !important; 90 | } 91 | 92 | .scrollpane.sp-projects .sp-item:hover, 93 | .project-grid .pg-project:hover { 94 | background-color: hsl(112, 100%, 79%); 95 | color: black; 96 | transition: unset; 97 | } 98 | 99 | .scrollpane.sp-studios .sp-item { 100 | background-color: hsl(112, 100%, 21%); 101 | color: white; 102 | transition: unset; 103 | } 104 | 105 | .scrollpane.sp-projects .sp-item:hover, 106 | .project-grid .pg-project:hover { 107 | background-color: hsl(112, 100%, 79%); 108 | color: black; 109 | transform: unset; 110 | transition: unset; 111 | } 112 | 113 | .scrollpane.sp-studios .sp-item:hover { 114 | background-color: hsl(112, 100%, 85%); 115 | color: black; 116 | } 117 | 118 | span.stats { 119 | color: lightgrey; 120 | } 121 | 122 | .scrollpane .sp-header { 123 | background-color: rgba(0, 0, 0, 0.544); 124 | } 125 | 126 | section { 127 | background: (66, 66, 66, 0.643); 128 | box-shadow: 0px 0px 10px hsl(112, 100%, 71%); 129 | color: white; 130 | } 131 | 132 | #banner { 133 | background: hsl(112, 100%, 33%); 134 | box-shadow: 0px 0px 10px hsl(112, 100%, 71%); 135 | color: white; 136 | } 137 | 138 | .project-grid .pg-project { 139 | background-color: hsl(112, 100%, 50%); 140 | color: rgb(255, 255, 255); 141 | } 142 | 143 | a { 144 | color: lime; 145 | text-decoration: none; 146 | } 147 | 148 | a:hover { 149 | text-decoration: underline; 150 | } 151 | 152 | blockquote, .forum-aside { 153 | border-radius: none !important; 154 | } 155 | 156 | #studio-banner section { 157 | background-color: rgba(6, 46, 0, 1); 158 | } -------------------------------------------------------------------------------- /static/styles-ice.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | A cold and icy dark theme for Snazzle. 4 | Author: @redstone-scratch 5 | 6 | */ 7 | @import url("master-styles.css"); 8 | @font-face { 9 | font-family: "Jost"; 10 | src: url("Jost.ttf") format("truetype"); 11 | font-weight: 400; 12 | } 13 | body { 14 | background-color: #202d38; 15 | } 16 | 17 | ::-webkit-scrollbar-thumb { 18 | background: #585d61; 19 | border-radius: 10px; 20 | } 21 | 22 | ::-webkit-scrollbar-thumb:hover { 23 | background: #43484c; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | p, 33 | div, 34 | button, 35 | input, 36 | article { 37 | font-family: 'Jost', sans-serif; 38 | } 39 | 40 | * { 41 | color: white; 42 | } 43 | 44 | input::placeholder { 45 | color: #46637b; 46 | } 47 | 48 | nav { 49 | background: rgba(0, 0, 0, 0.544); 50 | } 51 | 52 | .logo a { 53 | color: #fff; 54 | } 55 | 56 | .logo a:hover { 57 | text-shadow: 0px 0px 3px #91caf9; 58 | } 59 | 60 | .nav-links li a { 61 | color: #fff; 62 | } 63 | 64 | button, 65 | select { 66 | border: 1px solid #008cff; 67 | background-color: #008cff; 68 | color: white; 69 | } 70 | 71 | input { 72 | border: 1px solid #008cff; 73 | background-color: transparent; 74 | color: white; 75 | } 76 | 77 | input:focus { 78 | outline: 1px solid transparent; 79 | } 80 | 81 | button:hover { 82 | background-color: transparent; 83 | color: white; 84 | } 85 | 86 | .scrollpane { 87 | 88 | background-color: rgb(107, 109, 111); 89 | box-shadow: 0px 0px 10px rgb(0, 0, 0) inset; 90 | } 91 | 92 | .scrollpane.sp-projects .sp-item, 93 | .project-grid .pg-project { 94 | background-color: #008cff; 95 | color: rgb(255, 255, 255); 96 | 97 | } 98 | 99 | .scrollpane.sp-projects .sp-item:hover, 100 | .project-grid .pg-project:hover { 101 | background-color: #78c2ff; 102 | color: black; 103 | box-shadow: 5px 5px 3px rgba(0, 0, 0, 0.643); 104 | } 105 | 106 | .scrollpane.sp-studios .sp-item { 107 | background-color: #008cff; 108 | color: white; 109 | 110 | } 111 | 112 | .scrollpane.sp-studios .sp-item:hover { 113 | background-color: #78c2ff; 114 | color: black; 115 | } 116 | 117 | span.stats { 118 | color: lightgrey; 119 | } 120 | 121 | .scrollpane .sp-header { 122 | background-color: rgba(119, 119, 119, 0.643); 123 | } 124 | 125 | section { 126 | background: rgba(119, 119, 119, 0.643); 127 | box-shadow: 0px 0px 10px rgba(119, 119, 119, 0.643); 128 | color: white; 129 | } 130 | 131 | #banner { 132 | background: linear-gradient(90deg, rgba(120, 194, 255, 1) 0%, rgba(135, 120, 255, 1) 100%); 133 | box-shadow: 0px 0px 10px linear-gradient(00deg, rgba(120, 194, 255, 1) 0%, rgba(135, 120, 255, 1) 100%); 134 | color: white; 135 | } 136 | 137 | #banner button.purple, 138 | button.secondary { 139 | background-color: transparent; 140 | color: white; 141 | } 142 | 143 | #banner button.purple:hover, 144 | button.secondary:hover { 145 | background-color: #008cff; 146 | color: white; 147 | } 148 | 149 | .project-grid .pg-project { 150 | background-color: #008cff; 151 | color: rgb(255, 255, 255); 152 | } 153 | -------------------------------------------------------------------------------- /static/styles-newspaper.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | A clean light theme for Snazzle inspired by Wikipedia. 4 | Author: @redstone-scratch 5 | 6 | */ 7 | @import url("master-styles.css"); 8 | @font-face { 9 | font-family: "Playfair Display"; 10 | src: url("PlayFairDisplay.ttf") format("truetype"); 11 | font-weight: 400; 12 | } 13 | body { 14 | background-color: #c8c8c8; 15 | } 16 | 17 | ::-webkit-scrollbar-thumb { 18 | background: #ffffff; 19 | border-radius: 10px; 20 | } 21 | 22 | ::-webkit-scrollbar-thumb:hover { 23 | background: #bababa; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | p, 33 | div, 34 | button, 35 | input, 36 | select, 37 | article { 38 | font-family: 'Playfair Display', serif; 39 | } 40 | 41 | input::placeholder { 42 | color: #868686; 43 | } 44 | 45 | nav { 46 | background: rgba(0, 0, 0, 0.544); 47 | } 48 | 49 | .logo a { 50 | color: #fff; 51 | } 52 | 53 | .logo a:hover { 54 | text-shadow: 0px 0px 3px rgb(255, 255, 255); 55 | } 56 | 57 | .nav-links li a { 58 | color: #fff; 59 | } 60 | 61 | button, 62 | select { 63 | border: 1px solid #000; 64 | background-color: #fff; 65 | color: black; 66 | } 67 | 68 | input { 69 | border: 1px solid #000; 70 | background-color: #fff; 71 | color: black; 72 | } 73 | 74 | input:focus { 75 | outline: 1px solid #fff; 76 | } 77 | 78 | button:hover { 79 | background-color: #000; 80 | color: white; 81 | } 82 | 83 | .scrollpane { 84 | 85 | background-color: #7a7a7a; 86 | box-shadow: 0px 0px 10px rgb(0, 0, 0) inset; 87 | } 88 | 89 | .scrollpane.sp-projects .sp-item, 90 | .project-grid .pg-project { 91 | background-color: #8d8d8d; 92 | color: #fff; 93 | 94 | } 95 | 96 | .scrollpane.sp-projects .sp-item:hover, 97 | .project-grid .pg-project:hover { 98 | background-color: #fff; 99 | color: black !important; 100 | } 101 | 102 | .scrollpane.sp-studios .sp-item { 103 | background-color: #ca6120; 104 | color: white; 105 | 106 | } 107 | 108 | .scrollpane.sp-studios .sp-item:hover { 109 | background-color: #8d8d8d; 110 | color: white; 111 | } 112 | 113 | span.stats { 114 | color: lightgrey; 115 | } 116 | 117 | section { 118 | background: #fff; 119 | color: black; 120 | } 121 | 122 | #banner { 123 | background: linear-gradient(90deg, rgb(195, 195, 195) 0%, rgb(102, 102, 102) 100%); 124 | color: black; 125 | } 126 | 127 | #banner button.purple, 128 | button.secondary { 129 | background-color: transparent; 130 | color: white; 131 | } 132 | 133 | #banner button.purple:hover, 134 | button.secondary:hover { 135 | color: black; 136 | } 137 | 138 | a, 139 | a:visited { 140 | color: #2064ca; 141 | } 142 | 143 | button a, 144 | button a:visited { 145 | color: black; 146 | } 147 | 148 | .topic-author { 149 | color: rgba(0, 0, 0, 0.5); 150 | font-weight: bold; 151 | } 152 | 153 | .scrollpane.sp-forum.sp-vert a button:hover .topic-author { 154 | color: rgba(255, 255, 255, 0.5); 155 | font-weight: bold; 156 | } 157 | 158 | #studio-banner section { 159 | background-color: rgba(255, 255, 255, 0.25); 160 | } 161 | 162 | blockquote, .forum-aside { 163 | color: white; 164 | } -------------------------------------------------------------------------------- /static/styles-nord.css: -------------------------------------------------------------------------------- 1 | /* 2 | Credit to the team at https://www.nordtheme.com/ for this wonderful theme 3 | Actual CSS written by Darth-Ness with modified (as in mostly copied) code of Redstone's 'Choco' theme 4 | */ 5 | 6 | @import url("master-styles.css"); 7 | 8 | @font-face { 9 | font-family: "Jost"; 10 | src: url("Jost.ttf") format("truetype"); 11 | font-weight: 400; 12 | } 13 | body { 14 | background-color: #2e3440; 15 | } 16 | 17 | ::-webkit-scrollbar-thumb { 18 | background: #3f4655; 19 | border-radius: 10px; 20 | } 21 | 22 | ::-webkit-scrollbar-thumb:hover { 23 | background: #292e38; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | p, 33 | div, 34 | button, 35 | input, 36 | article { 37 | font-family: 'Jost', sans-serif; 38 | } 39 | 40 | input::placeholder { 41 | color: #d8dee9; 42 | } 43 | 44 | nav { 45 | background: rgba(46, 52, 64, 0.544); 46 | } 47 | 48 | .logo a { 49 | color: #d8dee9; 50 | } 51 | 52 | .logo a:hover { 53 | text-shadow: 0px 0px 3px #d08770; 54 | } 55 | 56 | .nav-links li a { 57 | color: #d8dee9; 58 | } 59 | 60 | button, 61 | select { 62 | border: 1px solid #434c5e; 63 | background-color: #bf616a; 64 | color: #d8dee9; 65 | } 66 | 67 | input { 68 | border: 1px solid #bf616a; 69 | background-color: transparent; 70 | color: #d8dee9; 71 | } 72 | 73 | input:focus { 74 | outline: 1px solid #d8dee9; 75 | } 76 | 77 | button:hover { 78 | background-color: transparent; 79 | color: #d8dee9; 80 | } 81 | 82 | .scrollpane { 83 | 84 | background-color: #4c566a; 85 | box-shadow: 0px 0px 10px #2e3440 inset; 86 | } 87 | 88 | .scrollpane.sp-projects .sp-item, 89 | .project-grid .pg-project { 90 | background-color: #5e81ac; 91 | color: #d8dee9; 92 | 93 | } 94 | 95 | .scrollpane.sp-projects .sp-item:hover, 96 | .project-grid .pg-project:hover { 97 | background-color: #8fbcbb; 98 | color: #2e3440; 99 | box-shadow: 5px 5px 3px rgba(46, 34, 64, 0.5); 100 | } 101 | 102 | .scrollpane.sp-studios .sp-item { 103 | background-color: #5e81ac; 104 | color: #d8dee9; 105 | 106 | } 107 | 108 | .scrollpane.sp-studios .sp-item:hover { 109 | background-color: #8fbcbb; 110 | color: #2e3440; 111 | } 112 | 113 | span.stats { 114 | color: #4c566a; 115 | } 116 | 117 | .scrollpane .sp-header { 118 | background-color: rgba(46, 52, 64, 0.544); 119 | } 120 | 121 | section { 122 | background: #4c566a; 123 | box-shadow: 0px 0px 10px #4c566a; 124 | color: #d8dee8; 125 | } 126 | 127 | #banner { 128 | background: linear-gradient(90deg, #5e81ac, #d8dee9); 129 | box-shadow: 0px 0px 10px #2e3440; 130 | color: #d8dee9; 131 | } 132 | 133 | #banner button.purple, 134 | button.secondary { 135 | background-color: transparent; 136 | color: #d8dee9; 137 | } 138 | 139 | #banner button.purple:hover, 140 | button.secondary:hover { 141 | background-color: #bf616a; 142 | color: #d8dee9; 143 | } 144 | 145 | a, 146 | a:visited { 147 | color: #b48ead; 148 | } 149 | 150 | button a, 151 | button a:visited { 152 | color: #d8dee9; 153 | } 154 | -------------------------------------------------------------------------------- /static/styles-voidzero.css: -------------------------------------------------------------------------------- 1 | @import url("master-styles.css"); 2 | 3 | body { 4 | background-color: rgb(26, 11, 32); 5 | } 6 | 7 | ::-webkit-scrollbar-thumb { 8 | background: rgb(201, 84, 255); 9 | border-radius: 4px; 10 | } 11 | 12 | ::-webkit-scrollbar-thumb:hover { 13 | background: rgb(167, 71, 211); 14 | } 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6, 21 | p, 22 | div, 23 | button, 24 | input, 25 | article, 26 | textarea { 27 | font-family: Arial, Helvetica, sans-serif; 28 | } 29 | 30 | input::placeholder { 31 | color: rgb(152, 64, 193); 32 | } 33 | 34 | nav { 35 | background: rgba(0, 0, 0, 0.544); 36 | } 37 | 38 | .logo a { 39 | color: #fff; 40 | } 41 | 42 | .logo a:hover { 43 | text-shadow: 0px 0px 3px rgb(201, 84, 255); 44 | } 45 | 46 | .nav-links li a { 47 | color: #fff; 48 | } 49 | 50 | button, 51 | select { 52 | border: 1px solid rgb(201, 84, 255); 53 | background-color: rgb(201, 84, 255); 54 | color: white; 55 | } 56 | 57 | input, textarea { 58 | border: 1px solid rgb(201, 84, 255); 59 | background-color: transparent; 60 | color: white; 61 | } 62 | 63 | input:focus, textarea:focus { 64 | outline: 1px solid #fff; 65 | } 66 | 67 | button:hover { 68 | background-color: transparent; 69 | color: white; 70 | } 71 | 72 | .scrollpane { 73 | 74 | background-color: rgb(99, 42, 126); 75 | box-shadow: 0px 0px 10px rgb(0, 0, 0) inset; 76 | } 77 | 78 | .scrollpane.sp-projects .sp-item, 79 | .project-grid .pg-project { 80 | background-color: rgb(201, 84, 255); 81 | color: rgb(255, 255, 255); 82 | 83 | } 84 | 85 | .scrollpane.sp-projects .sp-item:hover, 86 | .project-grid .pg-project:hover { 87 | background-color: rgb(228, 170, 255); 88 | color: black; 89 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); 90 | } 91 | 92 | .scrollpane.sp-studios .sp-item { 93 | background-color: rgb(201, 84, 255); 94 | color: white; 95 | 96 | } 97 | 98 | .scrollpane.sp-studios .sp-item:hover { 99 | background-color: rgb(228, 170, 255); 100 | color: black; 101 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); 102 | } 103 | 104 | span.stats { 105 | color: lightgrey; 106 | } 107 | 108 | .scrollpane .sp-header { 109 | background-color: rgba(0, 0, 0, 0.544); 110 | } 111 | 112 | section { 113 | background: rgb(99, 42, 126); 114 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.4); 115 | color: white; 116 | } 117 | 118 | #banner { 119 | background: linear-gradient(180deg, rgb(152, 64, 193) 0%, rgb(99, 42, 126) 100%); 120 | box-shadow: 0px 10px 10px linear-gradient(180deg, rgb(152, 64, 193) 0%, rgb(99, 42, 126) 100%); 121 | color: white; 122 | 123 | padding: none !important; 124 | } 125 | 126 | #banner button.purple, 127 | button.secondary { 128 | background-color: transparent; 129 | color: white; 130 | } 131 | 132 | #banner button.purple:hover, 133 | button.secondary:hover { 134 | background-color: rgb(201, 84, 255); 135 | color: white; 136 | } 137 | 138 | a, 139 | a:visited { 140 | color: rgb(201, 84, 255); 141 | } 142 | 143 | button a, 144 | button a:visited { 145 | color: white; 146 | } 147 | 148 | #studio-banner section { 149 | background-color: rgba(0, 0, 0, 0.8); 150 | } 151 | 152 | .omni { 153 | margin-left: 2vw; 154 | margin-right: 2vw; 155 | } 156 | 157 | section, #banner { 158 | margin-bottom: 2vh !important; 159 | } 160 | 161 | #footer { 162 | margin-top: 2vh; 163 | } 164 | 165 | button { 166 | border-radius: 4px !important; 167 | } 168 | 169 | div.sp-item { 170 | border-radius: 4px !important; 171 | } 172 | 173 | #banner { 174 | border-radius: 4px !important; 175 | } 176 | 177 | section { 178 | border-radius: 4px !important; 179 | } 180 | 181 | -------------------------------------------------------------------------------- /templates/_banner.html: -------------------------------------------------------------------------------- 1 |
2 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /templates/_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Snazzle - {% block title %}A better frontend for Scratch, built by the community, for the community{% endblock 6 | %} 7 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 61 | 62 | 63 |
64 | {% block content %} 65 |
66 |

Something went wrong.

67 |

The page didn't load properly.

68 |
69 | {% endblock %} 70 |
71 | 72 | {% include '_footer.html' %} 73 | 74 | 77 | 78 | {% if use_sb2 == False %} 79 | 88 | {% else %} 89 | 98 | {% endif %} 99 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /templates/_dl_mockup.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | 9 |
10 |
11 |

Download for Windows

12 |

Instructions

13 |

Installer (.msi)

14 |
    15 |
  1. Download the installer
  2. 16 |
  3. Run it with your favourite settings
  4. 17 |
  5. You can now launch Snazzle from wherever
  6. 18 |
19 |

Portable (.zip)

20 |
    21 |
  1. Download the file
  2. 22 |
  3. Unzip on your favourite USB drive
  4. 23 |
  5. You can now take Snazzle with you, wherever, or install it without admin priveliges
  6. 24 |
25 |
26 |
27 |

Download for macOS

28 |

Instructions

29 |

Installer (.dmg)

30 |
    31 |
  1. Download the installer
  2. 32 |
  3. Double-click it to attach it
  4. 33 |
  5. Drag Snazzle to your Applications folder
  6. 34 |
  7. You can now launch Snazzle from wherever
  8. 35 |
36 |
37 |
38 |

Download for Linux

39 |

Instructions

40 | 41 |

Just do what that guy is doing

42 |

(Linux builds coming when we get around to it)

43 |
44 |
45 |
-------------------------------------------------------------------------------- /templates/_download.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 |

Desktop versions coming soon

6 | 7 |
8 | 9 | 44 |
-------------------------------------------------------------------------------- /templates/_error.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 |

{{ errdata.code }}: {{ errdata.name }}

7 |

{{ errdata.description }} Do you want to go back?

8 |
9 | 10 | {% endblock %} -------------------------------------------------------------------------------- /templates/_footer.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /templates/dlm.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 | 5 | {% include '_dl_mockup.html' %} 6 | 7 | {% endblock %} -------------------------------------------------------------------------------- /templates/download.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block title %}Downloads{% endblock %} 4 | 5 | {% block content %} 6 | 10 | 11 |
12 |
13 |

Download for Windows

14 |

Instructions

15 |

Installer (.msi)

16 |
    17 |
  1. Download the installer
  2. 18 |
  3. Run it with your favourite settings
  4. 19 |
  5. You can now launch Snazzle from wherever
  6. 20 |
21 |

Portable (.zip)

22 |
    23 |
  1. Download the file
  2. 24 |
  3. Unzip on your favourite USB drive
  4. 25 |
  5. You can now take Snazzle with you, wherever, or install it without admin priveliges
  6. 26 |
27 |
28 |
29 |

Download for macOS

30 |

Instructions

31 |

Installer (.dmg)

32 |
    33 |
  1. Download the installer
  2. 34 |
  3. Double-click it to attach it
  4. 35 |
  5. Drag Snazzle to your Applications folder
  6. 36 |
  7. You can now launch Snazzle from wherever
  8. 37 |
38 |
39 |
40 |

Download for Linux

41 | 42 |
43 |
44 | {% endblock %} -------------------------------------------------------------------------------- /templates/editor.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 |
5 |

We are currently working on the editor. It will launch sometime next year.

6 |
7 | {% endblock %} -------------------------------------------------------------------------------- /templates/forum-topic.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block title %}Forums - {{ topic_title }}{% endblock %} 4 | 5 | {% block content %} 6 | {% macro forum_post(author, status, time, content, index, is_deleted, source, id) %} 7 | 8 | {% if is_deleted and show_deleted or not is_deleted %} 9 |
10 | 11 |
12 |
13 | {{ author }}'s profile picture 14 |

{{ author }}

15 |

{{ status or 'Status not found' }}

16 |

{{ time }}

17 |
18 | 25 |
26 |
27 |
28 | 36 |
37 | 38 |
39 | {% endif %} 40 | 41 | {% endmacro %} 42 | 43 | {% macro pagination(route) %} 44 |

45 | Previous 46 | | 47 | Next 48 |

49 | {% endmacro %} 50 | 51 |
52 |

{{ topic_title }}

53 | 54 |
55 | 56 |
57 | {{ pagination("/forums/topic/" + topic_id) }} 58 |
59 | 60 | {% for post in topic_posts %} 61 | {{ forum_post(post['author'], post['author_status']['status'], post['time'], post['html_content'], post['index'], 62 | post['is_deleted'], post['bb_content'], post['id']) }} 63 | {% endfor %} 64 | 65 |
66 | {{ pagination("/forums/topic/" + topic_id) }} 67 | 68 |
69 | 70 | {% endblock %} -------------------------------------------------------------------------------- /templates/forum-topics.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block title %}Forums - {{ subforum }}{% endblock %} 4 | 5 | {% macro pagination() %} 6 | Previous 7 | | 8 | Next 9 | {% endmacro %} 10 | 11 | {% block content %} 12 |
13 | 14 |

{{ subforum }}

15 |

Loaded {{ len(topics) }} topics.

16 | 17 | {% if subforum in pinned_subforums %} 18 | 19 | 20 | 21 | {% else %} 22 | 23 | 24 | 25 | {% endif %} 26 | {{ pagination() }} 27 | 28 |
29 |
30 | {% for topic in topics %} 31 | 32 | {% endfor %} 33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /templates/forums.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block title %}Forums{% endblock %} 3 | {% block content %} 4 |
5 |

Forum Browser

6 |

NOTE: This does not work very well at all anymore due to ScratchDB being down indefinitely.

7 |
8 | 9 | {% if pinned_subforums %} 10 |
11 |
12 |

Pinned subforums

13 |
14 | {% for subforum in pinned_subforums %} 15 |
16 |

17 | 19 | {{ subforum }} 20 | 21 |

22 |
23 | {% endfor %} 24 |
25 |
26 | {% endif %} 27 | {% if pinned_subforums %} 28 |
29 | {% else %} 30 |
31 | {% endif %} 32 | {% for title, subforums in data %} 33 |
34 |

{{ title }}

35 |
36 | {% for subforum in subforums %} 37 | 49 | {% endfor %} 50 |
51 |
52 | {% endfor %} 53 |
54 | {% if pinned_subforums %} 55 |
56 | {% endif %} 57 | {% endblock %} -------------------------------------------------------------------------------- /templates/img.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block content %} 4 | {{ img_url }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 | {% if signed_in %} 5 | {% include 'loggedin.html' %} 6 | {% endif %} 7 |
8 | {% if not signed_in %} 9 | {% include '_banner.html' %} 10 | {% endif %} 11 |
12 |

Featured projects

13 |
14 | {% for project in featured_projects %} 15 |
16 | 17 | {{ project['title'] }} 19 | 20 |
{{ project["title"] }} 22 |
23 | {{ project["creator"] }} 24 | {{project["love_count"]}} 25 |
26 | {% endfor %} 27 |
28 |

29 |
30 |

Featured studios

31 |
32 | {% for studio in featured_studios %} 33 |
34 | 35 | {{ studio['title'] }} 37 | 38 |
{{ studio["title"] 40 | }} 41 |
42 |
43 | {% endfor %} 44 |
45 |

46 |
47 |

Trending projects

48 |
49 | {% for project in trending_projects %} 50 |
51 | 52 | {{ project['title'] }} 54 | 55 |
{{ project["title"] }} 57 |
58 |
by {{ project["author"]["username"] }} 59 |
60 | {{ project["stats"]["loves"] }} 61 |
62 | {% endfor %} 63 |
64 |

65 |
66 |

What the community is loving

67 |
68 | {% for project in loving %} 69 |
70 | 71 | {{ project['title'] }} 73 | 74 |
{{ project["title"] }} 76 |
77 | {{ project["creator"] }} 78 | {{ project["love_count"] }} 79 |
80 | {% endfor %} 81 |
82 |
83 |
84 |
85 |

What the community is remixing

86 |
87 | {% for project in remixed %} 88 |
89 | 90 | {{project['title']}} 92 | 93 |
94 | {{ project["title"] }} 95 |
96 | {{ project["creator"] }} 97 | {{ project["love_count"] }} 98 |
99 | {% endfor %} 100 |
101 |
102 |
103 | {% endblock %} -------------------------------------------------------------------------------- /templates/loggedin.html: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |

What's happening

7 |
    8 | {% if feed %} 9 | {% for event in feed %} 10 |
  • {{ event.subject }} {{ event.verb }} {{ event.object }}
  • 11 | {% endfor %} 12 | {% else %} 13 |
  • Your feed is empty. Go try following some people!
  • 14 | {% endif %} 15 |
16 |
17 |
18 |

Scratch News

19 |
20 |
    21 | {% if feed %} 22 | {% for event in news %} 23 |
  • {{ event.text }}
  • 24 | {% endfor %} 25 | {% else %} 26 |
  • This site is pretty new, so...
  • 27 | {% endif %} 28 |
29 |
30 |
31 |
-------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnarpleDev/Snazzle/5eed72796303a52aafa5abdafd81280a1cc67c41/templates/login.html -------------------------------------------------------------------------------- /templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | 3 | {% block title %}Profile{% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% endblock %} -------------------------------------------------------------------------------- /templates/projects-scratch.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 |

5 | {% endblock %} -------------------------------------------------------------------------------- /templates/projects.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block title %}Projects - {{name}}{% endblock %} 4 | 5 | {% block content %} 6 | 38 |
39 |

{{ name }}

40 |

By {{ creator_name }}

41 |

42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/scratchapi-error.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% if message == "api_notfound" %} 6 | 7 |

Not Found

8 |

The studio, project or user you were looking for wasn't found.

9 | 10 | {% elif message == "api_invalid" %} 11 | 12 |

todo

13 | 14 | {% else %} 15 | 16 |

Unknown Error

17 |

An unknown error occurred with the Scratch API.

18 | 19 | {% endif %} 20 | 21 | 22 | 23 |
24 | 25 | {% endblock %} -------------------------------------------------------------------------------- /templates/scratchdb-error.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% if err == 'sdb_categorynotfounderror' %} 7 |

Invalid category

8 |

That category doesn't exist

9 | {% elif err == 'sdb_pagenotvaliderror' %} 10 |

Page not valid

11 |

This is a technical error on a developer's part.

12 |

This is the description from the ScratchDB docs:

13 | the page number was invalid, needs to be >=0 and a whole number. 14 | {% elif err == 'sdb_invalidoptionerror' %} 15 |

Invalid option

16 |

This is a technical error on a developer's part.

17 |

This is the description from the ScratchDB docs:

18 | the page number was invalid, needs to be >=0 and a whole number. 19 | {% elif err == 'lib_scratchdbdown' %} 20 |

ScratchDB is down

21 |

ScratchDB has been put on hiatus indefinitely, which breaks a whole bunch of Snazzle features.

22 |

This is not something we can fix, so we're working on something to remedy the situation 👀

23 | {% elif err == 'sdb_topicnotfound' %} 24 |

Topic not found

25 |

This topic might exist on Scratch, or ScratchDB hasn't indexed it yet.

26 | {% elif err == 'sdb_topicnotvalid' %} 27 |

Topic not valid

28 |

The topic ID is not valid (a positive whole number).

29 | {% elif err == 'sdb_nomorepostserror' %} 30 |

You've reached the end!

31 |

There are no more posts in the topic.

32 | {% endif %} 33 | 34 |
35 | 36 | {% endblock %} -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Search

6 |

Search results for {{ query }}

7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | {% if result == []%} 36 |

No results :(

37 | {% else %} 38 | {% for project in result %} 39 |
40 | 41 | project image 42 | 43 |
{{ project.title }} 44 | {{ project.author.username }}
45 | {{ project.stats.views}} views 46 |
47 | {% endfor %} 48 | {% endif %} 49 |
50 | {% endblock %} -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 | 9 |
10 |
11 |
12 |

13 | Chicken pics 14 |

15 |

Settings

16 |

Customize Snazzle to your liking.

17 | 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 51 | 52 |
53 |

Change Theme

54 | 64 | 65 |
66 | 70 |
71 |

Forums

72 | 74 |
75 |
77 |
78 | 79 | 80 |
81 | 89 | 93 |
94 | 95 |
96 |
97 |
98 |
99 | 100 | 120 | 121 | {% endblock %} 122 | 123 | -------------------------------------------------------------------------------- /templates/studio.html: -------------------------------------------------------------------------------- 1 | {% macro user(name, img_url, status, role) %} 2 | 3 |
4 |
5 | {{ img_url }} 6 |

{{ name }}

7 | {% if role == 2 %} 8 | 9 | {% elif role == 1 %} 10 | 11 | {% endif %} 12 |
13 |
14 | 15 | {% endmacro %} 16 | 17 | {% macro comment(user, comment, pinned) %} 18 | 19 |
20 | {{ user(user["name"], user["avatar"], user["status"], comment["user"]["role"]) }} 21 |

{{ comment["text"] }}

22 |
23 | 24 | {% endmacro %} 25 | 26 | {% extends '_base.html' %} 27 | 28 | {% block title %} 29 | Snazzle - Studios - {{ studio_name }} 30 | {% endblock %} 31 | 32 | {% block content %} 33 | 34 |
35 | 53 | 54 |
55 |
56 |

{{ studio_name }}

57 |
58 |

Description

{{ studio_description }} 59 |
60 |
61 |
62 |
63 |

Stats

64 |

Comments: {{ studio_stats['comments'] }}

65 |

Followers: {{ studio_stats['followers'] }}

66 |

Managers: {{ studio_stats['managers'] }}

67 |

Projects: {{ studio_stats['projects'] }}

68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 |
81 |

{{ studio_tab | title }}

82 | {% if studio_tab == "projects" %} 83 |

We're working on it!

84 | {% elif studio_tab == "comments" %} 85 |

Coming soon!

86 | {% elif studio_tab == "activity" %} 87 |

Hang tight!

88 | {% elif studio_tab == "people" %} 89 |

Patience is a virtue!

90 | {% else %} 91 |

Oops, that page doesn't exist.

92 | {% endif %} 93 |
94 |
95 | 96 | {% endblock %} -------------------------------------------------------------------------------- /templates/suggestions-post.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block title %}Forums - {{ topic_title }}{% endblock %} 4 | 5 | {% block content %} 6 | {% macro forum_post(author, status, time, content, index, is_deleted) %} 7 | 8 | {% if is_deleted and show_deleted or not is_deleted %} 9 |
10 | 11 |
12 |
13 | {{ author }}'s profile picture 14 |

{{ author }}

15 |

{{ status }}

16 |

{{ time }}

17 |
18 |
19 | {{ content | safe }} 20 | 21 |
22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | {% endif %} 30 | 31 | {% endmacro %} 32 | 33 | {% macro pagination(route) %} 34 |

35 | Previous 36 | | 37 | Next 38 |

39 | {% endmacro %} 40 | 41 |
42 |

{{ topic_title }}

43 | 44 |
45 | 46 |
47 | {{ pagination("/forums/topic/{{ topic_id }}") }} 48 |
49 | 50 | {% for post in topic_posts %} 51 | {{ forum_post(post['author'], post['author_status']['status'], post['time'], post['html_content'], post['index'], post['is_deleted']) }} 52 | {% endfor %} 53 | 54 |
55 | {{ pagination("/forums/topic/{{ topic_id }}") }} 56 | 57 |
58 | {% endblock %} -------------------------------------------------------------------------------- /templates/trending.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Explore

6 |

Find cool projects

7 |

Filters are not functional yet. What you see here is a mockup. We are working on it and will release it as fast 8 | as we can

9 |
10 |
11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | 40 | 69 | {% endblock %} 70 | 71 | 72 | --------------------------------------------------------------------------------