Merge pull request #18 from Fluffy-Bean/unstable

Unstable
This commit is contained in:
Michał 2023-04-05 20:56:21 +01:00 committed by GitHub
commit ad4c6f2fb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1121 additions and 847 deletions

BIN
.github/images/group-mobile.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

BIN
.github/images/group.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

BIN
.github/images/photo-mobile.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

BIN
.github/images/photo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -32,13 +32,27 @@
And many more planned things!
## screenshots
<h2>Screenshots</h2>
<details><summary>Home Screen</summary>
Home-screen
![screenshot](.github/images/homepage.png)
Image view
![screenshot](.github/images/imageview.png)
</details>
<details><summary>Photo View</summary>
![screenshot](.github/images/photo.png)
![screenshot](.github/images/photo-mobile.png)
</details>
<details><summary>Photo Group</summary>
![screenshot](.github/images/group.png)
![screenshot](.github/images/group-mobile.png)
</details>
## Running

View file

@ -11,6 +11,7 @@ import logging
from flask_compress import Compress
from flask_caching import Cache
from flask_assets import Environment, Bundle
from flask_login import LoginManager
from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException
@ -19,35 +20,41 @@ import platformdirs
from dotenv import load_dotenv
from yaml import safe_load
# Utils
from gallery.utils import theme_manager
# Import database
from sqlalchemy.orm import sessionmaker
from gallery import db
USER_DIR = platformdirs.user_config_dir('onlylegs')
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
login_manager = LoginManager()
assets = Environment()
cache = Cache(config={'CACHE_TYPE': 'SimpleCache', 'CACHE_DEFAULT_TIMEOUT': 300})
compress = Compress()
def create_app(test_config=None):
"""
Create and configure the main app
"""
app = Flask(__name__, instance_path=os.path.join(USER_DIR, 'instance'))
assets = Environment()
cache = Cache(config={'CACHE_TYPE': 'SimpleCache', 'CACHE_DEFAULT_TIMEOUT': 300})
compress = Compress()
# Get environment variables
load_dotenv(os.path.join(USER_DIR, '.env'))
print("Loaded environment variables")
# Get config file
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as file:
with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8', mode='r') as file:
conf = safe_load(file)
print("Loaded gallery config")
print("Loaded config")
# App configuration
app.config.from_mapping(
SECRET_KEY=os.environ.get('FLASK_SECRET'),
DATABASE=os.path.join(app.instance_path, 'gallery.sqlite'),
DATABASE=os.path.join(app.instance_path, 'gallery.sqlite3'),
UPLOAD_FOLDER=os.path.join(USER_DIR, 'uploads'),
ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'],
MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'],
@ -59,28 +66,54 @@ def create_app(test_config=None):
else:
app.config.from_mapping(test_config)
# Load theme
theme_manager.compile_theme('default', app.root_path)
login_manager.init_app(app)
login_manager.login_view = 'gallery.index'
login_manager.session_protection = 'strong'
# Bundle JS files
js_scripts = Bundle('js/*.js', output='gen/packed.js')
assets.register('js_all', js_scripts)
@login_manager.user_loader
def load_user(user_id):
return db_session.query(db.Users).filter_by(alt_id=user_id).first()
@login_manager.unauthorized_handler
def unauthorized():
error = 401
msg = 'You are not authorized to view this page!!!!'
return render_template('error.html', error=error, msg=msg), error
js_pre = Bundle(
'js/pre/*.js',
output='gen/pre_packed.js',
depends='js/pre/*.js'
)
js_post = Bundle(
'js/post/*.js',
output='gen/post_packed.js',
depends='js/post/*.js'
)
styles = Bundle(
'sass/*.sass',
filters='libsass',
output='gen/styles.css',
depends='sass/**/*.sass'
)
assets.register('js_pre', js_pre)
assets.register('js_post', js_post)
assets.register('styles', styles)
# Error handlers, if the error is not a HTTP error, return 500
@app.errorhandler(Exception)
def error_page(err): # noqa
if not isinstance(err, HTTPException):
abort(500)
return render_template('error.html',
error=err.code,
msg=err.description), err.code
return render_template('error.html', error=err.code, msg=err.description), err.code
# Load login, registration and logout manager
from gallery import auth
app.register_blueprint(auth.blueprint)
# Load the different routes
from gallery.routes import api, groups, routing, settings
from gallery.views import api, groups, routing, settings
app.register_blueprint(api.blueprint)
app.register_blueprint(groups.blueprint)
app.register_blueprint(routing.blueprint)

View file

@ -3,17 +3,14 @@ OnlyLegs - Authentication
User registration, login and logout and locking access to pages behind a login
"""
import re
import uuid
import logging
from datetime import datetime as dt
import functools
from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify
from flask import Blueprint, flash, redirect, request, url_for, abort, jsonify
from werkzeug.security import check_password_hash, generate_password_hash
from sqlalchemy.orm import sessionmaker
from sqlalchemy import exc
from flask_login import login_user, logout_user, login_required
from sqlalchemy.orm import sessionmaker
from gallery import db
@ -22,42 +19,31 @@ db_session = sessionmaker(bind=db.engine)
db_session = db_session()
def login_required(view):
@blueprint.route('/login', methods=['POST'])
def login():
"""
Decorator to check if a user is logged in before accessing a page
Log in a registered user by adding the user id to the session
"""
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None or session.get('uuid') is None:
logging.error('Authentication failed')
session.clear()
return redirect(url_for('gallery.index'))
error = []
return view(**kwargs)
username = request.form['username'].strip()
password = request.form['password'].strip()
remember = bool(request.form['remember-me'])
return wrapped_view
user = db_session.query(db.Users).filter_by(username=username).first()
if not user or not check_password_hash(user.password, password):
logging.error('Login attempt from %s', request.remote_addr)
error.append('Username or Password is incorrect!')
@blueprint.before_app_request
def load_logged_in_user():
"""
Runs before every request and checks if a user is logged in
"""
user_id = session.get('user_id')
user_uuid = session.get('uuid')
if error:
abort(403)
if user_id is None or user_uuid is None:
g.user = None
session.clear()
else:
is_alive = db_session.query(db.Sessions).filter_by(session_uuid=user_uuid).first()
login_user(user, remember=remember)
if is_alive is None:
logging.info('Session expired')
flash(['Session expired!', '3'])
session.clear()
else:
g.user = db_session.query(db.Users).filter_by(id=user_id).first()
logging.info('User %s logged in from %s', username, request.remote_addr)
flash(['Logged in successfully!', '4'])
return 'ok', 200
@blueprint.route('/register', methods=['POST'])
@ -65,17 +51,18 @@ def register():
"""
Register a new user
"""
error = []
# Thanks Fennec for reminding me to strip out the whitespace lol
username = request.form['username'].strip()
email = request.form['email'].strip()
password = request.form['password'].strip()
password_repeat = request.form['password-repeat'].strip()
error = []
email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
username_regex = re.compile(r'\b[A-Za-z0-9._-]+\b')
# Validate the form
if not username or not username_regex.match(username):
error.append('Username is invalid!')
@ -92,77 +79,30 @@ def register():
elif password_repeat != password:
error.append('Passwords do not match!')
if error:
return jsonify(error)
user_exists = db_session.query(db.Users).filter_by(username=username).first()
if user_exists:
error.append('User already exists!')
try:
register_user = db.Users(username=username,
email=email,
password=generate_password_hash(password),
created_at=dt.utcnow())
db_session.add(register_user)
db_session.commit()
except exc.IntegrityError:
return f'User {username} is already registered!'
except Exception as err:
logging.error('User %s could not be registered: %s', username, err)
return 'Something went wrong!'
# If there are errors, return them
if error:
print(error)
return jsonify(error), 400
register_user = db.Users(username=username, email=email,
password=generate_password_hash(password, method='sha256'))
db_session.add(register_user)
db_session.commit()
logging.info('User %s registered', username)
return 'gwa gwa'
@blueprint.route('/login', methods=['POST'])
def login():
"""
Log in a registered user by adding the user id to the session
"""
username = request.form['username'].strip()
password = request.form['password'].strip()
user = db_session.query(db.Users).filter_by(username=username).first()
error = []
if user is None:
logging.error('User %s does not exist. Login attempt from %s',
username, request.remote_addr)
error.append('Username or Password is incorrect!')
elif not check_password_hash(user.password, password):
logging.error('User %s entered wrong password. Login attempt from %s',
username, request.remote_addr)
error.append('Username or Password is incorrect!')
if error:
abort(403)
try:
session.clear()
session['user_id'] = user.id
session['uuid'] = str(uuid.uuid4())
session_query = db.Sessions(user_id=user.id,
session_uuid=session.get('uuid'),
ip_address=request.remote_addr,
user_agent=request.user_agent.string,
active=True,
created_at=dt.utcnow())
db_session.add(session_query)
db_session.commit()
except Exception as err:
logging.error('User %s could not be logged in: %s', username, err)
abort(500)
logging.info('User %s logged in from %s', username, request.remote_addr)
flash(['Logged in successfully!', '4'])
return 'gwa gwa'
return 'ok', 200
@blueprint.route('/logout')
@login_required
def logout():
"""
Clear the current session, including the stored user id
"""
logging.info('User (%s) %s logged out', session.get('user_id'), g.user.username)
session.clear()
logout_user()
flash(['Goodbye!!!', '4'])
return redirect(url_for('gallery.index'))

View file

@ -1,16 +1,18 @@
"""
OnlyLegs - Database models and functions for SQLAlchemy
OnlyLegs - Database models and ions for SQLAlchemy
"""
from uuid import uuid4
import os
import platformdirs
from sqlalchemy import (
create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey, PickleType)
from sqlalchemy import (create_engine, Column, Integer, String,
DateTime, ForeignKey, PickleType, func)
from sqlalchemy.orm import declarative_base, relationship
from flask_login import UserMixin
USER_DIR = platformdirs.user_config_dir('onlylegs')
DB_PATH = os.path.join(USER_DIR, 'gallery.sqlite')
DB_PATH = os.path.join(USER_DIR, 'instance', 'gallery.sqlite3')
# In the future, I want to add support for other databases
@ -18,24 +20,29 @@ engine = create_engine(f'sqlite:///{DB_PATH}', echo=False)
base = declarative_base()
class Users (base): # pylint: disable=too-few-public-methods, C0103
class Users (base, UserMixin): # pylint: disable=too-few-public-methods, C0103
"""
User table
Joins with post, groups, session and log
"""
__tablename__ = 'users'
# Gallery used information
id = Column(Integer, primary_key=True)
alt_id = Column(String, unique=True, nullable=False, default=str(uuid4()))
profile_picture = Column(String, nullable=True, default=None)
username = Column(String, unique=True, nullable=False)
email = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False)
joined_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102
posts = relationship('Posts', backref='users')
groups = relationship('Groups', backref='users')
session = relationship('Sessions', backref='users')
log = relationship('Logs', backref='users')
def get_id(self):
return str(self.alt_id)
class Posts (base): # pylint: disable=too-few-public-methods, C0103
"""
@ -46,20 +53,16 @@ class Posts (base): # pylint: disable=too-few-public-methods, C0103
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, nullable=False)
file_name = Column(String, unique=True, nullable=False)
file_type = Column(String, nullable=False)
image_exif = Column(PickleType, nullable=False)
image_colours = Column(PickleType, nullable=False)
post_description = Column(String, nullable=False)
post_alt = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102
filename = Column(String, unique=True, nullable=False)
mimetype = Column(String, nullable=False)
exif = Column(PickleType, nullable=False)
colours = Column(PickleType, nullable=False)
description = Column(String, nullable=False)
alt = Column(String, nullable=False)
junction = relationship('GroupJunction', backref='posts')
class Groups (base): # pylint: disable=too-few-public-methods, C0103
"""
Group table
@ -71,7 +74,7 @@ class Groups (base): # pylint: disable=too-few-public-methods, C0103
name = Column(String, nullable=False)
description = Column(String, nullable=False)
author_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102
junction = relationship('GroupJunction', backref='groups')
@ -84,27 +87,11 @@ class GroupJunction (base): # pylint: disable=too-few-public-methods, C0103
__tablename__ = 'group_junction'
id = Column(Integer, primary_key=True)
date_added = Column(DateTime, nullable=False)
date_added = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102
group_id = Column(Integer, ForeignKey('groups.id'))
post_id = Column(Integer, ForeignKey('posts.id'))
class Sessions (base): # pylint: disable=too-few-public-methods, C0103
"""
Session table
Joins with user
"""
__tablename__ = 'sessions'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
session_uuid = Column(String, nullable=False)
ip_address = Column(String, nullable=False)
user_agent = Column(String, nullable=False)
active = Column(Boolean, nullable=False)
created_at = Column(DateTime, nullable=False)
class Logs (base): # pylint: disable=too-few-public-methods, C0103
"""
Log table
@ -116,8 +103,8 @@ class Logs (base): # pylint: disable=too-few-public-methods, C0103
user_id = Column(Integer, ForeignKey('users.id'))
ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False)
msg = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False)
note = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102
class Bans (base): # pylint: disable=too-few-public-methods, C0103
@ -129,8 +116,8 @@ class Bans (base): # pylint: disable=too-few-public-methods, C0103
id = Column(Integer, primary_key=True)
ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False)
msg = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False)
note = Column(String, nullable=False)
banned_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102
# check if database file exists, if not create it

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,25 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
let labels = document.querySelectorAll('[data-label]');
for (let i = 0; i < labels.length; i++) {
labels[i].addEventListener('mouseover', function() {
let label = document.createElement('div');
label.classList.add('label');
label.innerHTML = this.dataset.label;
document.body.appendChild(label);
label.style.left = (this.offsetLeft + this.offsetWidth + 8) + 'px';
label.style.top = (this.offsetTop + (label.offsetHeight / 2) - 2) + 'px';
setTimeout(function() {
label.style.opacity = 1;
}, 250);
});
labels[i].addEventListener('mouseout', function() {
let label = document.querySelector('.label');
label.parentNode.removeChild(label);
});
}
});

