Compare commits

...

6 Commits

1755 changed files with 1743 additions and 289 deletions

View File

@ -3,6 +3,7 @@ certifi==2022.12.7
charset-normalizer==3.0.1
click==7.1.2
dnspython==1.16.0
ecdsa==0.18.0
email-validator==1.1.2
eventlet==0.30.1
Flask==1.1.2
@ -26,6 +27,7 @@ pyasn1-modules==0.2.8
pycairo==1.23.0
PyGObject==3.42.2
pyparsing==2.4.7
pywebpush==1.14.0
pyxdg==0.28
requests==2.28.2
rsa==4.7

View File

@ -4,6 +4,7 @@ import builtins
import threading
import re
import secrets
import subprocess
# Lib imports
from flask import session
@ -11,8 +12,12 @@ from flask import session
# Application imports
from core import app
from core.utils import Logger
from core.utils import MessageHandler # Get simple message processor
class BuiltinsException(Exception):
...
# NOTE: Threads WILL NOT die with parent's destruction.
def threaded_wrapper(fn):
@ -41,22 +46,76 @@ def _get_file_size(file):
# NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()})
builtins.app_name = "WebFM"
builtins.threaded = threaded_wrapper
builtins.daemon_threaded = daemon_threaded_wrapper
builtins.sizeof_fmt = sizeof_fmt_def
builtins.get_file_size = _get_file_size
builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
builtins.BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
builtins.BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
builtins.valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
builtins.logger = Logger().get_logger()
builtins.app_name = "WebFM"
builtins.threaded = threaded_wrapper
builtins.daemon_threaded = daemon_threaded_wrapper
builtins.sizeof_fmt = sizeof_fmt_def
builtins.get_file_size = _get_file_size
builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
builtins.BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
builtins.BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
builtins.valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
builtins.logger = Logger().get_logger()
builtins.json_message = MessageHandler()
# NOTE: Need threads defined before instantiating
def _start_rtsp_and_ntfy_server():
PATH = f"{ROOT_FILE_PTH}/utils"
RTSP_PATH = f"{PATH}/rtsp-server"
NTFY_PATH = f"{PATH}/ntfy"
RAMFS = "/dev/shm/webfm"
SYMLINK = app.config['REMUX_FOLDER']
if not os.path.exists(RTSP_PATH) or not os.path.exists(f"{RTSP_PATH}/rtsp-simple-server"):
msg = f"\n\nAlert: Reference --> https://github.com/aler9/rtsp-simple-server/releases" + \
f"\nPlease insure {RTSP_PATH} exists and rtsp-simple-server binary is there.\n\n"
raise BuiltinsException(msg)
if not os.path.exists(NTFY_PATH) or not os.path.exists(f"{NTFY_PATH}/ntfy"):
msg = f"\n\nAlert: Reference --> https://ntfy.sh/" + \
f"\nPlease insure {NTFY_PATH} exists and ntfy binary is there.\n\n"
raise BuiltinsException(msg)
if not os.path.exists(RAMFS):
os.mkdir(RAMFS)
if not os.path.exists(SYMLINK):
os.symlink(RAMFS, SYMLINK)
@daemon_threaded
def _start_rtsp_server_threaded():
os.chdir(RTSP_PATH)
command = ["./rtsp-simple-server", "./rtsp-simple-server.yml"]
process = subprocess.Popen(command)
process.wait()
@daemon_threaded
def _start_ntfy_server_threaded():
os.chdir(NTFY_PATH)
command = ["./ntfy", "serve", "--behind-proxy", "--listen-http", ":7777"]
process = subprocess.Popen(command)
process.wait()
_start_ntfy_server_threaded()
_start_rtsp_server_threaded()
_start_rtsp_and_ntfy_server()
# NOTE: Need threads defined befor instantiating
from core.utils.shellfm.windows.controller import WindowController # Get file manager controller
window_controllers = {}
processes = {}
def _get_sse_id():
return session["win_controller_id"]
def _get_view():
controller = None
try:
@ -84,11 +143,59 @@ def _get_view():
view.logger = logger
session['win_controller_id'] = id
window_controllers.update( {id: controller } )
window_controllers.update( {id: controller} )
controller = window_controllers[ session["win_controller_id"] ].get_window_by_index(0).get_tab_by_index(0)
return controller
def _get_stream(video_path=None, stream_target=None):
process = None
try:
window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
tab = window.get_tab_by_index(0)
id = f"{window.get_id()}{tab.get_id()}"
process = processes[id]
except Exception as e:
if video_path and stream_target:
# NOTE: Yes, technically we should check if cuda is supported.
# Yes, the process probably should give us info we can process when failure occures.
command = [
"ffmpeg", "-nostdin", "-fflags", "+genpts", "-hwaccel", "cuda",
"-stream_loop", "-1", "-i", video_path, "-strict", "experimental",
"-vcodec", "copy", "-acodec", "copy", "-f", "rtsp",
"-rtsp_transport", "tcp", stream_target
]
builtins.get_view = _get_view
proc = subprocess.Popen(command, shell=False, stdin=None, stdout=None, stderr=None)
window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
tab = window.get_tab_by_index(0)
id = f"{window.get_id()}{tab.get_id()}"
processes.update( {id: proc} )
process = processes[id]
return process
def _kill_stream(process):
try:
if process.poll() == None:
process.terminate()
while process.poll() == None:
...
window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
tab = window.get_tab_by_index(0)
id = f"{window.get_id()}{tab.get_id()}"
del processes[id]
except Exception as e:
return False
return True
builtins.get_view = _get_view
builtins.get_sse_id = _get_sse_id
builtins.get_stream = _get_stream
builtins.kill_stream = _kill_stream

