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! And many more planned things!
## screenshots <h2>Screenshots</h2>
<details><summary>Home Screen</summary>
Home-screen
![screenshot](.github/images/homepage.png) ![screenshot](.github/images/homepage.png)
Image view </details>
![screenshot](.github/images/imageview.png)
<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 ## Running

View file

@ -11,6 +11,7 @@ import logging
from flask_compress import Compress from flask_compress import Compress
from flask_caching import Cache from flask_caching import Cache
from flask_assets import Environment, Bundle from flask_assets import Environment, Bundle
from flask_login import LoginManager
from flask import Flask, render_template, abort from flask import Flask, render_template, abort
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@ -19,35 +20,41 @@ import platformdirs
from dotenv import load_dotenv from dotenv import load_dotenv
from yaml import safe_load from yaml import safe_load
# Utils # Import database
from gallery.utils import theme_manager from sqlalchemy.orm import sessionmaker
from gallery import db
USER_DIR = platformdirs.user_config_dir('onlylegs') 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): def create_app(test_config=None):
""" """
Create and configure the main app Create and configure the main app
""" """
app = Flask(__name__, instance_path=os.path.join(USER_DIR, 'instance')) 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 # Get environment variables
load_dotenv(os.path.join(USER_DIR, '.env')) load_dotenv(os.path.join(USER_DIR, '.env'))
print("Loaded environment variables") print("Loaded environment variables")
# Get config file # 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) conf = safe_load(file)
print("Loaded gallery config") print("Loaded config")
# App configuration # App configuration
app.config.from_mapping( app.config.from_mapping(
SECRET_KEY=os.environ.get('FLASK_SECRET'), 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'), UPLOAD_FOLDER=os.path.join(USER_DIR, 'uploads'),
ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'], ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'],
MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'], MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'],
@ -59,28 +66,54 @@ def create_app(test_config=None):
else: else:
app.config.from_mapping(test_config) app.config.from_mapping(test_config)
# Load theme login_manager.init_app(app)
theme_manager.compile_theme('default', app.root_path) login_manager.login_view = 'gallery.index'
login_manager.session_protection = 'strong'
# Bundle JS files @login_manager.user_loader
js_scripts = Bundle('js/*.js', output='gen/packed.js') def load_user(user_id):
assets.register('js_all', js_scripts) 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 # Error handlers, if the error is not a HTTP error, return 500
@app.errorhandler(Exception) @app.errorhandler(Exception)
def error_page(err): # noqa def error_page(err): # noqa
if not isinstance(err, HTTPException): if not isinstance(err, HTTPException):
abort(500) abort(500)
return render_template('error.html', return render_template('error.html', error=err.code, msg=err.description), err.code
error=err.code,
msg=err.description), err.code
# Load login, registration and logout manager # Load login, registration and logout manager
from gallery import auth from gallery import auth
app.register_blueprint(auth.blueprint) app.register_blueprint(auth.blueprint)
# Load the different routes # 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(api.blueprint)
app.register_blueprint(groups.blueprint) app.register_blueprint(groups.blueprint)
app.register_blueprint(routing.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 User registration, login and logout and locking access to pages behind a login
""" """
import re import re
import uuid
import logging import logging
from datetime import datetime as dt
import functools from flask import Blueprint, flash, redirect, request, url_for, abort, jsonify
from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
from sqlalchemy.orm import sessionmaker from flask_login import login_user, logout_user, login_required
from sqlalchemy import exc
from sqlalchemy.orm import sessionmaker
from gallery import db from gallery import db
@ -22,42 +19,31 @@ db_session = sessionmaker(bind=db.engine)
db_session = db_session() 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) error = []
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'))
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 if error:
def load_logged_in_user(): abort(403)
"""
Runs before every request and checks if a user is logged in
"""
user_id = session.get('user_id')
user_uuid = session.get('uuid')
if user_id is None or user_uuid is None: login_user(user, remember=remember)
g.user = None
session.clear()
else:
is_alive = db_session.query(db.Sessions).filter_by(session_uuid=user_uuid).first()
if is_alive is None: logging.info('User %s logged in from %s', username, request.remote_addr)
logging.info('Session expired') flash(['Logged in successfully!', '4'])
flash(['Session expired!', '3']) return 'ok', 200
session.clear()
else:
g.user = db_session.query(db.Users).filter_by(id=user_id).first()
@blueprint.route('/register', methods=['POST']) @blueprint.route('/register', methods=['POST'])
@ -65,17 +51,18 @@ def register():
""" """
Register a new user Register a new user
""" """
error = []
# Thanks Fennec for reminding me to strip out the whitespace lol # Thanks Fennec for reminding me to strip out the whitespace lol
username = request.form['username'].strip() username = request.form['username'].strip()
email = request.form['email'].strip() email = request.form['email'].strip()
password = request.form['password'].strip() password = request.form['password'].strip()
password_repeat = request.form['password-repeat'].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') 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') username_regex = re.compile(r'\b[A-Za-z0-9._-]+\b')
# Validate the form
if not username or not username_regex.match(username): if not username or not username_regex.match(username):
error.append('Username is invalid!') error.append('Username is invalid!')
@ -92,77 +79,30 @@ def register():
elif password_repeat != password: elif password_repeat != password:
error.append('Passwords do not match!') error.append('Passwords do not match!')
if error: user_exists = db_session.query(db.Users).filter_by(username=username).first()
return jsonify(error) if user_exists:
error.append('User already exists!')
try: # If there are errors, return them
register_user = db.Users(username=username, if error:
email=email, print(error)
password=generate_password_hash(password), return jsonify(error), 400
created_at=dt.utcnow())
register_user = db.Users(username=username, email=email,
password=generate_password_hash(password, method='sha256'))
db_session.add(register_user) db_session.add(register_user)
db_session.commit() 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!'
logging.info('User %s registered', username) logging.info('User %s registered', username)
return 'gwa gwa' return 'ok', 200
@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'
@blueprint.route('/logout') @blueprint.route('/logout')
@login_required
def logout(): def logout():
""" """
Clear the current session, including the stored user id Clear the current session, including the stored user id
""" """
logging.info('User (%s) %s logged out', session.get('user_id'), g.user.username) logout_user()
session.clear() flash(['Goodbye!!!', '4'])
return redirect(url_for('gallery.index')) 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 os
import platformdirs import platformdirs
from sqlalchemy import ( from sqlalchemy import (create_engine, Column, Integer, String,
create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey, PickleType) DateTime, ForeignKey, PickleType, func)
from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm import declarative_base, relationship
from flask_login import UserMixin
USER_DIR = platformdirs.user_config_dir('onlylegs') 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 # 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() 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 User table
Joins with post, groups, session and log Joins with post, groups, session and log
""" """
__tablename__ = 'users' __tablename__ = 'users'
# Gallery used information
id = Column(Integer, primary_key=True) 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) username = Column(String, unique=True, nullable=False)
email = Column(String, unique=True, nullable=False) email = Column(String, unique=True, nullable=False)
password = Column(String, 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') posts = relationship('Posts', backref='users')
groups = relationship('Groups', backref='users') groups = relationship('Groups', backref='users')
session = relationship('Sessions', backref='users')
log = relationship('Logs', 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 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) id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey('users.id')) 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
filename = Column(String, unique=True, nullable=False)
file_name = Column(String, unique=True, nullable=False) mimetype = Column(String, nullable=False)
file_type = Column(String, nullable=False) exif = Column(PickleType, nullable=False)
colours = Column(PickleType, nullable=False)
image_exif = Column(PickleType, nullable=False) description = Column(String, nullable=False)
image_colours = Column(PickleType, nullable=False) alt = Column(String, nullable=False)
post_description = Column(String, nullable=False)
post_alt = Column(String, nullable=False)
junction = relationship('GroupJunction', backref='posts') junction = relationship('GroupJunction', backref='posts')
class Groups (base): # pylint: disable=too-few-public-methods, C0103 class Groups (base): # pylint: disable=too-few-public-methods, C0103
""" """
Group table Group table
@ -71,7 +74,7 @@ class Groups (base): # pylint: disable=too-few-public-methods, C0103
name = Column(String, nullable=False) name = Column(String, nullable=False)
description = Column(String, nullable=False) description = Column(String, nullable=False)
author_id = Column(Integer, ForeignKey('users.id')) 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') junction = relationship('GroupJunction', backref='groups')
@ -84,27 +87,11 @@ class GroupJunction (base): # pylint: disable=too-few-public-methods, C0103
__tablename__ = 'group_junction' __tablename__ = 'group_junction'
id = Column(Integer, primary_key=True) 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')) group_id = Column(Integer, ForeignKey('groups.id'))
post_id = Column(Integer, ForeignKey('posts.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 class Logs (base): # pylint: disable=too-few-public-methods, C0103
""" """
Log table Log table
@ -116,8 +103,8 @@ class Logs (base): # pylint: disable=too-few-public-methods, C0103
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey('users.id'))
ip_address = Column(String, nullable=False) ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False) code = Column(Integer, nullable=False)
msg = Column(String, nullable=False) note = Column(String, nullable=False)
created_at = Column(DateTime, 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 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) id = Column(Integer, primary_key=True)
ip_address = Column(String, nullable=False) ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False) code = Column(Integer, nullable=False)
msg = Column(String, nullable=False) note = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False) banned_at = Column(DateTime, nullable=False, server_default=func.now()) # pylint: disable=E1102
# check if database file exists, if not create it # 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.placeholder = 'Passywassy';
passwordInput.id = 'password'; 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(usernameInput);
loginForm.appendChild(passwordInput); loginForm.appendChild(passwordInput);
loginForm.appendChild(rememberMeSpan);
popUpShow( popUpShow(
'Login!', 'Login!',
@ -47,6 +63,7 @@ function login(event) {
let formUsername = document.querySelector("#username").value; let formUsername = document.querySelector("#username").value;
let formPassword = document.querySelector("#password").value; let formPassword = document.querySelector("#password").value;
let formRememberMe = document.querySelector("#remember-me").checked;
if (formUsername === "" || formPassword === "") { if (formUsername === "" || formPassword === "") {
addNotification("Please fill in all fields!!!!", 3); addNotification("Please fill in all fields!!!!", 3);
@ -54,27 +71,24 @@ function login(event) {
} }
// Make form // Make form
var formData = new FormData(); const formData = new FormData();
formData.append("username", formUsername); formData.append("username", formUsername);
formData.append("password", formPassword); formData.append("password", formPassword);
formData.append("remember-me", formRememberMe);
fetch('/auth/login', { fetch('/auth/login', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(response => { }).then(response => {
if (response.status === 200) { if (response.ok) {
location.reload(); location.reload();
} else { } else {
switch (response.status) { if (response.status === 403) {
case 500:
addNotification('Server exploded, F\'s in chat', 2);
break;
case 403:
addNotification('None but devils play past here... Wrong information', 2); addNotification('None but devils play past here... Wrong information', 2);
break; } else if (response.status === 500) {
default: addNotification('Server exploded, F\'s in chat', 2);
} else {
addNotification('Error logging in, blame someone', 2); addNotification('Error logging in, blame someone', 2);
break;
} }
} }
}).catch(error => { }).catch(error => {
@ -153,37 +167,33 @@ function register(event) {
} }
// Make form // Make form
var formData = new FormData(); const formData = new FormData();
formData.append("username", formUsername); formData.append("username", formUsername);
formData.append("email", formEmail); formData.append("email", formEmail);
formData.append("password", formPassword); formData.append("password", formPassword);
formData.append("password-repeat", formPasswordRepeat); formData.append("password-repeat", formPasswordRepeat);
// Send form to server // Send form to server
fetch('/auth/login', { fetch('/auth/register', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(response => { }).then(response => {
if (response.status === 200) { if (response.ok) {
if (response === "gwa gwa") {
addNotification('Registered successfully! Now please login to continue', 1); addNotification('Registered successfully! Now please login to continue', 1);
showLogin(); showLogin();
} else { } else {
for (let i = 0; i < response.length; i++) { if (response.status === 400) {
addNotification(response[i], 2); response.json().then(data => {
for (let i = 0; i < data.length; i++) {
addNotification(data[i], 2);
} }
} });
} else { } else if (response.status === 403) {
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); addNotification('None but devils play past here... Wrong information', 2);
break; } else if (response.status === 500) {
default: addNotification('Server exploded, F\'s in chat', 2);
} else {
addNotification('Error logging in, blame someone', 2); addNotification('Error logging in, blame someone', 2);
break;
} }
} }
}).catch(error => { }).catch(error => {

View file

@ -141,38 +141,41 @@ function clearUpload() {
} }
function createJob(file) { // function createJob(file) {
jobContainer = document.createElement("div"); // jobContainer = document.createElement("div");
jobContainer.classList.add("job"); // jobContainer.classList.add("job");
jobStatus = document.createElement("span"); // jobStatus = document.createElement("span");
jobStatus.classList.add("job__status"); // jobStatus.classList.add("job__status");
jobStatus.innerHTML = "Uploading..."; // jobStatus.innerHTML = "Uploading...";
jobProgress = document.createElement("span"); // jobProgress = document.createElement("span");
jobProgress.classList.add("progress"); // jobProgress.classList.add("progress");
jobImg = document.createElement("img"); // jobImg = document.createElement("img");
jobImg.src = URL.createObjectURL(file); // jobImg.src = URL.createObjectURL(file);
jobImgFilter = document.createElement("span"); // jobImgFilter = document.createElement("span");
jobImgFilter.classList.add("img-filter"); // jobImgFilter.classList.add("img-filter");
jobContainer.appendChild(jobStatus); // jobContainer.appendChild(jobStatus);
jobContainer.appendChild(jobProgress); // jobContainer.appendChild(jobProgress);
jobContainer.appendChild(jobImg); // jobContainer.appendChild(jobImg);
jobContainer.appendChild(jobImgFilter); // jobContainer.appendChild(jobImgFilter);
return jobContainer; // return jobContainer;
} // }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', () => {
// Function to upload images // Function to upload images
let uploadTab = document.querySelector(".upload-panel"); 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 uploadTabDrag = uploadTab.querySelector("#dragIndicator");
let uploadForm = uploadTab.querySelector('#uploadForm'); let uploadForm = uploadTab.querySelector('#uploadForm');
let jobList = document.querySelector(".upload-jobs"); // let jobList = document.querySelector(".upload-jobs");
let fileDrop = uploadForm.querySelector('.fileDrop-block'); let fileDrop = uploadForm.querySelector('.fileDrop-block');
let fileDropTitle = fileDrop.querySelector('.status'); let fileDropTitle = fileDrop.querySelector('.status');
@ -225,53 +228,82 @@ document.addEventListener('DOMContentLoaded', function() {
formData.append("description", fileDescription.value); formData.append("description", fileDescription.value);
formData.append("tags", fileTags.value); formData.append("tags", fileTags.value);
jobItem = createJob(fileUpload.files[0]); // jobItem = createJob(fileUpload.files[0]);
jobStatus = jobItem.querySelector(".job__status"); // jobStatus = jobItem.querySelector(".job__status");
// Upload the information // Upload the information
$.ajax({ // $.ajax({
url: '/api/upload', // url: '/api/upload',
type: 'post', // type: 'post',
data: formData, // data: formData,
contentType: false, // contentType: false,
processData: false, // processData: false,
beforeSend: function () { // beforeSend: function () {
// Add job to list // // Add job to list
jobList.appendChild(jobItem); // jobList.appendChild(jobItem);
}, // },
success: function (response) { // success: function (response) {
jobItem.classList.add("success"); // jobItem.classList.add("success");
jobStatus.innerHTML = "Uploaded successfully"; // jobStatus.innerHTML = "Uploaded successfully";
if (!document.querySelector(".upload-panel").classList.contains("open")) { // if (!document.querySelector(".upload-panel").classList.contains("open")) {
addNotification("Image uploaded successfully", 1); // addNotification("Image uploaded successfully", 1);
} // }
}, // },
error: function (response) { // error: function (response) {
jobItem.classList.add("critical"); // 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) { switch (response.status) {
case 500: case 500:
jobStatus.innerHTML = "Server exploded, F's in chat"; addNotification("Server exploded, F's in chat", 2)
break; break;
case 400: case 400:
case 404: case 404:
jobStatus.innerHTML = "Error uploading. Blame yourself"; addNotification("Error uploading. Blame yourself", 2)
break; break;
case 403: case 403:
jobStatus.innerHTML = "None but devils play past here..."; addNotification("None but devils play past here...", 2)
break; break;
case 413: case 413:
jobStatus.innerHTML = "File too large!!!!!!"; addNotification("File too large!!!!!!", 2);
break; break;
default: default:
jobStatus.innerHTML = "Error uploading file, blame someone"; addNotification("Error uploading file, blame someone", 2)
break; break;
} }
if (!document.querySelector(".upload-panel").classList.contains("open")) {
addNotification("Error uploading file", 2);
}
},
}); });
clearUpload(); clearUpload();
// Reset drop // 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 // fade in images
function imgFade(obj, time = 250) { 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 // Lazy load images when they are in view
function loadOnView() { function loadOnView() {
let lazyLoad = document.querySelectorAll('#lazy-load'); const lazyLoad = document.querySelectorAll('#lazy-load');
for (let i = 0; i < lazyLoad.length; i++) { for (let i = 0; i < lazyLoad.length; i++) {
let image = lazyLoad[i]; let image = lazyLoad[i];
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) { if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
if (!image.src) { if (!image.src && webpSupport) {
image.src = `/api/file/${image.getAttribute('data-src')}?r=thumb` // e=webp 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 ' + 'Using <a href="https://phosphoricons.com/">Phosphoricons</a> and ' +
'<a href="https://www.gent.media/manrope">Manrope</a> <br>' + '<a href="https://www.gent.media/manrope">Manrope</a> <br>' +
'Made by Fluffy and others with ❤️ <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'); iconElement.classList.add('sniffle__notification-icon');
notification.appendChild(iconElement); notification.appendChild(iconElement);
// Set the icon based on the notification level, not pretty but it works :3 // Set the icon based on the notification level, not pretty but it works :3
if (notificationLevel == 1) { if (notificationLevel === 1) {
notification.classList.add('success'); notification.classList.add('success');
iconElement.innerHTML = successIcon; iconElement.innerHTML = successIcon;
} else if (notificationLevel == 2) { } else if (notificationLevel === 2) {
notification.classList.add('critical'); notification.classList.add('critical');
iconElement.innerHTML = criticalIcon; iconElement.innerHTML = criticalIcon;
} else if (notificationLevel == 3) { } else if (notificationLevel === 3) {
notification.classList.add('warning'); notification.classList.add('warning');
iconElement.innerHTML = warningIcon; iconElement.innerHTML = warningIcon;
} else { } 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 &.black
@include btn-block($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 .input-block
padding: 0.5rem 1rem padding: 0.5rem 1rem
@ -122,6 +137,7 @@
border-radius: $rad-inner border-radius: $rad-inner
cursor: pointer cursor: pointer
overflow: hidden
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out
input input
@ -130,6 +146,13 @@
opacity: 0 opacity: 0
cursor: pointer cursor: pointer
.status
width: 100%
white-space: nowrap
text-overflow: ellipsis
text-align: center
overflow: hidden
&:hover &:hover
background-color: RGBA($white, 0.2) background-color: RGBA($white, 0.2)
color: RGB($white) color: RGB($white)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,182 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block nav_groups %}selected{% endblock %} {% block nav_groups %}selected{% endblock %}
{% block head %} {% 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> <style>
{% if images %} {% 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 { .banner-content p {
color: {{ text_colour }} !important; color: {{ text_colour }} !important;
} }
@ -11,57 +185,97 @@
} }
.banner-filter { .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) { @media (max-width: 800px) {
.banner-filter { .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 %} {% endif %}
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="banner {% if not images %}small{% endif %}"> {% if images %}
{% if not images %} <div class="banner">
<div class="banner-content"> <img src="{{ url_for('api.file', file_name=images.0.filename ) }}?r=prev" onload="imgFade(this)" style="opacity:0;"/>
<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;"/>
<span class="banner-filter"></span> <span class="banner-filter"></span>
<div class="banner-content"> <div class="banner-content">
<p>By {{ group.author_username }} - {{ images|length }} Images</p> <p class="banner-info">By {{ group.author_username }} - {{ images|length }} Images</p>
<h1>{{ group.name }}</h1> <h1 class="banner-header">{{ group.name }}</h1>
<p>{{ group.description }}</p> <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> </div>
{% endif %} {% endif %}
</div> </div>
</div>
<form id="modifyGroup"> </div>
<input type="text" name="group" placeholder="group id" value="{{ group.id }}"> {% else %}
<input type="text" name="image" placeholder="image id"> <div class="banner-small">
<input type="text" name="action" placeholder="add/remove" value="add"> <div class="banner-content">
<button type="submit">Submit</button> <h1 class="banner-header">{{ group.name }}</h1>
</form> <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 %} {% if images %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% 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"> <div class="image-filter">
<p class="image-subtitle"></p> <p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p> <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div> </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> </a>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="big-text"> <div class="big-text">
<h1>*crickets chirping*</h1> <h1>*crickets chirping*</h1>
{% if g.user %} {% if current_user.is_authenticated %}
<p>Add some images to the group!</p> <p>Add some images to the group!</p>
{% else %} {% else %}
<p>Login to start managing this image group!</p> <p>Login to start managing this image group!</p>
@ -69,32 +283,3 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% 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' %} {% extends 'layout.html' %}
{% block nav_groups %}selected{% endblock %} {% 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 %} {% block content %}
<div class="banner small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1>{{ config.WEBSITE.name }}</h1> <h1 class="banner-header">{{ config.WEBSITE.name }}</h1>
{% if groups|length == 0 %} {% if groups|length == 0 %}
<p>0 groups :<</p> <p class="banner-info">No groups!!!!</p>
{% elif groups|length == 69 %} {% elif groups|length == 69 %}
<p>{{ groups|length }} groups, uwu</p> <p class="banner-info">{{ groups|length }} groups, uwu</p>
{% else %} {% 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 %} {% endif %}
</div> </div>
</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 %} {% if groups %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for group in groups %} {% for group in groups %}
@ -31,7 +127,7 @@
<div class="images size-{{ group.images|length }}"> <div class="images size-{{ group.images|length }}">
{% if group.images|length > 0 %} {% if group.images|length > 0 %}
{% for image in group.images %} {% 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 %} {% endfor %}
{% else %} {% else %}
<img src="{{ url_for('static', filename='error.png') }}" class="loaded"/> <img src="{{ url_for('static', filename='error.png') }}" class="loaded"/>
@ -43,7 +139,7 @@
{% else %} {% else %}
<div class="big-text"> <div class="big-text">
<h1>*crickets chirping*</h1> <h1>*crickets chirping*</h1>
{% if g.user %} {% if current_user.is_authenticated %}
<p>You can get started by creating a new image group!</p> <p>You can get started by creating a new image group!</p>
{% else %} {% else %}
<p>Login to start seeing anything here!</p> <p>Login to start seeing anything here!</p>

View file

@ -1,8 +1,8 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block wrapper_class %}image-wrapper{% endblock %} {% block wrapper_class %}image-wrapper{% endblock %}
{% block head %} {% block head %}
<meta property="og:image" content="{{ url_for('api.file', file_name=image.file_name) }}"/> <meta property="og:image" content="{{ url_for('api.file', file_name=image.filename) }}"/>
<meta name="theme-color" content="#{{ image.image_colours.0.0 }}{{ image.image_colours.0.1 }}{{ image.image_colours.0.2 }}"/> <meta name="theme-color" content="rgb({{ image.colours.0.0 }}{{ image.colours.0.1 }}{{ image.colours.0.2 }})"/>
<script type="text/javascript"> <script type="text/javascript">
function imageFullscreenOff() { function imageFullscreenOff() {
@ -18,7 +18,7 @@
function imageFullscreenOn() { function imageFullscreenOn() {
let fullscreen = document.querySelector('.image-fullscreen') 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"; document.querySelector("html").style.overflow = "hidden";
fullscreen.style.display = 'flex'; fullscreen.style.display = 'flex';
@ -33,7 +33,8 @@
} }
} }
{% if g.user['id'] == image['author_id'] %} {% if current_user.id == image.author_id %}
function imageDelete() {
cancelBtn = document.createElement('button'); cancelBtn = document.createElement('button');
cancelBtn.classList.add('btn-block'); cancelBtn.classList.add('btn-block');
cancelBtn.innerHTML = 'nuuuuuuuu'; cancelBtn.innerHTML = 'nuuuuuuuu';
@ -45,27 +46,27 @@
deleteBtn.innerHTML = 'Dewww eeeet!'; deleteBtn.innerHTML = 'Dewww eeeet!';
deleteBtn.onclick = deleteConfirm; deleteBtn.onclick = deleteConfirm;
function imageDelete() { popUpShow('DESTRUCTION!!!!!!',
popUpShow( 'Do you want to delete this image along with all of its data??? ' +
'DESTRUCTION!!!!!!', 'This action is irreversible!',
'Do you want to delete this image along with all of its data??? This action is irreversible!',
null, null,
[cancelBtn, deleteBtn] [cancelBtn, deleteBtn]);
);
} }
function deleteConfirm() { function deleteConfirm() {
popupDissmiss(); popupDissmiss();
$.ajax({ fetch('{{ url_for('api.delete_image', image_id=image['id']) }}', {
url: '{{ url_for('api.delete_image', image_id=image['id']) }}', method: 'POST',
type: 'post', headers: {
data: { 'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'delete' action: 'delete'
}, })
success: function (response) { }).then(function(response) {
if (response.ok) {
window.location.href = '/'; window.location.href = '/';
}, } else {
error: function (response) {
addNotification(`Image *clings*: ${response}`, 2); addNotification(`Image *clings*: ${response}`, 2);
} }
}); });
@ -79,30 +80,31 @@
<style> <style>
.background span { .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> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="background"> <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> <span></span>
</div> </div>
<div class="image-fullscreen" onclick="imageFullscreenOff()"> <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>
<div class="image-grid"> <div class="image-grid">
<div class="image-container" id="image-container"> <div class="image-container" id="image-container">
<img <img
src="{{ url_for('api.file', file_name=image.file_name) }}?r=prev" src="{{ url_for('api.file', file_name=image.filename) }}?r=prev"
alt="{{ image.post_alt }}" alt="{{ image.alt }}"
onload="imgFade(this)" style="opacity:0;" onload="imgFade(this)"
style="opacity: 0;"
onerror="this.src='{{ url_for('static', filename='error.png')}}'" onerror="this.src='{{ url_for('static', filename='error.png')}}'"
{% if "File" in image.image_exif %} {% if "File" in image.exif %}
width="{{ image.image_exif.File.Width.raw }}" width="{{ image.exif.File.Width.raw }}"
height="{{ image.image_exif.File.Height.raw }}" height="{{ image.exif.File.Height.raw }}"
{% endif %} {% endif %}
/> />
</div> </div>
@ -111,7 +113,7 @@
{% if next_url %} {% if next_url %}
<div> <div>
<a class="pill-item" href="{{ next_url }}"> <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"> <span class="tool-tip">
Previous 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> <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 %} {% endif %}
<div> <div>
<button class="pill-item" onclick="imageFullscreenOn()"> <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"> <span class="tool-tip">
Fullscreen 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> <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> </span>
</button> </button>
<button class="pill-item" onclick="imageShare()"> <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"> <span class="tool-tip">
Share 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> <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> </span>
</button> </button>
<a class="pill-item" href="/api/file/{{ image.file_name }}" download onclick="addNotification('Download started!', 4)"> <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="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> <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"> <span class="tool-tip">
Download 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> <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> </span>
</a> </a>
</div> </div>
{% if image.author_id == g.user.id %} {% if current_user.id == image.author_id %}
<div> <div>
<button class="pill-item pill__critical" onclick="imageDelete()"> <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"> <span class="tool-tip">
Delete 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> <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> </span>
</button> </button>
<button class="pill-item pill__critical" onclick="imageEdit()"> <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"> <span class="tool-tip">
Edit 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> <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 %} {% if prev_url %}
<div> <div>
<a class="pill-item" href="{{ prev_url }}"> <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"> <span class="tool-tip">
Next 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> <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> <table>
<tr> <tr>
<td>Author</td> <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>
<tr> <tr>
<td>Upload date</td> <td>Upload date</td>
@ -208,7 +210,7 @@
</tr> </tr>
</table> </table>
<div class="img-colours"> <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> <span style="background-color: rgb({{col.0}}, {{col.1}}, {{col.2}})"></span>
{% endfor %} {% endfor %}
</div> </div>
@ -224,7 +226,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% for tag in image.image_exif %} {% for tag in image.exif %}
<div class="info-tab"> <div class="info-tab">
<div class="info-header"> <div class="info-header">
{% if tag == 'Photographer' %} {% if tag == 'Photographer' %}
@ -249,17 +251,17 @@
</div> </div>
<div class="info-table"> <div class="info-table">
<table> <table>
{% for subtag in image.image_exif[tag] %} {% for subtag in image.exif[tag] %}
<tr> <tr>
<td>{{ subtag }}</td> <td>{{ subtag }}</td>
{% if image.image_exif[tag][subtag]['formatted'] %} {% if image.exif[tag][subtag]['formatted'] %}
{% if image.image_exif[tag][subtag]['type'] == 'date' %} {% if image.exif[tag][subtag]['type'] == 'date' %}
<td><span class="time">{{ image.image_exif[tag][subtag]['formatted'] }}</span></td> <td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
{% else %} {% else %}
<td>{{ image.image_exif[tag][subtag]['formatted'] }}</td> <td>{{ image.exif[tag][subtag]['formatted'] }}</td>
{% endif %} {% endif %}
{% elif image.image_exif[tag][subtag]['raw'] %} {% elif image.exif[tag][subtag]['raw'] %}
<td>{{ image.image_exif[tag][subtag]['raw'] }}</td> <td>{{ image.exif[tag][subtag]['raw'] }}</td>
{% else %} {% else %}
<td class="empty-table">Oops, an error</td> <td class="empty-table">Oops, an error</td>
{% endif %} {% endif %}

View file

@ -1,27 +1,27 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block nav_home %}selected{% endblock %} {% block nav_home %}selected{% endblock %}
{% block content %} {% block content %}
<div class="banner small"> <div class="banner-small">
<div class="banner-content"> <div class="banner-content">
<h1>{{ config.WEBSITE.name }}</h1> <h1 class="banner-header">{{ config.WEBSITE.name }}</h1>
{% if images|length == 0 %} {% if images|length == 0 %}
<p>0 images :<</p> <p class="banner-info">0 images D:</p>
{% elif images|length == 69 %} {% elif images|length == 69 %}
<p>{{ images|length }} images, nice</p> <p class="banner-info">{{ images|length }} images, nice</p>
{% else %} {% else %}
<p>{{ images|length }} images</p> <p class="banner-info">{{ images|length }} images</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if images %} {% if images %}
<div class="gallery-grid"> <div class="gallery-grid">
{% for image in images %} {% 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"> <div class="image-filter">
<p class="image-subtitle"></p> <p class="image-subtitle"></p>
<p class="image-title"><span class="time">{{ image.created_at }}</span></p> <p class="image-title"><span class="time">{{ image.created_at }}</span></p>
</div> </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> </a>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -1,17 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ config.WEBSITE.name }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ config.WEBSITE.name }}</title>
<meta name="description" content="{{ config.WEBSITE.motto }}"/> <meta name="description" content="{{ config.WEBSITE.motto }}"/>
<meta name="author" content="{{ config.WEBSITE.author }}"/> <meta name="author" content="{{ config.WEBSITE.author }}"/>
<meta property="og:title" content="{{ config.WEBSITE.name }}"/> <meta property="og:title" content="{{ config.WEBSITE.name }}"/>
<meta property="og:description" content="{{ config.WEBSITE.motto }}"/> <meta property="og:description" content="{{ config.WEBSITE.motto }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta name="twitter:title" content="{{ config.WEBSITE.name }}"/> <meta name="twitter:title" content="{{ config.WEBSITE.name }}"/>
<meta name="twitter:description" content="{{ config.WEBSITE.motto }}"/> <meta name="twitter:description" content="{{ config.WEBSITE.motto }}"/>
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
@ -27,12 +25,18 @@
type="image/svg+xml" type="image/svg+xml"
media="(prefers-color-scheme: dark)"/> media="(prefers-color-scheme: dark)"/>
<link rel="stylesheet" href="{{url_for('static', filename='theme/style.css')}}" defer> {% assets "js_pre" %}
{% assets "js_all" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script> <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% 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 %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
@ -58,11 +62,11 @@
<div class="wrapper"> <div class="wrapper">
<div class="navigation"> <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 %}"> <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> <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 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> <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> </span>
@ -70,16 +74,16 @@
<a href="{{url_for('group.groups')}}" class="navigation-item {% block nav_groups %}{% endblock %}"> <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> <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 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> <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> </span>
</a> </a>
{% if g.user %} {% if current_user.is_authenticated %}
<button class="navigation-item {% block nav_upload %}{% endblock %}" onclick="toggleUploadTab()"> <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> <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> <span class="tool-tip">
Upload 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> <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> </span>
@ -88,10 +92,10 @@
<span class="navigation-spacer"></span> <span class="navigation-spacer"></span>
{% if g.user %} {% if current_user.is_authenticated %}
<a href="{{url_for('gallery.profile')}}" class="navigation-item {% block nav_profile %}{% endblock %}"> <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> <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> <span class="tool-tip">
Profile 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> <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> </span>
@ -99,7 +103,7 @@
<a href="{{url_for('settings.general')}}" class="navigation-item {% block nav_settings %}{% endblock %}"> <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> <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> <span class="tool-tip">
Settings 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> <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> </span>
@ -107,7 +111,7 @@
{% else %} {% else %}
<button class="navigation-item {% block nav_login %}{% endblock %}" onclick="showLogin()"> <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> <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> <span class="tool-tip">
Login 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> <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> </span>
@ -115,7 +119,7 @@
{% endif %} {% endif %}
</div> </div>
{% if g.user %} {% if current_user.is_authenticated %}
<div class="upload-panel"> <div class="upload-panel">
<span class="click-off" onclick="closeUploadTab()"></span> <span class="click-off" onclick="closeUploadTab()"></span>
<div class="container"> <div class="container">
@ -132,7 +136,7 @@
<input class="input-block" type="text" placeholder="alt" id="alt"/> <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="description" id="description"/>
<input class="input-block" type="text" placeholder="tags" id="tags"/> <input class="input-block" type="text" placeholder="tags" id="tags"/>
<button class="btn-block primary" type="submit">Upload</button> <button class="btn-block" type="submit">Upload</button>
</form> </form>
<div class="upload-jobs"></div> <div class="upload-jobs"></div>
</div> </div>

View file

@ -2,17 +2,29 @@
{% block nav_profile %}selected{% endblock %} {% block nav_profile %}selected{% endblock %}
{% block content %} {% block content %}
<div class="banner"> <div class="banner-small">
<img src="{{ url_for('static', filename='') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
<div class="banner-content"> <div class="banner-content">
<h1>Profile</h1> <h1 class="banner-header">{{ user.username }}</h1>
<p>Hello {{ g.user['username'] }}</p> <p class="banner-info">Member since <span class="time">{{ user.joined_at }}</span></p>
</div> </div>
</div> </div>
{% if images %}
<h1>User</h1> <div class="gallery-grid">
<p>{{user_id}}</p> {% 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 %} {% endblock %}

View file

@ -3,5 +3,10 @@
{% block settings_account %}settings-nav__item-selected{% endblock %} {% block settings_account %}settings-nav__item-selected{% endblock %}
{% block settings_content %} {% block settings_content %}
<h2>Account</h2> <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 %} {% endblock %}

View file

@ -2,14 +2,17 @@
{% block nav_settings %}selected{% endblock %} {% block nav_settings %}selected{% endblock %}
{% block content %} {% block content %}
<div class="banner"> <div class="banner-small">
<img src="{{ url_for('static', filename='images/bg.svg') }}" onload="imgFade(this)" style="opacity:0;"/>
<span></span>
<div class="banner-content"> <div class="banner-content">
{% block banner_subtitle%}{% endblock %} <h1 class="banner-header">Settings</h1>
<h1>Settings</h1> <p class="banner-info">{% block banner_subtitle%}{% endblock %}</p>
<p>All the red buttons in one place, what could go wrong?</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>
</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

@ -4,7 +4,7 @@ Tools for generating images and thumbnails
import os import os
import platformdirs import platformdirs
from PIL import Image, ImageOps #, ImageFilter from PIL import Image, ImageOps
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@ -37,11 +37,9 @@ def generate_thumbnail(file_name, resolution, ext=None):
if resolution in ['prev', 'preview']: if resolution in ['prev', 'preview']:
res_x, res_y = (1920, 1080) res_x, res_y = (1920, 1080)
elif resolution in ['thumb', 'thumbnail']: elif resolution in ['thumb', 'thumbnail']:
res_x, res_y = (400, 400) res_x, res_y = (350, 350)
elif resolution in ['icon', 'favicon']: elif resolution in ['icon', 'favicon']:
res_x, res_y = (10, 10) res_x, res_y = (10, 10)
elif len(resolution.split('x')) == 2:
res_x, res_y = resolution.split('x')
else: else:
return None return None

View file

@ -279,7 +279,7 @@ def lens_specification(value):
""" """
try: try:
return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm' return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm'
except ValueError: except TypeError:
return None 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 os
import pathlib import pathlib
import logging import logging
from datetime import datetime as dt
import platformdirs 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 werkzeug.utils import secure_filename
from flask_login import login_required, current_user
from colorthief import ColorThief from colorthief import ColorThief
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from gallery.auth import login_required
from gallery import db from gallery import db
from gallery.utils import metadata as mt from gallery.utils import metadata as mt
@ -37,7 +37,7 @@ def file(file_name):
file_name = secure_filename(file_name) # Sanitize file name file_name = secure_filename(file_name) # Sanitize file name
# if no args are passed, return the raw file # 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)): if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name)):
abort(404) abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name) return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name)
@ -83,14 +83,13 @@ def upload():
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database # Save to database
query = db.Posts(author_id=g.user.id, query = db.Posts(author_id=current_user.id,
created_at=dt.utcnow(), filename=img_name + '.' + img_ext,
file_name=img_name+'.'+img_ext, mimetype=img_ext,
file_type=img_ext, exif=img_exif,
image_exif=img_exif, colours=img_colors,
image_colours=img_colors, description=form['description'],
post_description=form['description'], alt=form['alt'])
post_alt=form['alt'])
db_session.add(query) db_session.add(query)
db_session.commit() 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) # Check if image exists and if user is allowed to delete it (author)
if img is None: if img is None:
abort(404) abort(404)
if img.author_id != g.user.id: if img.author_id != current_user.id:
abort(403) abort(403)
# Delete file # Delete file
try: 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: 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 # Delete cached files
cache_path = os.path.join(platformdirs.user_config_dir('onlylegs'), 'cache') 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 + '*'): for cache_file in pathlib.Path(cache_path).glob(cache_name + '*'):
os.remove(cache_file) os.remove(cache_file)
@ -135,7 +134,7 @@ def delete_image(image_id):
# Commit all changes # Commit all changes
db_session.commit() 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']) flash(['Image was all in Le Head!', '1'])
return 'Gwa Gwa' return 'Gwa Gwa'
@ -148,8 +147,7 @@ def create_group():
""" """
new_group = db.Groups(name=request.form['name'], new_group = db.Groups(name=request.form['name'],
description=request.form['description'], description=request.form['description'],
author_id=g.user.id, author_id=current_user.id)
created_at=dt.utcnow())
db_session.add(new_group) db_session.add(new_group)
db_session.commit() db_session.commit()
@ -165,21 +163,21 @@ def modify_group():
""" """
group_id = request.form['group'] group_id = request.form['group']
image_id = request.form['image'] image_id = request.form['image']
action = request.form['action']
group = db_session.query(db.Groups).filter_by(id=group_id).first() group = db_session.query(db.Groups).filter_by(id=group_id).first()
if group is None: if group is None:
abort(404) abort(404)
elif group.author_id != g.user.id: elif group.author_id != current_user.id:
abort(403) abort(403)
if request.form['action'] == 'add': if action == 'add':
if not (db_session.query(db.GroupJunction) if not (db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id) .filter_by(group_id=group_id, post_id=image_id)
.first()): .first()):
db_session.add(db.GroupJunction(group_id=group_id, db_session.add(db.GroupJunction(group_id=group_id,
post_id=image_id, post_id=image_id))
date_added=dt.utcnow()))
elif request.form['action'] == 'remove': elif request.form['action'] == 'remove':
(db_session.query(db.GroupJunction) (db_session.query(db.GroupJunction)
.filter_by(group_id=group_id, post_id=image_id) .filter_by(group_id=group_id, post_id=image_id)
@ -190,17 +188,23 @@ def modify_group():
return ':3' return ':3'
@blueprint.route('/metadata/<int:img_id>', methods=['GET']) @blueprint.route('/group/delete', methods=['POST'])
def metadata(img_id): 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) abort(404)
elif group.author_id != current_user.id:
abort(403)
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img.file_name) db_session.query(db.Groups).filter_by(id=group_id).delete()
exif = mt.Metadata(img_path).yoink() 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 # For each image, get the image data and add it to the group item
group.images = [] group.images = []
for image in images: for image in images:
group.images.append(db_session.query(db.Posts.file_name, db.Posts.post_alt, group.images.append(db_session.query(db.Posts.filename, db.Posts.alt,
db.Posts.image_colours, db.Posts.id) db.Posts.colours, db.Posts.id)
.filter(db.Posts.id == image[0]) .filter(db.Posts.id == image[0])
.first()) .first())
@ -78,8 +78,8 @@ def group(group_id):
# Check contrast for the first image in the group for the banner # Check contrast for the first image in the group for the banner
text_colour = 'rgb(var(--fg-black))' text_colour = 'rgb(var(--fg-black))'
if images[0]: if images:
text_colour = contrast.contrast(images[0].image_colours[0], text_colour = contrast.contrast(images[0].colours[0],
'rgb(var(--fg-black))', 'rgb(var(--fg-black))',
'rgb(var(--fg-white))') 'rgb(var(--fg-white))')

View file

@ -3,6 +3,7 @@ Onlylegs Gallery - Routing
""" """
from flask import Blueprint, render_template, url_for, request from flask import Blueprint, render_template, url_for, request
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from flask_login import current_user
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from gallery import db from gallery import db
@ -18,9 +19,9 @@ def index():
""" """
Home page of the website, shows the feed of the latest images Home page of the website, shows the feed of the latest images
""" """
images = db_session.query(db.Posts.file_name, images = db_session.query(db.Posts.filename,
db.Posts.post_alt, db.Posts.alt,
db.Posts.image_colours, db.Posts.colours,
db.Posts.created_at, db.Posts.created_at,
db.Posts.id).order_by(db.Posts.id.desc()).all() 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 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>') # Get the user's data
def profile_id(user_id): user = db_session.query(db.Users).filter(db.Users.id == user_id).first()
"""
Shows user ofa given id, displays their uploads and other info if not user:
""" abort(404, 'User not found :c')
return render_template('profile.html', user_id=user_id)
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 OnlyLegs - Settings page
""" """
from flask import Blueprint, render_template from flask import Blueprint, render_template
from flask_login import login_required
from gallery.auth import login_required
blueprint = Blueprint('settings', __name__, url_prefix='/settings') 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]] [[package]]
name = "astroid" name = "astroid"
@ -253,21 +253,20 @@ brotli = "*"
flask = "*" flask = "*"
[[package]] [[package]]
name = "flask-session2" name = "flask-login"
version = "1.3.1" version = "0.6.2"
description = "Adds server-side session support to your Flask application" description = "User authentication and session management for Flask."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.2,<4.0.0" python-versions = ">=3.7"
files = [ files = [
{file = "Flask-Session2-1.3.1.tar.gz", hash = "sha256:171e986d4e314795f448a527095e42df6abfba76c3e4ce5c8e4c61c857c59cb2"}, {file = "Flask-Login-0.6.2.tar.gz", hash = "sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3"},
{file = "Flask_Session2-1.3.1-py3-none-any.whl", hash = "sha256:6d1615dfc4b247759f31f89bf16aba96fa1294077e700771875abe952f291959"}, {file = "Flask_Login-0.6.2-py3-none-any.whl", hash = "sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f"},
] ]
[package.dependencies] [package.dependencies]
cachelib = ">=0.9.0,<0.10.0" Flask = ">=1.0.4"
Flask = ">=2.2.2,<3.0.0" Werkzeug = ">=1.0.1"
pytz = ">=2022.2.1,<2023.0.0"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
@ -706,18 +705,6 @@ files = [
[package.extras] [package.extras]
cli = ["click (>=5.0)"] 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]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0" version = "6.0"
@ -1033,4 +1020,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "fdc83d433d98ba079b4614e37da26b9c81f07e0a646606c17de5c0fb07ae483e" content-hash = "431b58579dc3ebde52c8f3905c851556a465d5a82796a8a26718d69cb4915959"

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "onlylegs" name = "onlylegs"
version = "23.03.30" version = "23.04.05"
description = "Gallery built for fast and simple image management" description = "Gallery built for fast and simple image management"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"] authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT" license = "MIT"
@ -12,7 +12,7 @@ Flask = "^2.2.2"
Flask-Compress = "^1.13" Flask-Compress = "^1.13"
Flask-Caching = "^2.0.2" Flask-Caching = "^2.0.2"
Flask-Assets = "^2.0" Flask-Assets = "^2.0"
Flask-Session2 = "^1.3.1" Flask-Login = "^0.6.2"
SQLAlchemy = "^2.0.3" SQLAlchemy = "^2.0.3"
python-dotenv = "^0.21.0" python-dotenv = "^0.21.0"
gunicorn = "^20.1.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: if DEBUG:
from gallery import create_app 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) create_app().run(host=ADDRESS, port=PORT, debug=True, threaded=True)
else: else:
from setup.runner import OnlyLegs # pylint: disable=C0412 from setup.runner import OnlyLegs # pylint: disable=C0412
import sys
# If no address is specified, bind the server to all interfaces # Stop Gunicorn from reading the command line arguments as it causes errors
if not ADDRESS: sys.argv = [sys.argv[0]]
ADDRESS = '0.0.0.0'
options = { options = {
'bind': f'{ADDRESS}:{PORT}', 'bind': f'{ADDRESS}:{PORT}',

View file

@ -2,7 +2,7 @@
Startup arguments for the OnlyLegs gallery Startup arguments for the OnlyLegs gallery
-p, --port: Port to run on (default: 5000) -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) -w, --workers: Number of workers to run (default: 4)
-d, --debug: Run as Flask app in debug mode (default: False) -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 = argparse.ArgumentParser(description='Run the OnlyLegs gallery')
parser.add_argument('-p', '--port', type=int, default=5000, help='Port to run on') 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('-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') parser.add_argument('-d', '--debug', action='store_true', help='Run as Flask app in debug mode')
args = parser.parse_args() args = parser.parse_args()

View file

@ -135,15 +135,9 @@ class Configuration:
""" """
Set the logging config 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.getLogger('werkzeug').disabled = True
logging.basicConfig( logging.basicConfig(
filename=os.path.join(logs_path, 'only.log'), filename=os.path.join(USER_DIR, 'only.log'),
level=logging.INFO, level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S', datefmt='%Y-%m-%d %H:%M:%S',
format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s', format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',

View file

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