View file

@ -30,8 +30,24 @@ function showLogin() {
passwordInput.placeholder = 'Passywassy';
passwordInput.id = 'password';
// Container for remember me checkbox
rememberMeSpan = document.createElement('span');
rememberMeSpan.classList.add('input-checkbox');
rememberMeInput = document.createElement('input');
rememberMeInput.type = 'checkbox';
rememberMeInput.id = 'remember-me';
rememberMeLabel = document.createElement('label');
rememberMeLabel.innerHTML = 'No forgetty me pls';
rememberMeLabel.setAttribute('for', 'remember-me');
rememberMeSpan.appendChild(rememberMeInput);
rememberMeSpan.appendChild(rememberMeLabel);
loginForm.appendChild(usernameInput);
loginForm.appendChild(passwordInput);
loginForm.appendChild(rememberMeSpan);
popUpShow(
'Login!',
@ -47,6 +63,7 @@ function login(event) {
let formUsername = document.querySelector("#username").value;
let formPassword = document.querySelector("#password").value;
let formRememberMe = document.querySelector("#remember-me").checked;
if (formUsername === "" || formPassword === "") {
addNotification("Please fill in all fields!!!!", 3);
@ -54,27 +71,24 @@ function login(event) {
}
// Make form
var formData = new FormData();
const formData = new FormData();
formData.append("username", formUsername);
formData.append("password", formPassword);
formData.append("remember-me", formRememberMe);
fetch('/auth/login', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
if (response.ok) {
location.reload();
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Wrong information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
if (response.status === 403) {
addNotification('None but devils play past here... Wrong information', 2);
} else if (response.status === 500) {
addNotification('Server exploded, F\'s in chat', 2);
} else {
addNotification('Error logging in, blame someone', 2);
}
}
}).catch(error => {
@ -129,7 +143,7 @@ function showRegister() {
registerForm.appendChild(emailInput);
registerForm.appendChild(passwordInput);
registerForm.appendChild(passwordInputRepeat);
popUpShow(
'Who are you?',
'Already have an account? <span class="link" onclick="showLogin()">Login!</span>',
@ -153,37 +167,33 @@ function register(event) {
}
// Make form
var formData = new FormData();
const formData = new FormData();
formData.append("username", formUsername);
formData.append("email", formEmail);
formData.append("password", formPassword);
formData.append("password-repeat", formPasswordRepeat);
// Send form to server
fetch('/auth/login', {
fetch('/auth/register', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
if (response === "gwa gwa") {
addNotification('Registered successfully! Now please login to continue', 1);
showLogin();
} else {
for (let i = 0; i < response.length; i++) {
addNotification(response[i], 2);
}
}
if (response.ok) {
addNotification('Registered successfully! Now please login to continue', 1);
showLogin();
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Wrong information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
if (response.status === 400) {
response.json().then(data => {
for (let i = 0; i < data.length; i++) {
addNotification(data[i], 2);
}
});
} else if (response.status === 403) {
addNotification('None but devils play past here... Wrong information', 2);
} else if (response.status === 500) {
addNotification('Server exploded, F\'s in chat', 2);
} else {
addNotification('Error logging in, blame someone', 2);
}
}
}).catch(error => {

View file

@ -141,38 +141,41 @@ function clearUpload() {
}
function createJob(file) {
jobContainer = document.createElement("div");
jobContainer.classList.add("job");
// function createJob(file) {
// jobContainer = document.createElement("div");
// jobContainer.classList.add("job");
jobStatus = document.createElement("span");
jobStatus.classList.add("job__status");
jobStatus.innerHTML = "Uploading...";
// jobStatus = document.createElement("span");
// jobStatus.classList.add("job__status");
// jobStatus.innerHTML = "Uploading...";
jobProgress = document.createElement("span");
jobProgress.classList.add("progress");
// jobProgress = document.createElement("span");
// jobProgress.classList.add("progress");
jobImg = document.createElement("img");
jobImg.src = URL.createObjectURL(file);
// jobImg = document.createElement("img");
// jobImg.src = URL.createObjectURL(file);
jobImgFilter = document.createElement("span");
jobImgFilter.classList.add("img-filter");
// jobImgFilter = document.createElement("span");
// jobImgFilter.classList.add("img-filter");
jobContainer.appendChild(jobStatus);
jobContainer.appendChild(jobProgress);
jobContainer.appendChild(jobImg);
jobContainer.appendChild(jobImgFilter);
// jobContainer.appendChild(jobStatus);
// jobContainer.appendChild(jobProgress);
// jobContainer.appendChild(jobImg);
// jobContainer.appendChild(jobImgFilter);
return jobContainer;
}
// return jobContainer;
// }
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', () => {
// Function to upload images
let uploadTab = document.querySelector(".upload-panel");
if (!uploadTab) { return; } // If upload tab doesn't exist, don't run this code :3
let uploadTabDrag = uploadTab.querySelector("#dragIndicator");
let uploadForm = uploadTab.querySelector('#uploadForm');
let jobList = document.querySelector(".upload-jobs");
// let jobList = document.querySelector(".upload-jobs");
let fileDrop = uploadForm.querySelector('.fileDrop-block');
let fileDropTitle = fileDrop.querySelector('.status');
@ -225,53 +228,82 @@ document.addEventListener('DOMContentLoaded', function() {
formData.append("description", fileDescription.value);
formData.append("tags", fileTags.value);
jobItem = createJob(fileUpload.files[0]);
jobStatus = jobItem.querySelector(".job__status");
// jobItem = createJob(fileUpload.files[0]);
// jobStatus = jobItem.querySelector(".job__status");
// Upload the information
$.ajax({
url: '/api/upload',
type: 'post',
data: formData,
contentType: false,
processData: false,
beforeSend: function () {
// Add job to list
jobList.appendChild(jobItem);
},
success: function (response) {
jobItem.classList.add("success");
jobStatus.innerHTML = "Uploaded successfully";
if (!document.querySelector(".upload-panel").classList.contains("open")) {
addNotification("Image uploaded successfully", 1);
}
},
error: function (response) {
jobItem.classList.add("critical");
switch (response.status) {
case 500:
jobStatus.innerHTML = "Server exploded, F's in chat";
break;
case 400:
case 404:
jobStatus.innerHTML = "Error uploading. Blame yourself";
break;
case 403:
jobStatus.innerHTML = "None but devils play past here...";
break;
case 413:
jobStatus.innerHTML = "File too large!!!!!!";
break;
default:
jobStatus.innerHTML = "Error uploading file, blame someone";
break;
}
if (!document.querySelector(".upload-panel").classList.contains("open")) {
addNotification("Error uploading file", 2);
}
},
// $.ajax({
// url: '/api/upload',
// type: 'post',
// data: formData,
// contentType: false,
// processData: false,
// beforeSend: function () {
// // Add job to list
// jobList.appendChild(jobItem);
// },
// success: function (response) {
// jobItem.classList.add("success");
// jobStatus.innerHTML = "Uploaded successfully";
// if (!document.querySelector(".upload-panel").classList.contains("open")) {
// addNotification("Image uploaded successfully", 1);
// }
// },
// error: function (response) {
// jobItem.classList.add("critical");
// switch (response.status) {
// case 500:
// jobStatus.innerHTML = "Server exploded, F's in chat";
// break;
// case 400:
// case 404:
// jobStatus.innerHTML = "Error uploading. Blame yourself";
// break;
// case 403:
// jobStatus.innerHTML = "None but devils play past here...";
// break;
// case 413:
// jobStatus.innerHTML = "File too large!!!!!!";
// break;
// default:
// jobStatus.innerHTML = "Error uploading file, blame someone";
// break;
// }
// if (!document.querySelector(".upload-panel").classList.contains("open")) {
// addNotification("Error uploading file", 2);
// }
// },
// });
fetch('/api/upload', {
method: 'POST',
body: formData
})
// .then(response => response.json())
.then(data => { addNotification("Image uploaded successfully", 1); })
.catch(error => {
switch (response.status) {
case 500:
addNotification("Server exploded, F's in chat", 2)
break;
case 400:
case 404:
addNotification("Error uploading. Blame yourself", 2)
break;
case 403:
addNotification("None but devils play past here...", 2)
break;
case 413:
addNotification("File too large!!!!!!", 2);
break;
default:
addNotification("Error uploading file, blame someone", 2)
break;
}
});
clearUpload();
// Reset drop

View file

@ -1,16 +1,27 @@
let webpSupport = false;
try {
new Image().src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';
webpSupport = true;
} catch (e) {
webpSupport = false;
}
// fade in images
function imgFade(obj, time = 250) {
$(obj).animate({ opacity: 1 }, time);
obj.style.transition = `opacity ${time}ms`;
obj.style.opacity = 1;
}
// Lazy load images when they are in view
function loadOnView() {
let lazyLoad = document.querySelectorAll('#lazy-load');
const lazyLoad = document.querySelectorAll('#lazy-load');
for (let i = 0; i < lazyLoad.length; i++) {
let image = lazyLoad[i];
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
if (!image.src) {
image.src = `/api/file/${image.getAttribute('data-src')}?r=thumb` // e=webp
if (!image.src && webpSupport) {
image.src = image.getAttribute('data-src') + '&e=webp'
} else if (!image.src) {
image.src = image.getAttribute('data-src')
}
}
}
@ -64,7 +75,7 @@ window.onload = function () {
'Using <a href="https://phosphoricons.com/">Phosphoricons</a> and ' +
'<a href="https://www.gent.media/manrope">Manrope</a> <br>' +
'Made by Fluffy and others with ❤️ <br>' +
'<a href="https://github.com/Fluffy-Bean/onlylegs">V23.03.30</a>');
'<a href="https://github.com/Fluffy-Bean/onlylegs">V23.04.05</a>');
}
}
};

View file

@ -25,13 +25,13 @@ function addNotification(notificationText, notificationLevel) {
iconElement.classList.add('sniffle__notification-icon');
notification.appendChild(iconElement);
// Set the icon based on the notification level, not pretty but it works :3
if (notificationLevel == 1) {
if (notificationLevel === 1) {
notification.classList.add('success');
iconElement.innerHTML = successIcon;
} else if (notificationLevel == 2) {
} else if (notificationLevel === 2) {
notification.classList.add('critical');
iconElement.innerHTML = criticalIcon;
} else if (notificationLevel == 3) {
} else if (notificationLevel === 3) {
notification.classList.add('warning');
iconElement.innerHTML = warningIcon;
} else {

View file

@ -0,0 +1,181 @@
.banner,
.banner-small
width: 100%
position: relative
color: RGB($fg-white)
&::after
content: ''
width: $rad
height: calc(#{$rad} * 2)
position: absolute
bottom: calc(#{$rad} * -2)
left: 0
background-color: RGB($bg-bright)
border-radius: $rad 0 0 0
box-shadow: 0 calc(#{$rad} * -1) 0 0 RGB($bg-100)
.banner
height: 30rem
background-color: RGB($bg-300)
img
position: absolute
inset: 0
width: 100%
height: 100%
background-color: inherit
object-fit: cover
object-position: center center
.banner-filter
position: absolute
inset: 0
width: 100%
height: 100%
background: linear-gradient(to right, RGB($primary), transparent)
z-index: +1
.banner-content
padding: 0.5rem
width: 100%
height: auto
position: absolute
left: 0
bottom: 0
display: grid
grid-template-columns: 1fr auto
grid-template-rows: 1fr auto auto
grid-template-areas: 'info info' 'header header' 'subtitle options'
gap: 0.5rem
z-index: +2
.banner-header,
.banner-info,
.banner-subtitle
margin: 0
padding: 0
width: 100%
.banner-header
grid-area: header
white-space: nowrap
text-overflow: ellipsis
overflow: hidden
text-align: left
font-size: 6.9rem
font-weight: 800
color: RGB($primary)
.banner-info
grid-area: info
font-size: 1rem
font-weight: 600
.banner-subtitle
grid-area: subtitle
font-size: 1rem
font-weight: 600
.pill-row
margin-top: auto
grid-area: options
.banner-small
height: 3.5rem
background-color: RGB($bg-100)
.banner-content
padding: 0.5rem
width: 100%
height: 100%
position: absolute
inset: 0
display: flex
flex-direction: row
justify-content: flex-start
gap: 1rem
z-index: +2
.banner-header,
.banner-info
margin: auto 0
padding: 0
width: auto
height: auto
justify-self: flex-start
.banner-header
padding-bottom: 0.25rem
white-space: nowrap
text-overflow: ellipsis
overflow: hidden
text-align: left
font-weight: 800
font-size: 1.5rem
color: RGB($primary)
.banner-info
font-size: 0.9rem
font-weight: 600
.pill-row
margin-left: auto
width: auto
@media (max-width: $breakpoint)
.banner,
.banner-small
&::after
display: none
.banner
min-height: 17rem
height: auto
.banner-content
padding: 0.5rem
height: 100%
display: flex
flex-direction: column
justify-content: center
align-items: center
gap: 0.25rem
.banner-header
font-size: 3rem
text-align: center
.banner-info,
.banner-subtitle
font-size: 1.1rem
text-align: center
.pill-row
margin-top: 1rem
.banner-small
.banner-content
.banner-info
display: none

View file

@ -57,6 +57,21 @@
&.black
@include btn-block($black)
.input-checkbox
padding: 0
display: flex
justify-content: flex-start
align-items: center
gap: 0.5rem
position: relative
label
font-size: 1rem
font-weight: 600
text-align: left
color: RGB($fg-white)
.input-block
padding: 0.5rem 1rem
@ -122,6 +137,7 @@
border-radius: $rad-inner
cursor: pointer
overflow: hidden
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
input
@ -130,6 +146,13 @@
opacity: 0
cursor: pointer
.status
width: 100%
white-space: nowrap
text-overflow: ellipsis
text-align: center
overflow: hidden
&:hover
background-color: RGBA($white, 0.2)
color: RGB($white)

View file

@ -9,6 +9,7 @@
height: 100vh
background-color: transparent
color: RGB($fg-white)
overflow: hidden
z-index: 68
@ -20,9 +21,6 @@
font-size: 1.5rem
font-weight: 700
color: RGB($primary)
p
margin: 0
padding: 0
@ -30,8 +28,6 @@
font-size: 1rem
font-weight: 500
color: RGB($fg-white)
form
margin: 0
padding: 0

View file

@ -16,7 +16,7 @@
position: relative
border-radius: $rad
border-radius: $rad-inner
box-sizing: border-box
overflow: hidden
@ -26,7 +26,8 @@
padding: 0.5rem
width: 100%
height: 30%
min-height: 30%
height: auto
position: absolute
left: 0
@ -104,7 +105,7 @@
position: relative
border-radius: $rad
border-radius: $rad-inner
box-sizing: border-box
overflow: hidden
@ -114,7 +115,8 @@
padding: 0.5rem
width: 100%
height: 30%
min-height: 30%
height: auto
position: absolute
left: 0
@ -205,7 +207,7 @@
transform: scale(0.6) rotate(-5deg) translate(25%, 10%)
z-index: +2
.data-3
transform: scale(0.6) rotate(-1deg) translate(-15%, -25%)
transform: scale(0.6) rotate(-1deg) translate(-15%, -23%)
z-index: +1
&:after

View file

@ -61,12 +61,12 @@
width: 2.5rem
height: 2.5rem
color: RGB($fg-white)
border-radius: $rad-inner
color: RGB($fg-white)
transition: color 0.2s ease-out, transform 0.2s ease-out
span
.tool-tip
margin: 0
padding: 0.35rem 0.7rem
@ -89,7 +89,7 @@
pointer-events: none
svg
> svg
margin: 0
font-size: 1rem
@ -107,7 +107,7 @@
&:hover
> svg
background: RGB($bg-300)
background: RGBA($fg-white, 0.1)
span
opacity: 1
@ -129,7 +129,7 @@
height: calc(100% - 6px)
background-color: RGB($primary)
border-radius: 0 $rad-inner $rad-inner 0
border-radius: $rad-inner
@media (max-width: $breakpoint)
.navigation
@ -158,7 +158,7 @@
height: 3rem
min-height: 3rem
span
.tool-tip
display: none
&.selected::before
@ -167,6 +167,4 @@
left: 0
width: 100%
height: 3px
border-radius: $rad-inner
height: 3px

View file

@ -8,7 +8,6 @@
@import "components/elements/pop-up"
@import "components/elements/upload-panel"
@import "components/elements/tags"
@import "components/elements/labels"
@import "components/navigation"
@import "components/banner"

View file

@ -74,24 +74,11 @@ $breakpoint: 800px
--breakpoint: 800px
@font-face
font-family: 'Work Sans'
src: url('fonts/worksans-regular.woff2')
font-weight: 400
@font-face
font-family: 'Work Sans'
src: url('fonts/worksans-bold.woff2')
font-weight: 600
@font-face
font-family: 'Work Sans'
src: url('fonts/worksans-black.woff2')
font-weight: 900
// I have no clue if its webassets or libsass thats doing this shit
// But one of them is trying to "correct" the path, and 404-ing the
// font, so enjoy this path fuckery
@font-face
font-family: 'Manrope'
src: url('fonts/Manrope[wght].woff2') format('woff2')
src: url('../../../../static/fonts/Manrope[wght].woff2') format('woff2')
font-style: normal
font-display: swap

View file

@ -1,8 +1,182 @@
{% extends 'layout.html' %}
{% block nav_groups %}selected{% endblock %}
{% block head %}
{% if images %}
<meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/>
{% endif %}
<script type="text/javascript">
function groupShare() {
try {
navigator.clipboard.writeText(window.location.href)
addNotification("Copied link!", 4);
} catch (err) {
addNotification("Failed to copy link! Are you on HTTP?", 2);
}
}
{% if current_user.id == group.author_id %}
function groupDelete() {
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.innerHTML = 'AAAAAAAAAA';
cancelBtn.onclick = popupDissmiss;
deleteBtn = document.createElement('button');
deleteBtn.classList.add('btn-block');
deleteBtn.classList.add('critical');
deleteBtn.innerHTML = 'No ragrats!';
deleteBtn.onclick = deleteConfirm;
popUpShow('Yeet!',
'Are you surrrre? This action is irreversible and very final.' +
' This wont delete the images, but it will remove them from this group.',
null,
[cancelBtn, deleteBtn]);
}
function deleteConfirm(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let formID = {{ group.id }};
if (!formID) {
addNotification("Dont tamper with the JavaScript pls!", 3);
return;
}
// Make form
const formData = new FormData();
formData.append("group", formID);
fetch('{{ url_for('api.delete_group') }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
// Redirect to groups page
window.location.href = '{{ url_for('group.groups') }}';
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Bad information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
}
}
}).catch(error => {
addNotification('Error yeeting group!', 2);
});
}
function groupEdit() {
// Create elements
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.innerHTML = 'go baaaaack';
cancelBtn.onclick = popupDissmiss;
submitBtn = document.createElement('button');
submitBtn.classList.add('btn-block');
submitBtn.classList.add('primary');
submitBtn.innerHTML = 'Saveeee';
submitBtn.type = 'submit';
submitBtn.setAttribute('form', 'editForm');
// Create form
editForm = document.createElement('form');
editForm.id = 'editForm';
editForm.setAttribute('onsubmit', 'return edit(event);');
groupInput = document.createElement('input');
groupInput.classList.add('input-block');
groupInput.type = 'text';
groupInput.placeholder = 'Group ID';
groupInput.value = {{ group.id }};
groupInput.id = 'group';
imageInput = document.createElement('input');
imageInput.classList.add('input-block');
imageInput.type = 'text';
imageInput.placeholder = 'Image ID';
imageInput.id = 'image';
actionInput = document.createElement('input');
actionInput.classList.add('input-block');
actionInput.type = 'text';
actionInput.placeholder = 'add/remove';
actionInput.value = 'add';
actionInput.id = 'action';
editForm.appendChild(groupInput);
editForm.appendChild(imageInput);
editForm.appendChild(actionInput);
popUpShow(
'Nothing stays the same',
'Add, remove, or change, the power is in your hands...',
editForm,
[cancelBtn, submitBtn]
);
}
function edit(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let formGroup = document.querySelector("#group").value;
let formImage = document.querySelector("#image").value;
let formAction = document.querySelector("#action").value;
if (!formGroup || !formImage || !formAction) {
addNotification("All values must be set!", 3);
return;
}
// Make form
const formData = new FormData();
formData.append("group", formGroup);
formData.append("image", formImage);
formData.append("action", formAction);
fetch('{{ url_for('api.modify_group') }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
addNotification('Group edited!!!', 1);
popupDissmiss();
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Bad information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
}
}
}).catch(error => {
addNotification('Error!!!!! Panic!!!!', 2);
});
}
{% endif %}
</script>
<style>
{% if images %}
.banner::after {
box-shadow: 0 calc(var(--rad) * -1) 0 0 rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }});
}
.banner-content p {
color: {{ text_colour }} !important;
}
@ -11,57 +185,97 @@
}
.banner-filter {
background: linear-gradient(90deg, rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}), rgba({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}, 0.3)) !important;
background: linear-gradient(90deg, rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}), rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.3)) !important;
}
@media (max-width: 800px) {
.banner-filter {
background: linear-gradient(180deg, rgba({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}, 0.8), rgba({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }}, 0.5)) !important;
background: linear-gradient(180deg, rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.8), rgba({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}, 0.5)) !important;
}
}
.navigation {
background-color: rgb({{ images.0.colours.0.0 }}, {{ images.0.colours.0.1 }}, {{ images.0.colours.0.2 }}) !important;
}
.navigation-item > svg {
fill: {{ text_colour }} !important;
color: {{ text_colour }} !important;
}
.navigation-item.selected::before {
background-color: {{ text_colour }} !important;
}
{% endif %}
</style>
{% endblock %}
{% block content %}
<div class="banner {% if not images %}small{% endif %}">
{% if not images %}
<div class="banner-content">
<h1>{{ group.name }}</h1>
<p>By {{ group.author_username }}</p>
</div>
{% else %}
<img src="{{ url_for('api.file', file_name=images.0.file_name ) }}?r=prev" onload="imgFade(this)" style="opacity:0;"/>
{% if images %}
<div class="banner">
<img src="{{ url_for('api.file', file_name=images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;"/>
<span class="banner-filter"></span>
<div class="banner-content">
<p>By {{ group.author_username }} - {{ images|length }} Images</p>
<h1>{{ group.name }}</h1>
<p>{{ group.description }}</p>
<p class="banner-info">By {{ group.author_username }} - {{ images|length }} Images</p>
<h1 class="banner-header">{{ group.name }}</h1>
<p class="banner-subtitle">{{ group.description }}</p>
<div class="pill-row">
<div>
<button class="pill-item" onclick="groupShare()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button>
</div>
{% if current_user.id == group.author_id %}
<div>
<button class="pill-item pill__critical" onclick="groupDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
</button>
<button class="pill-item pill__critical" onclick="groupEdit()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg>
</button>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<form id="modifyGroup">
<input type="text" name="group" placeholder="group id" value="{{ group.id }}">
<input type="text" name="image" placeholder="image id">
<input type="text" name="action" placeholder="add/remove" value="add">
<button type="submit">Submit</button>
</form>
</div>
{% else %}
<div class="banner-small">
<div class="banner-content">
<h1 class="banner-header">{{ group.name }}</h1>
<p class="banner-info">By {{ group.author_username }}</p>
<div class="pill-row">
<div>
<button class="pill-item" onclick="groupShare()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
</button>
</div>
{% if current_user.id == group.author_id %}
<div>
<button class="pill-item pill__critical" onclick="groupDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
</button>
<button class="pill-item pill__critical" onclick="groupEdit()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg>
</button>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if images %}
<div class="gallery-grid">
{% for image in images %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('group.group_post', group_id=group.id, image_id=image.id) }}" style="background-color: rgb({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }})">
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('group.group_post', group_id=group.id, image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter">
<p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="this.classList.add('loaded');" id="lazy-load"/>
<img alt="{{ image.alt }}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a>
{% endfor %}
</div>
{% else %}
<div class="big-text">
<h1>*crickets chirping*</h1>
{% if g.user %}
{% if current_user.is_authenticated %}
<p>Add some images to the group!</p>
{% else %}
<p>Login to start managing this image group!</p>
@ -69,32 +283,3 @@
</div>
{% endif %}
{% endblock %}
{% block script %}
<script>
// /api/group/modify
modForm = document.querySelector('#modifyGroup');
modForm.addEventListener('submit', function (event) {
event.preventDefault();
const formData = new FormData();
formData.append('group', modForm.group.value);
formData.append('image', modForm.image.value);
formData.append('action', modForm.action.value);
$.ajax({
url: '/api/group/modify',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (data) {
addNotification('Image added to group', 1);
}
})
})
</script>
{% endblock %}

View file

@ -1,25 +1,121 @@
{% extends 'layout.html' %}
{% block nav_groups %}selected{% endblock %}
{% block head %}
{% if images %}
<meta name="theme-color" content="rgb({{ images.0.colours.0.0 }}{{ images.0.colours.0.1 }}{{ images.0.colours.0.2 }})"/>
{% endif %}
{% if current_user.is_authenticated %}
<script type="text/javascript">
function showCreate() {
// Create elements
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.innerHTML = 'nuuuuuuuu';
cancelBtn.onclick = popupDissmiss;
submitBtn = document.createElement('button');
submitBtn.classList.add('btn-block');
submitBtn.classList.add('primary');
submitBtn.innerHTML = 'Submit!!';
submitBtn.type = 'submit';
submitBtn.setAttribute('form', 'createForm');
// Create form
createForm = document.createElement('form');
createForm.id = 'createForm';
createForm.setAttribute('onsubmit', 'return create(event);');
titleInput = document.createElement('input');
titleInput.classList.add('input-block');
titleInput.type = 'text';
titleInput.placeholder = 'Group namey';
titleInput.id = 'name';
descriptionInput = document.createElement('input');
descriptionInput.classList.add('input-block');
descriptionInput.type = 'text';
descriptionInput.placeholder = 'What it about????';
descriptionInput.id = 'description';
createForm.appendChild(titleInput);
createForm.appendChild(descriptionInput);
popUpShow(
'New stuff!',
'Image groups are a simple way to "group" images together, are you ready?',
createForm,
[cancelBtn, submitBtn]
);
}
function create(event) {
// AJAX takes control of subby form :3
event.preventDefault();
let formName = document.querySelector("#name").value;
let formDescription = document.querySelector("#description").value;
if (!formName) {
addNotification("Group name must be set!", 3);
return;
}
// Make form
const formData = new FormData();
formData.append("name", formName);
formData.append("description", formDescription);
fetch('{{ url_for('api.create_group') }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.status === 200) {
addNotification('Group created!', 1);
popupDissmiss();
} else {
switch (response.status) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Bad information', 2);
break;
default:
addNotification('Error logging in, blame someone', 2);
break;
}
}
}).catch(error => {
addNotification('Error making group! :c', 2);
});
}
</script>
{% endif %}
{% endblock %}
{% block content %}
<div class="banner small">
<div class="banner-small">
<div class="banner-content">
<h1>{{ config.WEBSITE.name }}</h1>
<h1 class="banner-header">{{ config.WEBSITE.name }}</h1>
{% if groups|length == 0 %}
<p>0 groups :<</p>
<p class="banner-info">No groups!!!!</p>
{% elif groups|length == 69 %}
<p>{{ groups|length }} groups, uwu</p>
<p class="banner-info">{{ groups|length }} groups, uwu</p>
{% else %}
<p>{{ groups|length }} groups</p>
<p class="banner-info">{{ groups|length }} groups</p>
{% endif %}
{% if current_user.is_authenticated %}
<div class="pill-row">
<div>
<button class="pill-item" onclick="showCreate()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path></svg>
</button>
</div>
</div>
{% endif %}
</div>
</div>
<form action="/api/group/create" method="post" enctype="multipart/form-data">
<input class="input-block black" type="text" name="name" placeholder="name">
<input class="input-block black" type="text" name="description" placeholder="description">
<button class="btn-block black" type="submit">Submit</button>
</form>
{% if groups %}
<div class="gallery-grid">
{% for group in groups %}
@ -31,7 +127,7 @@
<div class="images size-{{ group.images|length }}">
{% if group.images|length > 0 %}
{% for image in group.images %}
<img data-src="{{ image.file_name }}" onload="this.classList.add('loaded');" id="lazy-load" class="data-{{ loop.index }}"/>
<img data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load" class="data-{{ loop.index }}"/>
{% endfor %}
{% else %}
<img src="{{ url_for('static', filename='error.png') }}" class="loaded"/>
@ -43,7 +139,7 @@
{% else %}
<div class="big-text">
<h1>*crickets chirping*</h1>
{% if g.user %}
{% if current_user.is_authenticated %}
<p>You can get started by creating a new image group!</p>
{% else %}
<p>Login to start seeing anything here!</p>