View File

@ -1,4 +1,5 @@
# Python imports
import os
# Lib imports
from flask import Flask
@ -6,7 +7,10 @@ from flask import Flask
from flask_oidc import OpenIDConnect
# Flask Login Path
from flask_bcrypt import Bcrypt
from flask_login import current_user, login_user, logout_user, LoginManager
from flask_login import current_user
from flask_login import login_user
from flask_login import logout_user
from flask_login import LoginManager
app = Flask(__name__)
app.config.from_object("core.config.ProductionConfig")
@ -36,10 +40,15 @@ app.jinja_env.globals['oidc_isAdmin'] = oidc_isAdmin
app.jinja_env.globals['TITLE'] = app.config["TITLE"]
from core.models import db, User, Favorites
from core.models import db
from core.models import User
from core.models import Favorites
db.init_app(app)
with app.app_context():
db.create_all()
from core.forms import RegisterForm, LoginForm
from core.forms import RegisterForm
from core.forms import LoginForm
from core import routes

View File

@ -1,14 +1,14 @@
# System import
import os, secrets
import os
import secrets
from datetime import timedelta
# Lib imports
# Apoplication imports
# Configs
APP_NAME = 'WebFM'
ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
@ -43,9 +43,9 @@ class Config(object):
# We are overiding some of the the shellmen view settings with these to make it all work with flask.
# These are passed along to the shellmen view from the Routes file upon the window controller creation.
ABS_THUMBS_PTH = f"{STATIC_FPTH}/imgs/thumbnails" # Used for thumbnail generation
REMUX_FOLDER = f"{STATIC_FPTH}/remuxs" # Remuxed files folder
FFMPG_THUMBNLR = f"{STATIC_FPTH}/ffmpegthumbnailer" # Thumbnail generator binary
ABS_THUMBS_PTH = f"{STATIC_FPTH}/imgs/thumbnails" # Used for thumbnail generation
REMUX_FOLDER = f"{STATIC_FPTH}/remuxs" # Remuxed files folder
FFMPG_THUMBNLR = f"{STATIC_FPTH}/ffmpegthumbnailer" # Thumbnail generator binary

View File

@ -1,9 +1,19 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from wtforms import StringField
from wtforms import PasswordField
from wtforms import SubmitField
from wtforms.validators import DataRequired
from wtforms.validators import Length
from wtforms.validators import Email
from wtforms.validators import EqualTo
from wtforms.validators import ValidationError
from core import User
class RegisterForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=4, max=24)])
email = StringField('Email', validators=[DataRequired(), Email()])

View File

@ -1,11 +1,12 @@
# System imports
# Lib imports
from flask_login import UserMixin
from flask_sqlalchemy import SQLAlchemy
# App imports
from . import app, login_manager
from flask_login import UserMixin
from . import app
from . import login_manager
db = SQLAlchemy(app)

View File

