Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

merge-requests/30/head
Bob Mottram 2020-11-14 22:08:11 +00:00
commit 6c491998a2
728 changed files with 2033 additions and 2353 deletions

View File

@ -250,13 +250,13 @@ If you want to use your own favicon then copy your `favicon.ico` file to the bas
## Adding Themes
If you want to add a new theme then open `theme.py` and add the theme name to the list within `getThemesList`. Add a function with the name `setTheme[YourThemeName]`. Have a look at the other themes to get an idea of how to set the colors and fonts.
If you want to add a new theme then first add the name of your theme to the translations files.
Add the name of your theme to the translations files.
Within the `theme` directory create a directory with the name of your theme and add icons and banners. As a quick way to begin you could copy the contents of `theme/default`, then edit the graphics. Keep the size of images as small as possible to avoid creating a laggy user interface.
Within the `img` directory add a default profile background image called `image_[YourThemeName].png` and a banner image `banner_[YourThemeName].png`. Because the banner image will be reloaded occasionally it should be small - preferably kilobytes rather than megabytes.
On a running instance you can experiment with colors or fonts by editing `epicyon.css` and then reloading the web page. Once you are happy with the results then you can update the changed variable values within your `theme/yourtheme/theme.json` file.
On a running instance you can experiment with colors or fonts by editing `epicyon.css` and then reloading the web page. Once you are happy with the results then you can update the changed values within your `setTheme` function.
Epicyon normally uses one set of CSS files whose variables are then altered per theme. If you want to use entirely bespoke CSS then copy `epicyon-*.css` into your theme directory and edit it to your requirements. This will be used rather than the default CSS files. Be warned that if you're maintaining the CSS files yourself then you may need to keep up with whatever changes are happening upstream, otherwise your user interface will break.
## Running Unit Tests

274
blog.py
View File