View file

@ -1,8 +1,8 @@
{% extends 'layout.html' %}
{% block wrapper_class %}image-wrapper{% endblock %}
{% block head %}
<meta property="og:image" content="{{ url_for('api.file', file_name=image.file_name) }}"/>
<meta name="theme-color" content="#{{ image.image_colours.0.0 }}{{ image.image_colours.0.1 }}{{ image.image_colours.0.2 }}"/>
<meta property="og:image" content="{{ url_for('api.file', file_name=image.filename) }}"/>
<meta name="theme-color" content="rgb({{ image.colours.0.0 }}{{ image.colours.0.1 }}{{ image.colours.0.2 }})"/>
<script type="text/javascript">
function imageFullscreenOff() {
@ -18,7 +18,7 @@
function imageFullscreenOn() {
let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.querySelector('img').src = '{{ url_for('api.file', file_name=image.file_name) }}';
fullscreen.querySelector('img').src = '{{ url_for('api.file', file_name=image.filename) }}';
document.querySelector("html").style.overflow = "hidden";
fullscreen.style.display = 'flex';
@ -33,39 +33,40 @@
}
}
{% if g.user['id'] == image['author_id'] %}
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.innerHTML = 'nuuuuuuuu';
cancelBtn.onclick = popupDissmiss;
deleteBtn = document.createElement('button');
deleteBtn.classList.add('btn-block');
deleteBtn.classList.add('critical');
deleteBtn.innerHTML = 'Dewww eeeet!';
deleteBtn.onclick = deleteConfirm;
{% if current_user.id == image.author_id %}
function imageDelete() {
popUpShow(
'DESTRUCTION!!!!!!',
'Do you want to delete this image along with all of its data??? This action is irreversible!',
null,
[cancelBtn, deleteBtn]
);
cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block');
cancelBtn.innerHTML = 'nuuuuuuuu';
cancelBtn.onclick = popupDissmiss;
deleteBtn = document.createElement('button');
deleteBtn.classList.add('btn-block');
deleteBtn.classList.add('critical');
deleteBtn.innerHTML = 'Dewww eeeet!';
deleteBtn.onclick = deleteConfirm;
popUpShow('DESTRUCTION!!!!!!',
'Do you want to delete this image along with all of its data??? ' +
'This action is irreversible!',
null,
[cancelBtn, deleteBtn]);
}
function deleteConfirm() {
popupDissmiss();
$.ajax({
url: '{{ url_for('api.delete_image', image_id=image['id']) }}',
type: 'post',
data: {
fetch('{{ url_for('api.delete_image', image_id=image['id']) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'delete'
},
success: function (response) {
})
}).then(function(response) {
if (response.ok) {
window.location.href = '/';
},
error: function (response) {
} else {
addNotification(`Image *clings*: ${response}`, 2);
}
});
@ -79,30 +80,31 @@
<style>
.background span {
background-image: linear-gradient(to top, rgba({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }}, 1), transparent);
background-image: linear-gradient(to top, rgba({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }}, 1), transparent);
}
</style>
{% endblock %}
{% block content %}
<div class="background">
<img src="{{ url_for('api.file', file_name=image.file_name) }}?r=prev" alt="{{ image.post_alt }}" onload="imgFade(this)" style="opacity:0;"/>
<img src="{{ url_for('api.file', file_name=image.filename) }}?r=prev" alt="{{ image.alt }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
</div>
<div class="image-fullscreen" onclick="imageFullscreenOff()">
<img src="" alt="{{ image.post_alt }}" onload="imgFade(this);" style="opacity:0;" />
<img src="" alt="{{ image.alt }}"/>
</div>
<div class="image-grid">
<div class="image-container" id="image-container">
<img
src="{{ url_for('api.file', file_name=image.file_name) }}?r=prev"
alt="{{ image.post_alt }}"
onload="imgFade(this)" style="opacity:0;"
src="{{ url_for('api.file', file_name=image.filename) }}?r=prev"
alt="{{ image.alt }}"
onload="imgFade(this)"
style="opacity: 0;"
onerror="this.src='{{ url_for('static', filename='error.png')}}'"
{% if "File" in image.image_exif %}
width="{{ image.image_exif.File.Width.raw }}"
height="{{ image.image_exif.File.Height.raw }}"
{% if "File" in image.exif %}
width="{{ image.exif.File.Width.raw }}"
height="{{ image.exif.File.Height.raw }}"
{% endif %}
/>
</div>
@ -111,7 +113,7 @@
{% if next_url %}
<div>
<a class="pill-item" href="{{ next_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm40,112H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168a8,8,0,0,1,0,16Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
<span class="tool-tip">
Previous
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
@ -121,38 +123,38 @@
{% endif %}
<div>
<button class="pill-item" onclick="imageFullscreenOn()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M117.66,138.34a8,8,0,0,1,0,11.32L83.31,184l18.35,18.34A8,8,0,0,1,96,216H48a8,8,0,0,1-8-8V160a8,8,0,0,1,13.66-5.66L72,172.69l34.34-34.35A8,8,0,0,1,117.66,138.34ZM208,40H160a8,8,0,0,0-5.66,13.66L172.69,72l-34.35,34.34a8,8,0,0,0,11.32,11.32L184,83.31l18.34,18.35A8,8,0,0,0,216,96V48A8,8,0,0,0,208,40Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48V96a8,8,0,0,1-16,0V67.31l-50.34,50.35a8,8,0,0,1-11.32-11.32L188.69,56H160a8,8,0,0,1,0-16h48A8,8,0,0,1,216,48ZM106.34,138.34,56,188.69V160a8,8,0,0,0-16,0v48a8,8,0,0,0,8,8H96a8,8,0,0,0,0-16H67.31l50.35-50.34a8,8,0,0,0-11.32-11.32Z"></path></svg>
<span class="tool-tip">
Fullscreen
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</button>
<button class="pill-item" onclick="imageShare()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M212,200a36,36,0,1,1-69.85-12.25l-53-34.05a36,36,0,1,1,0-51.4l53-34a36.09,36.09,0,1,1,8.67,13.45l-53,34.05a36,36,0,0,1,0,24.5l53,34.05A36,36,0,0,1,212,200Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112A16,16,0,0,1,56,96H80a8,8,0,0,1,0,16H56v96H200V112H176a8,8,0,0,1,0-16h24A16,16,0,0,1,216,112ZM93.66,69.66,120,43.31V136a8,8,0,0,0,16,0V43.31l26.34,26.35a8,8,0,0,0,11.32-11.32l-40-40a8,8,0,0,0-11.32,0l-40,40A8,8,0,0,0,93.66,69.66Z"></path></svg>
<span class="tool-tip">
Share
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</button>
<a class="pill-item" href="/api/file/{{ image.file_name }}" download onclick="addNotification('Download started!', 4)">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,85.66A8,8,0,0,1,85.66,74.34L120,108.69V24a8,8,0,0,1,16,0v84.69l34.34-34.35a8,8,0,0,1,11.32,11.32l-48,48a8,8,0,0,1-11.32,0ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H84.4a4,4,0,0,1,2.83,1.17L111,145A24,24,0,0,0,145,145l23.8-23.8A4,4,0,0,1,171.6,120H224A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<a class="pill-item" href="/api/file/{{ image.filename }}" download onclick="addNotification('Download started!', 4)">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-42.34-61.66a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L120,164.69V120a8,8,0,0,1,16,0v44.69l10.34-10.35A8,8,0,0,1,157.66,154.34Z"></path></svg>
<span class="tool-tip">
Download
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</a>
</div>
{% if image.author_id == g.user.id %}
{% if current_user.id == image.author_id %}
<div>
<button class="pill-item pill__critical" onclick="imageDelete()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM112,168a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm0-120H96V40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
<span class="tool-tip">
Delete
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span>
</button>
<button class="pill-item pill__critical" onclick="imageEdit()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM51.31,160l90.35-90.35,16.68,16.69L68,176.68ZM48,179.31,76.69,208H48Zm48,25.38L79.31,188l90.35-90.35h0l16.68,16.69Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"></path></svg>
<span class="tool-tip">
Edit
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
@ -163,7 +165,7 @@
{% if prev_url %}
<div>
<a class="pill-item" href="{{ prev_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,109.66-32,32a8,8,0,0,1-11.32-11.32L148.69,136H88a8,8,0,0,1,0-16h60.69l-18.35-18.34a8,8,0,0,1,11.32-11.32l32,32A8,8,0,0,1,173.66,133.66Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>
<span class="tool-tip">
Next
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
@ -200,7 +202,7 @@
<table>
<tr>
<td>Author</td>
<td><a href="{{ url_for('gallery.profile_id', user_id=image.author_id) }}" class="link">{{ image.author_username }}</a></td>
<td><a href="{{ url_for('gallery.profile', id=image.author_id) }}" class="link">{{ image.author_username }}</a></td>
</tr>
<tr>
<td>Upload date</td>
@ -208,7 +210,7 @@
</tr>
</table>
<div class="img-colours">
{% for col in image.image_colours %}
{% for col in image.colours %}
<span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
{% endfor %}
</div>
@ -224,7 +226,7 @@
{% endif %}
</div>
</div>
{% for tag in image.image_exif %}
{% for tag in image.exif %}
<div class="info-tab">
<div class="info-header">
{% if tag == 'Photographer' %}
@ -249,17 +251,17 @@
</div>
<div class="info-table">
<table>
{% for subtag in image.image_exif[tag] %}
{% for subtag in image.exif[tag] %}
<tr>
<td>{{ subtag }}</td>
{% if image.image_exif[tag][subtag]['formatted'] %}
{% if image.image_exif[tag][subtag]['type'] == 'date' %}
<td><span class="time">{{ image.image_exif[tag][subtag]['formatted'] }}</span></td>
{% if image.exif[tag][subtag]['formatted'] %}
{% if image.exif[tag][subtag]['type'] == 'date' %}
<td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
{% else %}
<td>{{ image.image_exif[tag][subtag]['formatted'] }}</td>
<td>{{ image.exif[tag][subtag]['formatted'] }}</td>
{% endif %}
{% elif image.image_exif[tag][subtag]['raw'] %}
<td>{{ image.image_exif[tag][subtag]['raw'] }}</td>
{% elif image.exif[tag][subtag]['raw'] %}
<td>{{ image.exif[tag][subtag]['raw'] }}</td>
{% else %}
<td class="empty-table">Oops, an error</td>
{% endif %}