@ -9,7 +9,6 @@ from flask_uploads import ALL
from flask_uploads import configure_uploads
from flask_uploads import UploadSet
# App imports
# Get from __init__
from core import app
@ -17,10 +16,6 @@ from core import db
from core import Favorites
from core import oidc
from core.utils import MessageHandler # Get simple message processor
json_message = MessageHandler()
@app.route('/api/delete/<_hash>', methods=['GET', 'POST'])
@ -46,6 +41,8 @@ def delete_item(_hash = None):
msg = "[Error] Unable to delete the file/folder...."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
@app.route('/api/create/<_type>', methods=['GET', 'POST'])
@ -82,9 +79,9 @@ def create_item(_type = None):
msg = "[Success] created the file/dir..."
return json_message.create("success", msg)
else:
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
@app.route('/api/upload', methods=['GET', 'POST'])
@ -114,6 +111,6 @@ def upload():
msg = "[Success] Uploaded file(s)..."
return json_message.create("success", msg)
else:
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)

View File

@ -7,15 +7,11 @@ from flask import request
from core import app
from core import db
from core import Favorites # Get from __init__
from core.utils import MessageHandler # Get simple message processor
json_message = MessageHandler()
@app.route('/api/list-favorites', methods=['GET', 'POST'])
def listFavorites():
def list_favorites():
if request.method == 'POST':
list = db.session.query(Favorites).all()
faves = []
@ -23,12 +19,12 @@ def listFavorites():
faves.append([fave.link, fave.id])
return json_message.faves_list(faves)
else:
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
def loadFavorite(_id):
def load_favorite(_id):
if request.method == 'POST':
try:
ID = int(_id)
@ -40,13 +36,13 @@ def loadFavorite(_id):
print(repr(e))
msg = "Incorrect Favorites ID..."
return json_message.create("danger", msg)
else:
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
def manageFavorites(_action):
def manage_favorites(_action):
if request.method == 'POST':
ACTION = _action.strip()
view = get_view()
@ -66,6 +62,6 @@ def manageFavorites(_action):
db.session.commit()
return json_message.create("success", msg)
else:
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)

View File

@ -6,21 +6,19 @@ import shutil
# Lib imports
from flask import request
# App imports
# Get from __init__
from core import app
from core.utils import MessageHandler # Get simple message processor
from core.utils.tmdbscraper import scraper # Get media art scraper
json_message = MessageHandler()
tmdb = scraper.get_tmdb_scraper()
tmdb = scraper.get_tmdb_scraper()
@app.route('/api/get-background-poster-trailer', methods=['GET', 'POST'])
def getPosterTrailer():
def get_poster_trailer():
if request.method == 'GET':
info = {}
view = get_view()
@ -71,6 +69,8 @@ def getPosterTrailer():
return info
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
@app.route('/backgrounds', methods=['GET', 'POST'])
def backgrounds():
@ -83,10 +83,10 @@ def backgrounds():
return json_message.backgrounds(files)
@app.route('/api/get-thumbnails', methods=['GET', 'POST'])
def getThumbnails():
def get_thumbnails():
if request.method == 'GET':
view = get_view()
return json_message.thumbnails( view.get_video_icons() )
else:
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)

View File

