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

main
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 ## 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 ## Running Unit Tests

274
blog.py
View File

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

View File

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

150
daemon.py
View File

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

View File

@ -15,9 +15,12 @@
--title-background: #ccc; --title-background: #ccc;
--focus-color: white; --focus-color: white;
--calendar-horizontal-padding: 0; --calendar-horizontal-padding: 0;
--calendar-cell-size: 1.5vw;
--calendar-cell-size-mobile: 1.5vw;
--font-size-calendar-header: 3rem; --font-size-calendar-header: 3rem;
--font-size-calendar-day: 1rem; --font-size-calendar-day: 1rem;
--font-size-calendar-cell: 4rem; --font-size-calendar-cell: 2rem;
--font-size-calendar-cell-mobile: 4rem;
} }
@font-face { @font-face {
@ -38,15 +41,8 @@
body { body {
background-color: var(--main-bg-color); background-color: var(--main-bg-color);
color: var(--day-number2); 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-family: 'Montserrat';
font-weight: 700; font-weight: 700;
margin: 5%;
} }
main { main {
@ -55,12 +51,6 @@ main {
flex-basis: 980px; flex-basis: 980px;
} }
.calendar {
table-display: fixed;
width: 100%;
padding: 0 var(--calendar-horizontal-padding);
}
a:visited{ a:visited{
color: var(--day-number); color: var(--day-number);
text-decoration: none; text-decoration: none;
@ -97,11 +87,6 @@ a:focus {
border-right: none; border-right: none;
} }
.calendar__day__header,
.calendar__day__cell {
padding: .75rem 0 1.5rem;
}
.calendar__banner--month { .calendar__banner--month {
text-align: center; text-align: center;
padding: .75rem; padding: .75rem;
@ -123,11 +108,6 @@ a:focus {
text-transform: uppercase; text-transform: uppercase;
} }
.calendar__day__cell {
font-size: var(--font-size-calendar-cell);
position: relative;
}
.year { .year {
font-size: 30px; font-size: 30px;
} }
@ -210,3 +190,37 @@ tr:nth-child(even) > .calendar__day__cell:nth-child(even) {
-ms-transform: translateY(30%) scaleX(-1); -ms-transform: translateY(30%) scaleX(-1);
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: 20px;
--font-size-likes-mobile: 32px; --font-size-likes-mobile: 32px;
--font-size-pgp-key: 16px; --font-size-pgp-key: 16px;
--font-size-pgp-key2: 8px; --font-size-pgp-key2: 18px;
--font-size-tox: 16px; --font-size-tox: 16px;
--font-size-tox2: 8px; --font-size-tox2: 18px;
--time-color: #aaa; --time-color: #aaa;
--time-vertical-align: 4px; --time-vertical-align: 4px;
--time-vertical-align-mobile: 25px; --time-vertical-align-mobile: 25px;
@ -130,6 +130,8 @@
--post-separator-width: 95%; --post-separator-width: 95%;
--post-separator-height: 1px; --post-separator-height: 1px;
--header-vertical-offset: 0; --header-vertical-offset: 0;
--profile-background-height: 25vw;
--profile-text-align: left;
} }
@font-face { @font-face {
@ -251,13 +253,61 @@ a:focus {
border: 2px solid var(--focus-color); border: 2px solid var(--focus-color);
} }
.hero-image { .profileHeader {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("image.png"); font-family: Arial, Helvetica, sans-serif;
height: 50%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
position: relative; 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 { .rssfeed img {
@ -292,10 +342,6 @@ a:focus {
transform: translateY(30%) scaleX(-1); transform: translateY(30%) scaleX(-1);
} }
.hero-image img {
width: 50%;
}
.new-post-text { .new-post-text {
font-size: var(--font-size2); font-size: var(--font-size2);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
@ -323,30 +369,6 @@ a:focus {
background-color: var(--main-bg-color); 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 { .timelineIcon {
width: 10%; width: 10%;
} }
@ -487,16 +509,6 @@ a:focus {
vertical-align: middle; 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 { .containericons {
padding: 0px 0px; padding: 0px 0px;
margin: 0px var(--containericons-horizontal-offset); margin: 0px var(--containericons-horizontal-offset);
@ -980,8 +992,9 @@ div.container {
display: none; display: none;
} }
.timeline-banner { .timeline-banner {
object-fit: cover;
width: 98vw; width: 98vw;
height: var(--banner-height); max-height: var(--banner-height);
} }
.timeline { .timeline {
border: 0; border: 0;
@ -1481,26 +1494,6 @@ div.container {
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
opacity: 0.7; 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] input[type=checkbox]
{ {
-ms-transform: scale(2); -ms-transform: scale(2);
@ -1651,8 +1644,9 @@ div.container {
display: inline; display: inline;
} }
.timeline-banner { .timeline-banner {
object-fit: cover;
width: 98vw; width: 98vw;
height: var(--banner-height-mobile); max-height: var(--banner-height-mobile);
} }
.timeline { .timeline {
border: 0; border: 0;
@ -2114,26 +2108,6 @@ div.container {
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
opacity: 0.7; 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] input[type=checkbox]
{ {
-ms-transform: scale(4); -ms-transform: scale(4);

View File

@ -193,8 +193,9 @@ input[type=text] {
@media screen and (min-width: 400px) { @media screen and (min-width: 400px) {
.timeline-banner { .timeline-banner {
object-fit: cover;
width: 98vw; width: 98vw;
height: var(--search-banner-height); max-height: var(--search-banner-height);
} }
.hashtagswarm { .hashtagswarm {
font-size: var(--hashtag-size1); font-size: var(--hashtag-size1);
@ -232,20 +233,21 @@ input[type=text] {
} }
input[type=checkbox] input[type=checkbox]
{ {
-ms-transform: scale(2); -ms-transform: scale(2);
-moz-transform: scale(2); -moz-transform: scale(2);
-webkit-transform: scale(2); -webkit-transform: scale(2);
-o-transform: scale(2); -o-transform: scale(2);
transform: scale(2); transform: scale(2);
padding: 10px; padding: 10px;
margin: 20px 30px; margin: 20px 30px;
} }
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
.timeline-banner { .timeline-banner {
object-fit: cover;
width: 98vw; width: 98vw;
height: var(--search-banner-height-mobile); max-height: var(--search-banner-height-mobile);
} }
.hashtagswarm { .hashtagswarm {
font-size: var(--hashtag-size2); 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 sendSignedJson
from posts import sendToFollowersThread from posts import sendToFollowersThread
from webapp import individualPostAsHtml from webapp import individualPostAsHtml
from webapp import getIconsDir from webapp import getIconsWebPath
from question import questionUpdateVotes from question import questionUpdateVotes
from media import replaceYouTube from media import replaceYouTube
from git import isGitPatch from git import isGitPatch
@ -139,7 +139,7 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int,
if boxname != 'tlevents' and boxname != 'outbox': if boxname != 'tlevents' and boxname != 'outbox':
boxname = 'inbox' boxname = 'inbox'
individualPostAsHtml(True, recentPostsCache, maxRecentPosts, individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
getIconsDir(baseDir), translate, pageNumber, getIconsWebPath(baseDir), translate, pageNumber,
baseDir, session, cachedWebfingers, personCache, baseDir, session, cachedWebfingers, personCache,
nickname, domain, port, postJsonObject, nickname, domain, port, postJsonObject,
avatarUrl, True, allowDeletion, avatarUrl, True, allowDeletion,

View File

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

View File

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