View file

@ -1,27 +1,27 @@
{% extends 'layout.html' %}
{% block nav_home %}selected{% endblock %}
{% block content %}
<div class="banner small">
<div class="banner-small">
<div class="banner-content">
<h1>{{ config.WEBSITE.name }}</h1>
<h1 class="banner-header">{{ config.WEBSITE.name }}</h1>
{% if images|length == 0 %}
<p>0 images :<</p>
<p class="banner-info">0 images D:</p>
{% elif images|length == 69 %}
<p>{{ images|length }} images, nice</p>
<p class="banner-info">{{ images|length }} images, nice</p>
{% else %}
<p>{{ images|length }} images</p>
<p class="banner-info">{{ images|length }} images</p>
{% endif %}
</div>
</div>
{% if images %}
<div class="gallery-grid">
{% for image in images %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('gallery.image', image_id=image.id) }}" style="background-color: rgb({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }})">
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('gallery.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter">
<p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<img alt="{{ image.post_alt }}" data-src="{{ image.file_name }}" onload="this.classList.add('loaded');" id="lazy-load"/>
<img fetchpriority="low" alt="{{ image.alt }}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a>
{% endfor %}
</div>

View file

@ -1,17 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ config.WEBSITE.name }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ config.WEBSITE.name }}</title>
<meta name="description" content="{{ config.WEBSITE.motto }}"/>
<meta name="author" content="{{ config.WEBSITE.author }}"/>
<meta property="og:title" content="{{ config.WEBSITE.name }}"/>
<meta property="og:description" content="{{ config.WEBSITE.motto }}"/>
<meta property="og:type" content="website"/>
<meta name="twitter:title" content="{{ config.WEBSITE.name }}"/>
<meta name="twitter:description" content="{{ config.WEBSITE.motto }}"/>
<meta name="twitter:card" content="summary_large_image">
@ -27,12 +25,18 @@
type="image/svg+xml"
media="(prefers-color-scheme: dark)"/>
<link rel="stylesheet" href="{{url_for('static', filename='theme/style.css')}}" defer>
{% assets "js_all" %}
{% assets "js_pre" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets "js_post" %}
<script type="text/javascript" src="{{ ASSET_URL }}" defer></script>
{% endassets %}
{% assets "styles" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css" defer>
{% endassets %}
{% block head %}{% endblock %}
</head>
<body>
@ -58,11 +62,11 @@
<div class="wrapper">
<div class="navigation">
<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">
<!--<img src="{{url_for('static', filename='icon.png')}}" alt="Logo" class="logo" onload="this.style.opacity=1;" style="opacity:0">-->
<a href="{{url_for('gallery.index')}}" class="navigation-item {% block nav_home %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M208,32H80A16,16,0,0,0,64,48V64H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V192h16a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48H208v69.38l-16.7-16.7a16,16,0,0,0-22.62,0L93.37,176H80Zm96,160H48V80H64v96a16,16,0,0,0,16,16h96ZM104,88a16,16,0,1,1,16,16A16,16,0,0,1,104,88Z"></path></svg>
<span>
<span class="tool-tip">
Home
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
@ -70,73 +74,73 @@
<a href="{{url_for('group.groups')}}" class="navigation-item {% block nav_groups %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64l27.73,20.8a16.12,16.12,0,0,0,9.6,3.2H200v16H146.43a16,16,0,0,0-8.88,2.69l-20,13.31H69.42a15.94,15.94,0,0,0-14.86,10.06L40,166.46V64Z"></path></svg>
<span>
<span class="tool-tip">
Groups
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a>
{% if g.user %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,77.66a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,0l48,48a8,8,0,0,1-11.32,11.32L136,43.31V128a8,8,0,0,1-16,0V43.31L85.66,77.66A8,8,0,0,1,74.34,77.66ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16h68a4,4,0,0,1,4,4v3.46c0,13.45,11,24.79,24.46,24.54A24,24,0,0,0,152,128v-4a4,4,0,0,1,4-4h68A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<span>
Upload
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</button>
{% if current_user.is_authenticated %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M74.34,77.66a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,0l48,48a8,8,0,0,1-11.32,11.32L136,43.31V128a8,8,0,0,1-16,0V43.31L85.66,77.66A8,8,0,0,1,74.34,77.66ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16h68a4,4,0,0,1,4,4v3.46c0,13.45,11,24.79,24.46,24.54A24,24,0,0,0,152,128v-4a4,4,0,0,1,4-4h68A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<span class="tool-tip">
Upload
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</button>
{% endif %}
<span class="navigation-spacer"></span>
{% if g.user %}
<a href="{{url_for('gallery.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg>
<span>
Profile
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a>
<a href="{{url_for('settings.general')}}" class="navigation-item {% block nav_settings %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"></path></svg>
<span>
Settings
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a>
{% if current_user.is_authenticated %}
<a href="{{url_for('gallery.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M231.73,221.94A8,8,0,0,1,224,232H160A8,8,0,0,1,152.27,222a40,40,0,0,1,17.11-23.33,32,32,0,1,1,45.24,0A40,40,0,0,1,231.73,221.94ZM216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16h80a8,8,0,0,0,0-16H40V64H93.33l27.74,20.8a16.12,16.12,0,0,0,9.6,3.2H216v32a8,8,0,0,0,16,0V88A16,16,0,0,0,216,72Z"></path></svg>
<span class="tool-tip">
Profile
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a>
<a href="{{url_for('settings.general')}}" class="navigation-item {% block nav_settings %}{% endblock %}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"></path></svg>
<span class="tool-tip">
Settings
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</a>
{% else %}
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M141.66,133.66l-40,40A8,8,0,0,1,88,168V136H24a8,8,0,0,1,0-16H88V88a8,8,0,0,1,13.66-5.66l40,40A8,8,0,0,1,141.66,133.66ZM192,32H136a8,8,0,0,0,0,16h56V208H136a8,8,0,0,0,0,16h56a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Z"></path></svg>
<span>
Login
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</button>
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M141.66,133.66l-40,40A8,8,0,0,1,88,168V136H24a8,8,0,0,1,0-16H88V88a8,8,0,0,1,13.66-5.66l40,40A8,8,0,0,1,141.66,133.66ZM192,32H136a8,8,0,0,0,0,16h56V208H136a8,8,0,0,0,0,16h56a16,16,0,0,0,16-16V48A16,16,0,0,0,192,32Z"></path></svg>
<span class="tool-tip">
Login
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M168,48V208a8,8,0,0,1-13.66,5.66l-80-80a8,8,0,0,1,0-11.32l80-80A8,8,0,0,1,168,48Z"></path></svg>
</span>
</button>
{% endif %}
</div>
{% if g.user %}
<div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span>
<div class="container">
<span id="dragIndicator"></span>
<h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p>
<form id="uploadForm">
<button class="fileDrop-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H80a8,8,0,0,1,0,16H32v64H224V136H176a8,8,0,0,1,0-16h48A16,16,0,0,1,240,136ZM85.66,77.66,120,43.31V128a8,8,0,0,0,16,0V43.31l34.34,34.35a8,8,0,0,0,11.32-11.32l-48-48a8,8,0,0,0-11.32,0l-48,48A8,8,0,0,0,85.66,77.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<span class="status">Choose or Drop file</span>
<input type="file" id="file" tab-index="-1"/>
</button>
{% if current_user.is_authenticated %}
<div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span>
<div class="container">
<span id="dragIndicator"></span>
<h3>Upload stuffs</h3>
<p>May the world see your stuff 👀</p>
<form id="uploadForm">
<button class="fileDrop-block" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H80a8,8,0,0,1,0,16H32v64H224V136H176a8,8,0,0,1,0-16h48A16,16,0,0,1,240,136ZM85.66,77.66,120,43.31V128a8,8,0,0,0,16,0V43.31l34.34,34.35a8,8,0,0,0,11.32-11.32l-48-48a8,8,0,0,0-11.32,0l-48,48A8,8,0,0,0,85.66,77.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<span class="status">Choose or Drop file</span>
<input type="file" id="file" tab-index="-1"/>
</button>
<input class="input-block" type="text" placeholder="alt" id="alt"/>
<input class="input-block" type="text" placeholder="description" id="description"/>
<input class="input-block" type="text" placeholder="tags" id="tags"/>
<button class="btn-block primary" type="submit">Upload</button>
</form>
<div class="upload-jobs"></div>
</div>
<input class="input-block" type="text" placeholder="alt" id="alt"/>
<input class="input-block" type="text" placeholder="description" id="description"/>
<input class="input-block" type="text" placeholder="tags" id="tags"/>
<button class="btn-block" type="submit">Upload</button>
</form>
<div class="upload-jobs"></div>
</div>
</div>
{% endif %}
<div class="content">

View file

@ -2,17 +2,29 @@
{% block nav_profile %}selected{% endblock %}
{% block content %}
<div class="banner">
<img src="{{ url_for('static', filename='') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
<div class="banner-small">
<div class="banner-content">
<h1>Profile</h1>
<p>Hello {{ g.user['username'] }}</p>
<h1 class="banner-header">{{ user.username }}</h1>
<p class="banner-info">Member since <span class="time">{{ user.joined_at }}</span></p>
</div>
</div>
<h1>User</h1>
<p>{{user_id}}</p>
{% if images %}
<div class="gallery-grid">
{% for image in images %}
<a id="image-{{ image.id }}" class="gallery-item" href="{{ url_for('gallery.image', image_id=image.id) }}" style="background-color: rgb({{ image.colours.0.0 }}, {{ image.colours.0.1 }}, {{ image.colours.0.2 }})">
<div class="image-filter">
<p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div>
<img fetchpriority="low" alt="{{ image.alt }}" data-src="{{ url_for('api.file', file_name=image.filename) }}?r=thumb" onload="this.classList.add('loaded');" id="lazy-load"/>
</a>
{% endfor %}
</div>
{% else %}
<div class="big-text">
<h1>*crickets chirping*</h1>
<p>There are no images here yet, upload some!</p>
</div>
{% endif %}
{% endblock %}

View file

@ -3,5 +3,10 @@
{% block settings_account %}settings-nav__item-selected{% endblock %}
{% block settings_content %}
<h2>Account</h2>
<a href="{{ url_for( 'auth.logout' ) }}">Logout</a>
<p>Is session fresh?</p>
{% if fresh %}
<p>Yes</p>
{% else %}
<p>No</p>
{% endif %}
{% endblock %}

View file

@ -2,14 +2,17 @@
{% block nav_settings %}selected{% endblock %}
{% block content %}
<div class="banner">
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
<div class="banner-small">
<div class="banner-content">
{% block banner_subtitle%}{% endblock %}
<h1>Settings</h1>
<p>All the red buttons in one place, what could go wrong?</p>
<h1 class="banner-header">Settings</h1>
<p class="banner-info">{% block banner_subtitle%}{% endblock %}</p>
<div class="pill-row">
<div>
<a class="pill-item pill__critical" href="{{ url_for( 'auth.logout' ) }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M112,216a8,8,0,0,1-8,8H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32h56a8,8,0,0,1,0,16H48V208h56A8,8,0,0,1,112,216Zm109.66-93.66-40-40a8,8,0,0,0-11.32,11.32L196.69,120H104a8,8,0,0,0,0,16h92.69l-26.35,26.34a8,8,0,0,0,11.32,11.32l40-40A8,8,0,0,0,221.66,122.34Z"></path></svg>
</a>
</div>
</div>
</div>
</div>

View file

@ -1,119 +0,0 @@
.banner
width: 100%
height: 50vh
position: relative
background-color: RGB($bg-300)
color: RGB($fg-white)
overflow: hidden
transition: opacity 0.3s ease-in-out
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
background-color: RGB($bg-300)
object-fit: cover
object-position: center center
.banner-filter
position: absolute
top: 0
left: 0
width: 100%
height: 100%
background: linear-gradient(to right, RGB($primary), transparent)
z-index: +1
.banner-content
padding: 1rem
width: 100%
height: inherit
position: relative
display: flex
flex-direction: column
justify-content: flex-end
z-index: +2
h1
margin: 0
padding: 0
font-size: 6.9rem
font-weight: 800
text-align: left
color: RGB($primary)
p
margin: 0
padding: 0
font-size: 1rem
font-weight: 600
line-height: 1
text-align: left
&.small
height: 3.5rem
background-color: RGB($bg-100)
.banner-content
padding: 0.5rem
flex-direction: row
justify-content: flex-start
align-items: center
gap: 1rem
h1
padding-bottom: 0.25rem
font-size: 1.5rem
text-align: left
p
font-size: 0.9rem
text-align: left
@media (max-width: $breakpoint)
.banner
width: 100vw
height: 25vh
.banner-content
padding: 0.5rem
display: flex
justify-content: center
align-items: center
h1
font-size: 3rem
text-align: center
p
font-size: 1.1rem
text-align: center
&.small .banner-content
justify-content: center
h1
text-align: center
p
display: none

View file

@ -1,33 +0,0 @@
.label
padding: 0.4rem 0.7rem
display: block
position: absolute
font-size: 0.9rem
font-weight: 600
background-color: RGB($bg-dim)
color: RGB($fg-white)
border-radius: $rad-inner
opacity: 0
transition: opacity 0.2s cubic-bezier(.76,0,.17,1), left 0.2s cubic-bezier(.76,0,.17,1)
pointer-events: none
z-index: 999
svg
margin: 0
font-size: 1rem
width: 0.75rem
height: 0.75rem
display: block
position: absolute
top: 50%
left: -0.45rem
transform: translateY(-50%)
color: RGB($bg-dim)

View file

@ -3,7 +3,7 @@ Calculate the contrast between two colors
"""
def contrast(background, light, dark, threshold = 0.179):
def contrast(background, light, dark, threshold=0.179):
"""
background: tuple of (r, g, b) values
light: color to use if the background is light

View file

@ -4,7 +4,7 @@ Tools for generating images and thumbnails
import os
import platformdirs
from PIL import Image, ImageOps #, ImageFilter
from PIL import Image, ImageOps
from werkzeug.utils import secure_filename
@ -37,11 +37,9 @@ def generate_thumbnail(file_name, resolution, ext=None):
if resolution in ['prev', 'preview']:
res_x, res_y = (1920, 1080)
elif resolution in ['thumb', 'thumbnail']:
res_x, res_y = (400, 400)
res_x, res_y = (350, 350)
elif resolution in ['icon', 'favicon']:
res_x, res_y = (10, 10)
elif len(resolution.split('x')) == 2:
res_x, res_y = resolution.split('x')
else:
return None
@ -65,13 +63,13 @@ def generate_thumbnail(file_name, resolution, ext=None):
# Save image to cache directory
try:
image.save(os.path.join(CACHE_PATH, f'{file_name}_{res_x}x{res_y}.{ext}'),
icc_profile=image_icc)
icc_profile=image_icc)
except OSError:
# This usually happens when saving a JPEG with an ICC profile,
# so we convert to RGB and try again
image = image.convert('RGB')
image.save(os.path.join(CACHE_PATH, f'{file_name}_{res_x}x{res_y}.{ext}'),
icc_profile=image_icc)
icc_profile=image_icc)
# No need to keep the image in memory, learned the hard way
image.close()

View file

@ -279,7 +279,7 @@ def lens_specification(value):
"""
try:
return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm'
except ValueError:
except TypeError:
return None

View file

@ -1,55 +0,0 @@
"""
OnlyLegs - Theme Manager
"""
import os
import sys
import shutil
from datetime import datetime
import sass
def compile_theme(theme_name, app_path):
"""
Compiles the theme into the static folder
"""
print(f"Loading '{theme_name}' theme...")
# Set Paths
theme_source = os.path.join(app_path, 'themes', theme_name)
theme_destination = os.path.join(app_path, 'static', 'theme')
# If the theme doesn't exist, exit
if not os.path.exists(theme_source):
print("Theme does not exist!")
sys.exit(1)
# If the destination folder doesn't exist, create it
if not os.path.exists(theme_destination):
os.makedirs(theme_destination)
# Theme source file doesn't exist, exit
if not os.path.join(theme_source, 'style.sass'):
print("No sass file found!")
sys.exit(1)
# Compile the theme
with open(os.path.join(theme_destination, 'style.css'),
encoding='utf-8', mode='w+') as file:
try:
file.write(sass.compile(filename=os.path.join(theme_source, 'style.sass'),
output_style='compressed'))
except sass.CompileError as err:
print("Failed to compile!\n", err)
sys.exit(1)
print("Compiled successfully!")
# If the destination folder exists, remove it
if os.path.exists(os.path.join(theme_destination, 'fonts')):
shutil.rmtree(os.path.join(theme_destination, 'fonts'))
# Copy the fonts
shutil.copytree(os.path.join(theme_source, 'fonts'),
os.path.join(theme_destination, 'fonts'))
print("Fonts copied successfully!")
print(f"{datetime.now().hour}:{datetime.now().minute}:{datetime.now().second} - Done!\n")

View file

@ -0,0 +1 @@
# :3

View file

@ -5,16 +5,16 @@ from uuid import uuid4
import os
import pathlib
import logging
from datetime import datetime as dt
import platformdirs
from flask import Blueprint, send_from_directory, abort, flash, jsonify, request, g, current_app
from flask import Blueprint, send_from_directory, abort, flash, request, current_app
from werkzeug.utils import secure_filename
from flask_login import login_required, current_user
from colorthief import ColorThief
from sqlalchemy.orm import sessionmaker
from gallery.auth import login_required
from gallery import db
from gallery.utils import metadata as mt
@ -37,7 +37,7 @@ def file(file_name):
file_name = secure_filename(file_name) # Sanitize file name
# if no args are passed, return the raw file
if not request.args:
if not res and not ext:
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name)):
abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name)
@ -64,7 +64,7 @@ def upload():
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(form_file.filename).suffix.replace('.', '').lower()
img_name = "GWAGWA_"+str(uuid4())
img_name = "GWAGWA_" + str(uuid4())
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img_name+'.'+img_ext)
# Check if file extension is allowed
@ -83,14 +83,13 @@ def upload():
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database
query = db.Posts(author_id=g.user.id,
created_at=dt.utcnow(),
file_name=img_name+'.'+img_ext,
file_type=img_ext,
image_exif=img_exif,
image_colours=img_colors,
post_description=form['description'],
post_alt=form['alt'])
query = db.Posts(author_id=current_user.id,
filename=img_name + '.' + img_ext,
mimetype=img_ext,
exif=img_exif,
colours=img_colors,
description=form['description'],
alt=form['alt'])
db_session.add(query)
db_session.commit()
@ -109,18 +108,18 @@ def delete_image(image_id):
# Check if image exists and if user is allowed to delete it (author)
if img is None:
abort(404)
if img.author_id != g.user.id:
if img.author_id != current_user.id:
abort(403)
# Delete file
try:
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'], img.filename))
except FileNotFoundError:
logging.warning('File not found: %s, already deleted or never existed', img.file_name)
logging.warning('File not found: %s, already deleted or never existed', img.filename)
# Delete cached files
cache_path = os.path.join(platformdirs.user_config_dir('onlylegs'), 'cache')
cache_name = img.file_name.rsplit('.')[0]
cache_name = img.filename.rsplit('.')[0]
for cache_file in pathlib.Path(cache_path).glob(cache_name + '*'):
os.remove(cache_file)
@ -135,7 +134,7 @@ def delete_image(image_id):
# Commit all changes
db_session.commit()
logging.info('Removed image (%s) %s', image_id, img.file_name)
logging.info('Removed image (%s) %s', image_id, img.filename)
flash(['Image was all in Le Head!', '1'])
return 'Gwa Gwa'
@ -148,8 +147,7 @@ def create_group():
"""
new_group = db.Groups(name=request.form['name'],
description=request.form['description'],
author_id=g.user.id,
created_at=dt.utcnow())
author_id=current_user.id)
db_session.add(new_group)
db_session.commit()
@ -165,21 +163,21 @@ def modify_group():
"""
group_id = request.form['group']
image_id = request.form['image']
action = request.form['action']
group = db_session.query(db.Groups).filter_by(id=group_id).first()
if group is None:
abort(404)
elif group.author_id != g.user.id:
elif group.author_id != current_user.id:
abort(403)
if request.form['action'] == 'add':
if action == 'add':
if not (db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id)
.first()):
db_session.add(db.GroupJunction(group_id=group_id,
post_id=image_id,
date_added=dt.utcnow()))
post_id=image_id))
elif request.form['action'] == 'remove':
(db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id)
@ -190,17 +188,23 @@ def modify_group():
return ':3'
@blueprint.route('/metadata/<int:img_id>', methods=['GET'])
def metadata(img_id):
@blueprint.route('/group/delete', methods=['POST'])
def delete_group():
"""
Yoinks metadata from an image
Deletes a group
"""
img = db_session.query(db.Posts).filter_by(id=img_id).first()
group_id = request.form['group']
if not img:
group = db_session.query(db.Groups).filter_by(id=group_id).first()
if group is None:
abort(404)
elif group.author_id != current_user.id:
abort(403)
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name)
exif = mt.Metadata(img_path).yoink()
db_session.query(db.Groups).filter_by(id=group_id).delete()
db_session.query(db.GroupJunction).filter_by(group_id=group_id).delete()
db_session.commit()
return jsonify(exif)
flash(['Group yeeted!', '1'])
return ':3'

View file

@ -37,8 +37,8 @@ def groups():
# For each image, get the image data and add it to the group item
group.images = []
for image in images:
group.images.append(db_session.query(db.Posts.file_name, db.Posts.post_alt,
db.Posts.image_colours, db.Posts.id)
group.images.append(db_session.query(db.Posts.filename, db.Posts.alt,
db.Posts.colours, db.Posts.id)
.filter(db.Posts.id == image[0])
.first())
@ -78,8 +78,8 @@ def group(group_id):
# Check contrast for the first image in the group for the banner
text_colour = 'rgb(var(--fg-black))'
if images[0]:
text_colour = contrast.contrast(images[0].image_colours[0],
if images:
text_colour = contrast.contrast(images[0].colours[0],
'rgb(var(--fg-black))',
'rgb(var(--fg-white))')

View file

@ -3,6 +3,7 @@ Onlylegs Gallery - Routing
"""
from flask import Blueprint, render_template, url_for, request
from werkzeug.exceptions import abort
from flask_login import current_user
from sqlalchemy.orm import sessionmaker
from gallery import db
@ -18,9 +19,9 @@ def index():
"""
Home page of the website, shows the feed of the latest images
"""
images = db_session.query(db.Posts.file_name,
db.Posts.post_alt,
db.Posts.image_colours,
images = db_session.query(db.Posts.filename,
db.Posts.alt,
db.Posts.colours,
db.Posts.created_at,
db.Posts.id).order_by(db.Posts.id.desc()).all()
@ -81,12 +82,21 @@ def profile():
"""
Profile overview, shows all profiles on the onlylegs gallery
"""
return render_template('profile.html', user_id='gwa gwa')
user_id = request.args.get('id', default=None, type=int)
# If there is no userID set, check if the user is logged in and display their profile
if not user_id:
if current_user.is_authenticated:
user_id = current_user.id
else:
abort(404, 'You must be logged in to view your own profile!')
@blueprint.route('/profile/<int:user_id>')
def profile_id(user_id):
"""
Shows user ofa given id, displays their uploads and other info
"""
return render_template('profile.html', user_id=user_id)
# Get the user's data
user = db_session.query(db.Users).filter(db.Users.id == user_id).first()
if not user:
abort(404, 'User not found :c')
images = db_session.query(db.Posts).filter(db.Posts.author_id == user_id).all()
return render_template('profile.html', user=user, images=images)

View file

@ -2,9 +2,7 @@
OnlyLegs - Settings page
"""
from flask import Blueprint, render_template
from gallery.auth import login_required
from flask_login import login_required
blueprint = Blueprint('settings', __name__, url_prefix='/settings')

33
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
[[package]]
name = "astroid"
@ -253,21 +253,20 @@ brotli = "*"
flask = "*"
[[package]]
name = "flask-session2"
version = "1.3.1"
description = "Adds server-side session support to your Flask application"
name = "flask-login"
version = "0.6.2"
description = "User authentication and session management for Flask."
category = "main"
optional = false
python-versions = ">=3.7.2,<4.0.0"
python-versions = ">=3.7"
files = [
{file = "Flask-Session2-1.3.1.tar.gz", hash = "sha256:171e986d4e314795f448a527095e42df6abfba76c3e4ce5c8e4c61c857c59cb2"},
{file = "Flask_Session2-1.3.1-py3-none-any.whl", hash = "sha256:6d1615dfc4b247759f31f89bf16aba96fa1294077e700771875abe952f291959"},
{file = "Flask-Login-0.6.2.tar.gz", hash = "sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3"},
{file = "Flask_Login-0.6.2-py3-none-any.whl", hash = "sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f"},
]
[package.dependencies]
cachelib = ">=0.9.0,<0.10.0"
Flask = ">=2.2.2,<3.0.0"
pytz = ">=2022.2.1,<2023.0.0"
Flask = ">=1.0.4"
Werkzeug = ">=1.0.1"
[[package]]
name = "greenlet"
@ -706,18 +705,6 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pytz"
version = "2022.7.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
{file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
]
[[package]]
name = "pyyaml"
version = "6.0"
@ -1033,4 +1020,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "fdc83d433d98ba079b4614e37da26b9c81f07e0a646606c17de5c0fb07ae483e"
content-hash = "431b58579dc3ebde52c8f3905c851556a465d5a82796a8a26718d69cb4915959"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "onlylegs"
version = "23.03.30"
version = "23.04.05"
description = "Gallery built for fast and simple image management"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT"
@ -12,7 +12,7 @@ Flask = "^2.2.2"
Flask-Compress = "^1.13"
Flask-Caching = "^2.0.2"
Flask-Assets = "^2.0"
Flask-Session2 = "^1.3.1"
Flask-Login = "^0.6.2"
SQLAlchemy = "^2.0.3"
python-dotenv = "^0.21.0"
gunicorn = "^20.1.0"

13
run.py
View file

@ -14,7 +14,7 @@ print("""
#+# #+# #+# #+#+# #+# #+# #+# #+# #+# #+# #+# #+#
######## ### #### ########## ### ########## ######### ######### ########
Created by Fluffy Bean - Version 23.03.30
Created by Fluffy Bean - Version 23.04.05
""")
@ -24,18 +24,13 @@ Configuration()
if DEBUG:
from gallery import create_app
# If no address is specified, use localhost
if not ADDRESS:
ADDRESS = 'localhost'
create_app().run(host=ADDRESS, port=PORT, debug=True, threaded=True)
else:
from setup.runner import OnlyLegs # pylint: disable=C0412
import sys
# If no address is specified, bind the server to all interfaces
if not ADDRESS:
ADDRESS = '0.0.0.0'
# Stop Gunicorn from reading the command line arguments as it causes errors
sys.argv = [sys.argv[0]]
options = {
'bind': f'{ADDRESS}:{PORT}',

View file

@ -2,7 +2,7 @@
Startup arguments for the OnlyLegs gallery
-p, --port: Port to run on (default: 5000)
-a, --address: Address to run on (default: For Debug: localhost, For Production: 0.0.0.0)
-a, --address: Address to run on (default: 127.0.0.0)
-w, --workers: Number of workers to run (default: 4)
-d, --debug: Run as Flask app in debug mode (default: False)
@ -15,7 +15,7 @@ import argparse
parser = argparse.ArgumentParser(description='Run the OnlyLegs gallery')
parser.add_argument('-p', '--port', type=int, default=5000, help='Port to run on')
parser.add_argument('-a', '--address', type=str, default=None, help='Address to run on')
parser.add_argument('-a', '--address', type=str, default='127.0.0.0', help='Address to run on')
parser.add_argument('-w', '--workers', type=int, default=4, help='Number of workers to run')
parser.add_argument('-d', '--debug', action='store_true', help='Run as Flask app in debug mode')
args = parser.parse_args()

View file

@ -135,15 +135,9 @@ class Configuration:
"""
Set the logging config
"""
logs_path = os.path.join(platformdirs.user_config_dir('onlylegs'), 'logs')
if not os.path.isdir(logs_path):
os.mkdir(logs_path)
print("Created logs directory at:", logs_path)
logging.getLogger('werkzeug').disabled = True
logging.basicConfig(
filename=os.path.join(logs_path, 'only.log'),
filename=os.path.join(USER_DIR, 'only.log'),
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S',
format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',

View file

@ -9,7 +9,7 @@ class OnlyLegs(Application):
"""
Gunicorn application
"""
def __init__(self, options={}): # pylint: disable=W0102, W0231 # noqa
def __init__(self, options={}): # pylint: disable=W0102, W0231
self.usage = None
self.callable = None
self.options = options
@ -25,7 +25,8 @@ class OnlyLegs(Application):
cfg[setting.lower()] = value
return cfg
def prog(self): # pylint: disable=C0116, E0202 # noqa
@staticmethod
def prog(): # pylint: disable=C0116, E0202
return 'OnlyLegs'
def load(self):