@ -1,42 +1,49 @@
# Python imports
import os
import requests
import uuid
# Lib imports
from flask import make_response
from flask import redirect
from flask import request
from flask import render_template
from flask import session
from flask import send_from_directory
# App imports
# Get from __init__
# Get from __init__
from core import app
from core import db
from core import Favorites
from core import oidc
from core.utils import MessageHandler # Get simple message processor
json_message = MessageHandler()
@app.route('/', methods=['GET', 'POST'])
def home():
if request.method == 'GET':
view = get_view()
sse_id = get_sse_id()
_dot_dots = view.get_dot_dots()
_current_directory = view.get_current_directory()
return render_template('pages/index.html', current_directory = _current_directory, dot_dots = _dot_dots)
response = make_response(
render_template(
'pages/index.html',
current_directory = _current_directory,
dot_dots = _dot_dots
)
)
response.set_cookie('sse_id', sse_id, secure=True, httponly = False)
return response
return render_template('error.html', title = 'Error!',
message = 'Must use GET request type...')
@app.route('/api/list-files/<_hash>', methods=['GET', 'POST'])
def listFiles(_hash = None):
def list_files(_hash = None):
if request.method == 'POST':
view = get_view()
dot_dots = view.get_dot_dots()
@ -70,14 +77,13 @@ def listFiles(_hash = None):
in_fave = "true" if fave else "false"
files.update({'in_fave': in_fave})
return files
else:
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
msg = "Can't manage the request type..."
return json_message.create("danger", msg)
@app.route('/api/file-manager-action/<_type>/<_hash>', methods=['GET', 'POST'])
def fileManagerAction(_type, _hash = None):
def file_manager_action(_type, _hash = None):
view = get_view()
if _type == "reset-path" and _hash == "None":
@ -93,19 +99,16 @@ def fileManagerAction(_type, _hash = None):
if _type == "files":
logger.debug(f"Downloading:\n\tDirectory: {folder}\n\tFile: {file}")
return send_from_directory(directory=folder, filename=file)
if _type == "remux":
# NOTE: Need to actually implimint a websocket to communicate back to client that remux has completed.
# As is, the remux thread hangs until completion and client tries waiting until server reaches connection timeout.
# I.E....this is stupid but for now works better than nothing
good_result = view.remux_video(_hash, fpath)
if good_result:
return '{"path":"static/remuxs/' + _hash + '.mp4"}'
else:
msg = "Remuxing: Remux failed or took too long; please, refresh the page and try again..."
return json_message.create("success", msg)
if _type == "remux":
stream_target = view.remux_video(_hash, fpath)
remux_video(get_sse_id(), _hash, fpath, view)
msg = "Remuxing: Remux process has started..."
return json_message.create("success", msg)
if _type == "stream":
setup_stream(get_sse_id(), _hash, fpath)
msg = "Streaming: Streaming process is being setup..."
return json_message.create("success", msg)
# NOTE: Positionally protecting actions further down that are privlidged
@ -123,3 +126,63 @@ def fileManagerAction(_type, _hash = None):
msg = "Opened media..."
view.open_file_locally(fpath)
return json_message.create("success", msg)
@daemon_threaded
def remux_video(sse_id, hash, path, view):
link = f"https://www.webfm.com/sse/{sse_id}"
body = '{"path":"static/remuxs/' + hash + '.mp4"}'
# good_result = view.remux_video(hash, path)
good_result = view.handbrake_remux_video(hash, path)
if not good_result:
body = json_message.create("warning", "Remuxing: Remux failed...")
requests.post(link, data=body, timeout=10)
def setup_stream(sse_id, hash, path):
link = f"https://www.webfm.com/sse/{sse_id}"
_sub_uuid = uuid.uuid4().hex
_video_path = path
_stub = f"{hash}{_sub_uuid}"
_rtsp_path = f"rtsp://127.0.0.1:8554/{_stub}"
_webrtc_path = f"http://www.{app_name.lower()}.com:8889/{_stub}/"
_stream_target = _rtsp_path
process = get_stream()
if process:
if not kill_stream(process):
msg = "Couldn't stop an existing stream!"
body = json_message.create("danger", msg)
requests.post(link, data=body, timeout=10)
return
stream = get_stream(_video_path, _stream_target)
if stream.poll():
msg = "Streaming: Setting up stream failed! Please try again..."
body = json_message.create("danger", msg)
requests.post(link, data=body, timeout=10)
return
_stream_target = _webrtc_path
body = '{"stream":"' + _stream_target + '"}'
requests.post(link, data=body, timeout=10)
@app.route('/api/stop-current-stream', methods=['GET', 'POST'])
def stop_current_stream():
type = "success"
msg = "Stopped found stream process..."
process = get_stream()
if process:
if not kill_stream(process):
type = "danger"
msg = "Couldn't stop an existing stream!"
else:
type = "warning"
msg = "No stream process found. Nothing to stop..."
return json_message.create(type, msg)

View File

@ -1,15 +1,24 @@
# Python imports
# Lib imports
from flask import request, render_template, flash, redirect, url_for
from flask_login import current_user, login_user, logout_user
from flask import request
from flask import render_template
from flask import flash
from flask import redirect
from flask import url_for
from flask_login import current_user
from flask_login import login_user
from flask_login import logout_user
# App imports
from core import app, bcrypt, db, User, LoginForm
from core.utils import MessageHandler # Get simple message processor
from core import app
from core import bcrypt
from core import db
from core import User
from core import LoginForm
msgHandler = MessageHandler()
@app.route('/app-login', methods=['GET', 'POST'])
def app_login():