@ -10,8 +10,8 @@ import os
from datetime import datetime
from content import replaceEmojiFromTags
from webapp import getIconsDir
from webapp import htmlHeader
from webapp import getIconsWebPath
from webapp import htmlHeaderWithExternalStyle
from webapp import htmlFooter
from webapp_media import addEmbeddedElements
from webapp_utils import getPostAttachmentsAsHtml
@ -381,39 +381,36 @@ def htmlBlogPost(authorized: bool,
cssFilename = baseDir + '/epicyon-blog.css'
if os.path.isfile(baseDir + '/blog.css'):
cssFilename = baseDir + '/blog.css'
with open(cssFilename, 'r') as cssFile:
blogCSS = cssFile.read()
blogStr = htmlHeader(cssFilename, blogCSS)
htmlBlogRemoveCwButton(blogStr, translate)
blogStr = htmlHeaderWithExternalStyle(cssFilename)
htmlBlogRemoveCwButton(blogStr, translate)
blogStr += htmlBlogPostContent(authorized, baseDir,
httpPrefix, translate,
nickname, domain,
domainFull, postJsonObject,
None, False)
blogStr += htmlBlogPostContent(authorized, baseDir,
httpPrefix, translate,
nickname, domain,
domainFull, postJsonObject,
None, False)
# show rss links
iconsDir = getIconsDir(baseDir)
blogStr += '<p class="rssfeed">'
# show rss links
iconsPath = getIconsWebPath(baseDir)
blogStr += '<p class="rssfeed">'
blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.xml">'
blogStr += '<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsDir + '/logorss.png" /></a>'
blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.xml">'
blogStr += '<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsPath + '/logorss.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
# blogStr += '<img style="width:3%;min-width:50px" ' + \
# 'loading="lazy" alt="RSS 3.0" ' + \
# 'title="RSS 3.0" src="/' + \
# iconsDir + '/rss3.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
# blogStr += '<img style="width:3%;min-width:50px" ' + \
# 'loading="lazy" alt="RSS 3.0" ' + \
# 'title="RSS 3.0" src="/' + \
# iconsPath + '/rss3.png" /></a>'
blogStr += '</p>'
blogStr += '</p>'
return blogStr + htmlFooter()
return None
return blogStr + htmlFooter()
def htmlBlogPage(authorized: bool, session,
@ -430,85 +427,81 @@ def htmlBlogPage(authorized: bool, session,
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
blogCSS = cssFile.read()
blogStr = htmlHeader(cssFilename, blogCSS)
htmlBlogRemoveCwButton(blogStr, translate)
blogsIndex = baseDir + '/accounts/' + \
nickname + '@' + domain + '/tlblogs.index'
if not os.path.isfile(blogsIndex):
return blogStr + htmlFooter()
timelineJson = createBlogsTimeline(session, baseDir,
nickname, domain, port,
httpPrefix,
noOfItems, False,
pageNumber)
if not timelineJson:
return blogStr + htmlFooter()
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
# show previous and next buttons
if pageNumber is not None:
iconsDir = getIconsDir(baseDir)
navigateStr = '<p>'
if pageNumber > 1:
# show previous button
navigateStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + \
nickname + '?page=' + str(pageNumber-1) + '">' + \
'<img loading="lazy" alt="<" title="<" ' + \
'src="/' + iconsDir + \
'/prev.png" class="buttonprev"/></a>\n'
if len(timelineJson['orderedItems']) >= noOfItems:
# show next button
navigateStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + \
'?page=' + str(pageNumber + 1) + '">' + \
'<img loading="lazy" alt=">" title=">" ' + \
'src="/' + iconsDir + \
'/prev.png" class="buttonnext"/></a>\n'
navigateStr += '</p>'
blogStr += navigateStr
for item in timelineJson['orderedItems']:
if item['type'] != 'Create':
continue
blogStr += htmlBlogPostContent(authorized, baseDir,
httpPrefix, translate,
nickname, domain,
domainFull, item,
None, True)
if len(timelineJson['orderedItems']) >= noOfItems:
blogStr += navigateStr
# show rss link
blogStr += '<p class="rssfeed">'
blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.xml">'
blogStr += '<img loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsDir + '/logorss.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
# blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
# 'title="RSS 3.0" src="/' + \
# iconsDir + '/rss3.png" /></a>'
blogStr += '</p>'
blogStr = htmlHeaderWithExternalStyle(cssFilename)
htmlBlogRemoveCwButton(blogStr, translate)
blogsIndex = baseDir + '/accounts/' + \
nickname + '@' + domain + '/tlblogs.index'
if not os.path.isfile(blogsIndex):
return blogStr + htmlFooter()
return None
timelineJson = createBlogsTimeline(session, baseDir,
nickname, domain, port,
httpPrefix,
noOfItems, False,
pageNumber)
if not timelineJson:
return blogStr + htmlFooter()
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
# show previous and next buttons
if pageNumber is not None:
iconsPath = getIconsWebPath(baseDir)
navigateStr = '<p>'
if pageNumber > 1:
# show previous button
navigateStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + \
nickname + '?page=' + str(pageNumber-1) + '">' + \
'<img loading="lazy" alt="<" title="<" ' + \
'src="/' + iconsPath + \
'/prev.png" class="buttonprev"/></a>\n'
if len(timelineJson['orderedItems']) >= noOfItems:
# show next button
navigateStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + \
'?page=' + str(pageNumber + 1) + '">' + \
'<img loading="lazy" alt=">" title=">" ' + \
'src="/' + iconsPath + \
'/prev.png" class="buttonnext"/></a>\n'
navigateStr += '</p>'
blogStr += navigateStr
for item in timelineJson['orderedItems']:
if item['type'] != 'Create':
continue
blogStr += htmlBlogPostContent(authorized, baseDir,
httpPrefix, translate,
nickname, domain,
domainFull, item,
None, True)
if len(timelineJson['orderedItems']) >= noOfItems:
blogStr += navigateStr
# show rss link
blogStr += '<p class="rssfeed">'
blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.xml">'
blogStr += '<img loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsPath + '/logorss.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
# blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
# 'title="RSS 3.0" src="/' + \
# iconsPath + '/rss3.png" /></a>'
blogStr += '</p>'
return blogStr + htmlFooter()
def htmlBlogPageRSS2(authorized: bool, session,
@ -678,40 +671,37 @@ def htmlBlogView(authorized: bool,
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
blogCSS = cssFile.read()
blogStr = htmlHeader(cssFilename, blogCSS)
blogStr = htmlHeaderWithExternalStyle(cssFilename)
if noOfBlogAccounts(baseDir) <= 1:
nickname = singleBlogAccountNickname(baseDir)
if nickname:
return htmlBlogPage(authorized, session,
baseDir, httpPrefix, translate,
nickname, domain, port,
noOfItems, 1)
if noOfBlogAccounts(baseDir) <= 1:
nickname = singleBlogAccountNickname(baseDir)
if nickname:
return htmlBlogPage(authorized, session,
baseDir, httpPrefix, translate,
nickname, domain, port,
noOfItems, 1)
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for acct in dirs:
if '@' not in acct:
continue
if 'inbox@' in acct:
continue
accountDir = os.path.join(baseDir + '/accounts', acct)
blogsIndex = accountDir + '/tlblogs.index'
if os.path.isfile(blogsIndex):
blogStr += '<p class="blogaccount">'
blogStr += '<a href="' + \
httpPrefix + '://' + domainFull + '/blog/' + \
acct.split('@')[0] + '">' + acct + '</a>'
blogStr += '</p>'
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for acct in dirs:
if '@' not in acct:
continue
if 'inbox@' in acct:
continue
accountDir = os.path.join(baseDir + '/accounts', acct)
blogsIndex = accountDir + '/tlblogs.index'
if os.path.isfile(blogsIndex):
blogStr += '<p class="blogaccount">'
blogStr += '<a href="' + \
httpPrefix + '://' + domainFull + '/blog/' + \
acct.split('@')[0] + '">' + acct + '</a>'
blogStr += '</p>'
return blogStr + htmlFooter()
return None
return blogStr + htmlFooter()
def htmlEditBlog(mediaInstance: bool, translate: {},
@ -732,7 +722,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
print('Edit blog: json not loaded for ' + postFilename)
return None
iconsDir = getIconsDir(baseDir)
iconsPath = getIconsWebPath(baseDir)
editBlogText = '<p class="new-post-text">' + \
translate['Write your post text below.'] + '</p>'
@ -744,10 +734,6 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
editBlogCSS = cssFile.read()
if httpPrefix != 'https':
editBlogCSS = editBlogCSS.replace('https://', httpPrefix+'://')
if '?' in path:
path = path.split('?')[0]
@ -781,7 +767,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
dateAndLocation += \
'<p><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>'
iconsPath + '/calendar.png"/>'
dateAndLocation += \
'<label class="labels">' + translate['Date'] + ': </label>'
dateAndLocation += '<input type="date" name="eventDate">'
@ -794,7 +780,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
dateAndLocation += '<input type="text" name="location">'
dateAndLocation += '</div>'
editBlogForm = htmlHeader(cssFilename, editBlogCSS)
editBlogForm = htmlHeaderWithExternalStyle(cssFilename)
editBlogForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
@ -812,7 +798,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {},
editBlogForm += ' <div class="dropbtn">'
editBlogForm += \
' <img loading="lazy" alt="" title="" src="/' + iconsDir + \
' <img loading="lazy" alt="" title="" src="/' + iconsPath + \
'/' + scopeIcon + '"/><b class="scope-desc">' + \
scopeDescription + '</b>'
editBlogForm += ' </div>'

View File

@ -159,6 +159,7 @@ def dangerousMarkup(content: str) -> bool:
if '>' not in content:
return False
contentSections = content.split('<')
invalidPartials = ('127.0.', '192.168', '10.0.')
invalidStrings = ('script', 'canvas', 'style', 'abbr',
'frame', 'iframe', 'html', 'body',
'hr')
@ -166,6 +167,9 @@ def dangerousMarkup(content: str) -> bool:
if '>' not in markup:
continue
markup = markup.split('>')[0].strip()
for partialMatch in invalidPartials:
if partialMatch in markup:
return True
if ' ' not in markup:
for badStr in invalidStrings:
if badStr in markup:

150
daemon.py
View File

@ -166,6 +166,7 @@ from shares import getSharesFeedForPerson
from shares import addShare
from shares import removeShare
from shares import expireShares
from utils import mediaFileMimeType
from utils import getCSS
from utils import firstParagraphFromString
from utils import clearFromPostCaches
@ -2441,7 +2442,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.debug,
self.server.projectVersion,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly)
self.server.showPublishedDateOnly,
self.server.defaultTimeline)
if profileStr:
msg = profileStr.encode('utf-8')
self._login_headers('text/html',
@ -3408,6 +3410,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
adminNickname = getConfigParam(self.server.baseDir, 'admin')
# get the various avatar, banner and background images
actorChanged = True
profileMediaTypes = ('avatar', 'image',
@ -3416,6 +3420,13 @@ class PubServer(BaseHTTPRequestHandler):
'left_col_image', 'right_col_image')
profileMediaTypesUploaded = {}
for mType in profileMediaTypes:
# some images can only be changed by the admin
if mType == 'instanceLogo':
if nickname != adminNickname:
print('WARN: only the admin can change ' +
'instance logo')
continue
if debug:
print('DEBUG: profile update extracting ' + mType +
' image or font from POST')
@ -4371,12 +4382,16 @@ class PubServer(BaseHTTPRequestHandler):
if 'image/avif' in self.headers['Accept']:
favType = 'image/avif'
favFilename = 'favicon.avif'
themeName = getConfigParam(baseDir, 'theme')
if not themeName:
themeName = 'default'
# custom favicon
faviconFilename = baseDir + '/' + favFilename
faviconFilename = \
baseDir + '/theme/' + themeName + '/icons/' + favFilename
if not os.path.isfile(faviconFilename):
# default favicon
faviconFilename = \
baseDir + '/img/icons/' + favFilename
baseDir + '/theme/default/icons/' + favFilename
if self._etag_exists(faviconFilename):
# The file has not changed
if debug:
@ -4764,25 +4779,7 @@ class PubServer(BaseHTTPRequestHandler):
self._304()
return
mediaFileType = 'image/png'
if mediaFilename.endswith('.png'):
mediaFileType = 'image/png'
elif mediaFilename.endswith('.jpg'):
mediaFileType = 'image/jpeg'
elif mediaFilename.endswith('.gif'):
mediaFileType = 'image/gif'
elif mediaFilename.endswith('.webp'):
mediaFileType = 'image/webp'
elif mediaFilename.endswith('.avif'):
mediaFileType = 'image/avif'
elif mediaFilename.endswith('.mp4'):
mediaFileType = 'video/mp4'
elif mediaFilename.endswith('.ogv'):
mediaFileType = 'video/ogv'
elif mediaFilename.endswith('.mp3'):
mediaFileType = 'audio/mpeg'
elif mediaFilename.endswith('.ogg'):
mediaFileType = 'audio/ogg'
mediaFileType = mediaFileMimeType(mediaFilename)
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
@ -4841,7 +4838,13 @@ class PubServer(BaseHTTPRequestHandler):
"""
if path.endswith('.png'):
mediaStr = path.split('/icons/')[1]
mediaFilename = baseDir + '/img/icons/' + mediaStr
if '/' not in mediaStr:
self._404()
return
theme = mediaStr.split('/')[0]
iconFilename = mediaStr.split('/')[1]
mediaFilename = \
baseDir + '/theme/' + theme + '/icons/' + iconFilename
if self._etag_exists(mediaFilename):
# The file has not changed
self._304()
@ -4849,7 +4852,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.iconsCache.get(mediaStr):
mediaBinary = self.server.iconsCache[mediaStr]
self._set_headers_etag(mediaFilename,
'image/png',
mediaFileMimeType(mediaFilename),
mediaBinary, None,
callingDomain)
self._write(mediaBinary)
@ -4858,8 +4861,9 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isfile(mediaFilename):
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
mimeType = mediaFileMimeType(mediaFilename)
self._set_headers_etag(mediaFilename,
'image/png',
mimeType,
mediaBinary, None,
callingDomain)
self._write(mediaBinary)
@ -4883,39 +4887,11 @@ class PubServer(BaseHTTPRequestHandler):
return
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
if mediaFilename.endswith('.png'):
self._set_headers_etag(mediaFilename,
'image/png',
mediaBinary, None,
callingDomain)
elif mediaFilename.endswith('.jpg'):
self._set_headers_etag(mediaFilename,
'image/jpeg',
mediaBinary, None,
callingDomain)
elif mediaFilename.endswith('.gif'):
self._set_headers_etag(mediaFilename,
'image/gif',
mediaBinary, None,
callingDomain)
elif mediaFilename.endswith('.webp'):
self._set_headers_etag(mediaFilename,
'image/webp',
mediaBinary, None,
callingDomain)
elif mediaFilename.endswith('.avif'):
self._set_headers_etag(mediaFilename,
'image/avif',
mediaBinary, None,
callingDomain)
else:
# default to jpeg
self._set_headers_etag(mediaFilename,
'image/jpeg',
mediaBinary, None,
callingDomain)
# self._404()
return
mimeType = mediaFileMimeType(mediaFilename)
self._set_headers_etag(mediaFilename,
mimeType,
mediaBinary, None,
callingDomain)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done',
@ -8327,7 +8303,8 @@ class PubServer(BaseHTTPRequestHandler):
time.sleep(1)
tries += 1
if mediaBinary:
self._set_headers_etag(qrFilename, 'image/png',
mimeType = mediaFileMimeType(qrFilename)
self._set_headers_etag(qrFilename, mimeType,
mediaBinary, None,
callingDomain)
self._write(mediaBinary)
@ -8365,7 +8342,8 @@ class PubServer(BaseHTTPRequestHandler):
time.sleep(1)
tries += 1
if mediaBinary:
self._set_headers_etag(bannerFilename, 'image/png',
mimeType = mediaFileMimeType(bannerFilename)
self._set_headers_etag(bannerFilename, mimeType,
mediaBinary, None,
callingDomain)
self._write(mediaBinary)
@ -8406,7 +8384,8 @@ class PubServer(BaseHTTPRequestHandler):
time.sleep(1)
tries += 1
if mediaBinary:
self._set_headers_etag(bannerFilename, 'image/png',
mimeType = mediaFileMimeType(bannerFilename)
self._set_headers_etag(bannerFilename, mimeType,
mediaBinary, None,
callingDomain)
self._write(mediaBinary)
@ -8955,6 +8934,13 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings,
'create session', 'hasAccept')
# get css
# Note that this comes before the busy flag to avoid conflicts
if self.path.endswith('.css'):
if self._getStyleSheet(callingDomain, self.path,
GETstartTime, GETtimings):
return
# get fonts
if '/fonts/' in self.path:
self._getFonts(callingDomain, self.path,
@ -9316,17 +9302,6 @@ class PubServer(BaseHTTPRequestHandler):
'robots txt',
'show login screen done')
# get css
# Note that this comes before the busy flag to avoid conflicts
if self.path.endswith('.css'):
if self._getStyleSheet(callingDomain, self.path,
GETstartTime, GETtimings):
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show login screen done',
'profile.css done')
# manifest images used to create a home screen icon
# when selecting "add to home screen" in browsers
# which support progressive web apps
@ -9358,8 +9333,8 @@ class PubServer(BaseHTTPRequestHandler):
time.sleep(1)
tries += 1
if mediaBinary:
self._set_headers_etag(mediaFilename,
'image/png',
mimeType = mediaFileMimeType(mediaFilename)
self._set_headers_etag(mediaFilename, mimeType,
mediaBinary, cookie,
callingDomain)
self._write(mediaBinary)
@ -9398,8 +9373,8 @@ class PubServer(BaseHTTPRequestHandler):
time.sleep(1)
tries += 1
if mediaBinary:
self._set_headers_etag(screenFilename,
'image/png',
mimeType = mediaFileMimeType(screenFilename)
self._set_headers_etag(screenFilename, mimeType,
mediaBinary, cookie,
callingDomain)
self._write(mediaBinary)
@ -9443,7 +9418,7 @@ class PubServer(BaseHTTPRequestHandler):
tries += 1
if mediaBinary:
self._set_headers_etag(iconFilename,
'image/png',
mediaFileMimeType(iconFilename),
mediaBinary, cookie,
callingDomain)
self._write(mediaBinary)
@ -10815,26 +10790,7 @@ class PubServer(BaseHTTPRequestHandler):
except BaseException:
pass
mediaFileType = 'application/json'
if checkPath.endswith('.png'):
mediaFileType = 'image/png'
elif checkPath.endswith('.jpg'):
mediaFileType = 'image/jpeg'
elif checkPath.endswith('.gif'):
mediaFileType = 'image/gif'
elif checkPath.endswith('.webp'):
mediaFileType = 'image/webp'
elif checkPath.endswith('.avif'):
mediaFileType = 'image/avif'
elif checkPath.endswith('.mp4'):
mediaFileType = 'video/mp4'
elif checkPath.endswith('.ogv'):
mediaFileType = 'video/ogv'
elif checkPath.endswith('.mp3'):
mediaFileType = 'audio/mpeg'
elif checkPath.endswith('.ogg'):
mediaFileType = 'audio/ogg'
mediaFileType = mediaFileMimeType(checkPath)
self._set_headers_head(mediaFileType, fileLength,
etag, callingDomain)

View File

@ -15,9 +15,12 @@
--title-background: #ccc;
--focus-color: white;
--calendar-horizontal-padding: 0;
--calendar-cell-size: 1.5vw;
--calendar-cell-size-mobile: 1.5vw;
--font-size-calendar-header: 3rem;
--font-size-calendar-day: 1rem;
--font-size-calendar-cell: 4rem;
--font-size-calendar-cell: 2rem;
--font-size-calendar-cell-mobile: 4rem;
}
@font-face {
@ -38,15 +41,8 @@
body {
background-color: var(--main-bg-color);
color: var(--day-number2);
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
font-family: 'Montserrat';
font-weight: 700;
margin: 5%;
}
main {
@ -55,12 +51,6 @@ main {
flex-basis: 980px;
}
.calendar {
table-display: fixed;
width: 100%;
padding: 0 var(--calendar-horizontal-padding);
}
a:visited{
color: var(--day-number);
text-decoration: none;
@ -97,11 +87,6 @@ a:focus {
border-right: none;
}
.calendar__day__header,
.calendar__day__cell {
padding: .75rem 0 1.5rem;
}
.calendar__banner--month {
text-align: center;
padding: .75rem;
@ -123,11 +108,6 @@ a:focus {
text-transform: uppercase;
}
.calendar__day__cell {
font-size: var(--font-size-calendar-cell);
position: relative;
}
.year {
font-size: 30px;
}
@ -210,3 +190,37 @@ tr:nth-child(even) > .calendar__day__cell:nth-child(even) {
-ms-transform: translateY(30%) scaleX(-1);
transform: translateY(30%) scaleX(-1);
}
@media screen and (min-width: 400px) {
.calendar {
table-display: fixed;
margin: 0 20%;
width: 60%;
padding: 0 var(--calendar-horizontal-padding);
}
.calendar__day__cell {
font-size: var(--font-size-calendar-cell);
position: relative;
}
.calendar__day__header,
.calendar__day__cell {
padding: var(--calendar-cell-size) 0 var(--calendar-cell-size);
}
}
@media screen and (max-width: 1000px) {
.calendar {
table-display: fixed;
margin: 0 0;
width: 100%;
padding: 0 var(--calendar-horizontal-padding);
}
.calendar__day__cell {
font-size: var(--font-size-calendar-cell-mobile);
position: relative;
}
.calendar__day__header,
.calendar__day__cell {
padding: var(--calendar-cell-size-mobile) 0 var(--calendar-cell-size-mobile);
}
}

View File

@ -39,9 +39,9 @@
--font-size-likes: 20px;
--font-size-likes-mobile: 32px;
--font-size-pgp-key: 16px;
--font-size-pgp-key2: 8px;
--font-size-pgp-key2: 18px;
--font-size-tox: 16px;
--font-size-tox2: 8px;
--font-size-tox2: 18px;
--time-color: #aaa;
--time-vertical-align: 4px;
--time-vertical-align-mobile: 25px;
@ -130,6 +130,8 @@
--post-separator-width: 95%;
--post-separator-height: 1px;
--header-vertical-offset: 0;
--profile-background-height: 25vw;
--profile-text-align: left;
}
@font-face {
@ -251,13 +253,61 @@ a:focus {
border: 2px solid var(--focus-color);
}
.hero-image {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("image.png");
height: 50%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
.profileHeader {
font-family: Arial, Helvetica, sans-serif;
position: relative;
overflow: hidden;
margin: 10px;
width: 100%;
color: #ffffff;
text-align: var(--profile-text-align);
line-height: 1.4em;
background-color: #141414;
}
.profileHeader .title {
border-radius: 50%;
position: absolute;
bottom: 100%;
left: 25px;
z-index: 1;
max-width: 20%;
opacity: 1;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
}
.profileHeader * {
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: all 0.25s ease;
transition: all 0.25s ease;
}
.profileHeader img.profileBackground {
object-fit: cover;
max-height: var(--profile-background-height);
width: 100%;
vertical-align: top;
}
.profileHeader figcaption {
width: 100%;
background-color: var(--main-bg-color);
color: var(--main-fg-color);
padding: 25px;
position: relative;
}
.profileHeader figcaption:before {
position: absolute;
content: '';
bottom: 100%;
left: 0;
width: 0;
height: 0;
border-style: solid;
border-width: 55px 0 0 400px;
border-color: transparent transparent transparent var(--main-bg-color);
}
.rssfeed img {
@ -292,10 +342,6 @@ a:focus {
transform: translateY(30%) scaleX(-1);
}
.hero-image img {
width: 50%;
}
.new-post-text {
font-size: var(--font-size2);
font-family: Arial, Helvetica, sans-serif;
@ -323,30 +369,6 @@ a:focus {
background-color: var(--main-bg-color);
}
.hero-text img.emoji {
width: 50px;
padding: 0 0;
margin: 0 0;
float: none;
}
.hero-text button {
border: none;
outline: 0;
display: inline-block;
padding: 10px 25px;
color: black;
background-color: #ddd;
text-align: center;
cursor: pointer;
font-family: Arial, Helvetica, sans-serif;
}
.hero-text button:hover {
background-color: var(--button-background);
color: var(--button-text);
}
.timelineIcon {
width: 10%;
}
@ -487,16 +509,6 @@ a:focus {
vertical-align: middle;
}
.hero-text img.emojiprofile {
float: none;
width: 50px;
margin-left: 0px;
margin-right: 0px;
padding-right: 0px;
border-radius: 0px;
vertical-align: middle;
}
.containericons {
padding: 0px 0px;
margin: 0px var(--containericons-horizontal-offset);
@ -980,8 +992,9 @@ div.container {
display: none;
}
.timeline-banner {
object-fit: cover;
width: 98vw;
height: var(--banner-height);
max-height: var(--banner-height);
}
.timeline {
border: 0;
@ -1481,26 +1494,6 @@ div.container {
font-family: Arial, Helvetica, sans-serif;
opacity: 0.7;
}
.hero-text {
text-align: center;
position: absolute;
top: 50%;
left: 50%;
width: 70%;
transform: translate(-50%, -50%);
color: var(--font-color-header);
font-size: var(--font-size-header);
font-family: Arial, Helvetica, sans-serif;
}
.hero-text img.qrcode {
border-radius: 1%;
width: 5%;
min-width: 20px;
}
.hero-text img.title {
border-radius: 1%;
width: 15%;
}
input[type=checkbox]
{
-ms-transform: scale(2);
@ -1651,8 +1644,9 @@ div.container {
display: inline;
}
.timeline-banner {
object-fit: cover;
width: 98vw;
height: var(--banner-height-mobile);
max-height: var(--banner-height-mobile);
}
.timeline {
border: 0;
@ -2114,26 +2108,6 @@ div.container {
font-family: Arial, Helvetica, sans-serif;
opacity: 0.7;
}
.hero-text {
text-align: center;
position: absolute;
top: 50%;
left: 50%;
width: 70%;
transform: translate(-50%, -50%);
color: var(--font-color-header);
font-size: var(--font-size-header-mobile);
font-family: Arial, Helvetica, sans-serif;
}
.hero-text img.qrcode {
border-radius: 1%;
width: 15%;
min-width: 20px;
}
.hero-text img.title {
border-radius: 1%;
width: 25%;
}
input[type=checkbox]
{
-ms-transform: scale(4);

View File

@ -193,8 +193,9 @@ input[type=text] {
@media screen and (min-width: 400px) {
.timeline-banner {
object-fit: cover;
width: 98vw;
height: var(--search-banner-height);
max-height: var(--search-banner-height);
}
.hashtagswarm {
font-size: var(--hashtag-size1);
@ -232,20 +233,21 @@ input[type=text] {
}
input[type=checkbox]
{
-ms-transform: scale(2);
-moz-transform: scale(2);
-webkit-transform: scale(2);
-o-transform: scale(2);
transform: scale(2);
padding: 10px;
margin: 20px 30px;
-ms-transform: scale(2);
-moz-transform: scale(2);
-webkit-transform: scale(2);
-o-transform: scale(2);
transform: scale(2);
padding: 10px;
margin: 20px 30px;
}
}
@media screen and (max-width: 1000px) {
.timeline-banner {
object-fit: cover;
width: 98vw;
height: var(--search-banner-height-mobile);
max-height: var(--search-banner-height-mobile);
}
.hashtagswarm {
font-size: var(--hashtag-size2);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

View File

@ -57,7 +57,7 @@ from posts import isImageMedia
from posts import sendSignedJson
from posts import sendToFollowersThread
from webapp import individualPostAsHtml
from webapp import getIconsDir
from webapp import getIconsWebPath
from question import questionUpdateVotes
from media import replaceYouTube
from git import isGitPatch
@ -139,7 +139,7 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int,
if boxname != 'tlevents' and boxname != 'outbox':
boxname = 'inbox'
individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
getIconsDir(baseDir), translate, pageNumber,
getIconsWebPath(baseDir), translate, pageNumber,
baseDir, session, cachedWebfingers, personCache,
nickname, domain, port, postJsonObject,
avatarUrl, True, allowDeletion,

View File

@ -483,17 +483,19 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
baseDir + '/accounts/' + nickname + '@' + domain +
'/avatar.png')
theme = getConfigParam(baseDir, 'theme')
defaultProfileImageFilename = baseDir + '/img/image.png'
if not theme:
theme = 'default'
defaultProfileImageFilename = baseDir + '/theme/default/image.png'
if theme:
if os.path.isfile(baseDir + '/img/image_' + theme + '.png'):
defaultBannerFilename = baseDir + '/img/image_' + theme + '.png'
if os.path.isfile(baseDir + '/theme/' + theme + '/image.png'):
defaultBannerFilename = baseDir + '/theme/' + theme + '/image.png'
if os.path.isfile(defaultProfileImageFilename):
copyfile(defaultProfileImageFilename, baseDir +
'/accounts/' + nickname + '@' + domain + '/image.png')
defaultBannerFilename = baseDir + '/img/banner.png'
defaultBannerFilename = baseDir + '/theme/default/banner.png'
if theme:
if os.path.isfile(baseDir + '/img/banner_' + theme + '.png'):
defaultBannerFilename = baseDir + '/img/banner_' + theme + '.png'
if os.path.isfile(baseDir + '/theme/' + theme + '/banner.png'):
defaultBannerFilename = baseDir + '/theme/' + theme + '/banner.png'
if os.path.isfile(defaultBannerFilename):
copyfile(defaultBannerFilename, baseDir + '/accounts/' +
nickname + '@' + domain + '/banner.png')

View File

@ -1943,32 +1943,52 @@ def testDangerousMarkup():
print('testDangerousMarkup')
content = '<p>This is a valid message</p>'
assert(not dangerousMarkup(content))
content = 'This is a valid message without markup'
assert(not dangerousMarkup(content))
content = '<p>This is a valid-looking message. But wait... ' + \
'<script>document.getElementById("concentrated")' + \
'.innerHTML = "evil";</script></p>'
assert(dangerousMarkup(content))
content = '<p>This is a valid-looking message. But wait... ' + \
'<script src="https://evilsite/payload.js" /></p>'
assert(dangerousMarkup(content))
content = '<p>This message embeds an evil frame.' + \
'<iframe src="somesite"></iframe></p>'
assert(dangerousMarkup(content))
content = '<p>This message tries to obfuscate an evil frame.' + \
'< iframe src = "somesite"></ iframe ></p>'
assert(dangerousMarkup(content))
content = '<p>This message is not necessarily evil, but annoying.' + \
'<hr><br><br><br><br><br><br><br><hr><hr></p>'
assert(dangerousMarkup(content))
content = '<p>This message contans a ' + \
'<a href="https://validsite/index.html">valid link.</a></p>'
assert(not dangerousMarkup(content))
content = '<p>This message contans a ' + \
'<a href="https://validsite/iframe.html">' + \
'valid link having invalid but harmless name.</a></p>'
assert(not dangerousMarkup(content))
content = '<p>This message which <a href="127.0.0.1:8736">' + \
'tries to access the local network</a></p>'
assert(dangerousMarkup(content))
content = '<p>This message which <a href="http://192.168.5.10:7235">' + \
'tries to access the local network</a></p>'
assert(dangerousMarkup(content))
content = '<p>127.0.0.1 This message which does not access ' + \
'the local network</a></p>'
assert(not dangerousMarkup(content))
def runHtmlReplaceQuoteMarks():
print('htmlReplaceQuoteMarks')

1034
theme.py

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 978 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -0,0 +1,31 @@
{
"newswire-publish-icon": "True",
"full-width-timeline-buttons": "False",
"icons-as-buttons": "False",
"rss-icon-at-top": "True",
"publish-button-at-top": "False",
"banner-height": "20vh",
"banner-height-mobile": "10vh",
"newswire-date-color": "blue",
"font-size-header": "22px",
"font-size-header-mobile": "32px",
"font-size": "45px",
"font-size2": "45px",
"font-size3": "45px",
"font-size4": "35px",
"font-size5": "29px",
"gallery-font-size": "35px",
"gallery-font-size-mobile": "55px",
"main-bg-color": "#002365",
"column-left-color": "#002365",
"text-entry-background": "#002365",
"link-bg-color": "#002365",
"main-bg-color-reply": "#002365",
"main-bg-color-report": "#002365",
"day-number2": "#002365",
"hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px",
"time-vertical-align": "-10px",
"*font-family": "'Domestic_Manners'",
"*src": "url('./fonts/Domestic_Manners.woff2') format('woff2')"
}

View File

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 981 B

View File

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 978 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 864 B

After

Width:  |  Height:  |  Size: 864 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

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