View File

@ -1,16 +1,21 @@
# Python imports
# Lib imports
from flask import request, render_template, url_for, redirect, flash
from flask import render_template
from flask import url_for
from flask import redirect
from flask import flash
# App imports
from core import app, bcrypt, db, current_user, RegisterForm # Get from __init__
# Get from __init__
from core import app
from core import bcrypt
from core import db
from core import current_user
from core import RegisterForm
from core.models import User
from core.utils import MessageHandler # Get simple message processor
msgHandler = MessageHandler()
@app.route('/app-register', methods=['GET', 'POST'])
def app_register():

View File

@ -1,17 +1,19 @@
# Python imports
# Lib imports
from flask import request, redirect, flash
from flask import request
from flask import redirect
from flask import flash
# App imports
from ... import app, oidc
from ... import app
from ... import oidc
@app.route('/oidc-login', methods=['GET', 'POST'])
@oidc.require_login
def oidc_login():
print(request)
return redirect("/")

View File

@ -1,15 +1,19 @@
# Python imports
# Lib imports
from flask import request, render_template, url_for, redirect, flash
from flask import request
from flask import render_template
from flask import url_for
from flask import redirect
from flask import flash
# App imports
from ... import app, oidc, db # Get from __init__
from ...utils import MessageHandler # Get simple message processor
# Get from __init__
from ... import app
from ... import oidc
from ... import db
msgHandler = MessageHandler()
@app.route('/oidc-register', methods=['GET', 'POST'])
def oidc_register():

View File

Before

Width:  |  Height:  |  Size: 870 B

After

Width:  |  Height:  |  Size: 870 B

View File

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 367 B

View File

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 626 B

View File

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 711 B

View File

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

View File

Before

Width:  |  Height:  |  Size: 315 B

After

Width:  |  Height:  |  Size: 315 B

View File

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 318 B

View File

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 316 B

View File

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 318 B

View File

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

View File

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

View File

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

View File

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 359 B

View File

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 401 B

View File

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 350 B

View File

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 349 B

View File

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 350 B

View File

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 349 B

View File

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View File

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View File

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View File

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 376 B

View File

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 352 B

View File

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 359 B

View File

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 321 B

View File

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

View File

Before

Width:  |  Height:  |  Size: 377 B

After

Width:  |  Height:  |  Size: 377 B

View File

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 451 B

View File

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 286 B

View File

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 379 B

View File

Before

Width:  |  Height:  |  Size: 453 B

After

Width:  |  Height:  |  Size: 453 B

View File

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 289 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 359 B

View File

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 444 B

View File

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 457 B

View File

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 309 B

View File

Before

Width:  |  Height:  |  Size: 320 B

After

Width:  |  Height:  |  Size: 320 B

View File

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

View File

Before

Width:  |  Height:  |  Size: 453 B

After

Width:  |  Height:  |  Size: 453 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 362 B

After

Width:  |  Height:  |  Size: 362 B

View File

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 445 B

View File

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

View File

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 582 B

View File

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 373 B

View File

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 372 B

View File

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 322 B

View File

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 372 B

View File

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 316 B

View File

Before

Width:  |  Height:  |  Size: 361 B

After

Width:  |  Height:  |  Size: 361 B

View File

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 446 B

View File

Before

Width:  |  Height:  |  Size: 312 B

After

Width:  |  Height:  |  Size: 312 B

View File

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 769 B

View File

Before

Width:  |  Height:  |  Size: 977 B

After

Width:  |  Height:  |  Size: 977 B

View File

Before

Width:  |  Height:  |  Size: 320 B

After

Width:  |  Height:  |  Size: 320 B

View File

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 369 B

View File

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 372 B

View File

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 446 B

View File

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 284 B

View File

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 376 B

View File

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 449 B

View File

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

View File

Before

Width:  |  Height:  |  Size: 315 B

After

Width:  |  Height:  |  Size: 315 B

View File

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 358 B

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

View File

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 309 B

View File

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 463 B

View File

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 462 B

View File

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 499 B

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 730 B

View File

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 484 B

View File

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 358 B

Some files were not shown because too many files have changed in this diff Show More