diff --git a/README.md b/README.md index 548807b5b..f4ee63e46 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/blog.py b/blog.py index 9963ae0ea..ca6d5a6a2 100644 --- a/blog.py +++ b/blog.py @@ -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 += '

' + # show rss links + iconsPath = getIconsWebPath(baseDir) + blogStr += '

' - blogStr += '' - blogStr += 'RSS 2.0' + blogStr += '' + blogStr += 'RSS 2.0' - # blogStr += '' - # blogStr += 'RSS 3.0' + # blogStr += '' + # blogStr += 'RSS 3.0' - blogStr += '

' + blogStr += '

' - 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 = '

' - if pageNumber > 1: - # show previous button - navigateStr += '' + \ - '<\n' - if len(timelineJson['orderedItems']) >= noOfItems: - # show next button - navigateStr += '' + \ - '>\n' - navigateStr += '

' - 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 += '

' - - blogStr += '' - blogStr += 'RSS 2.0' - - # blogStr += '' - # blogStr += 'RSS 3.0' - - blogStr += '

' + 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 = '

' + if pageNumber > 1: + # show previous button + navigateStr += '' + \ + '<\n' + if len(timelineJson['orderedItems']) >= noOfItems: + # show next button + navigateStr += '' + \ + '>\n' + navigateStr += '

' + 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 += '

' + + blogStr += '' + blogStr += 'RSS 2.0' + + # blogStr += '' + # blogStr += 'RSS 3.0' + + blogStr += '

' + 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 += '

' - blogStr += '' + acct + '' - blogStr += '

' + 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 += '

' + blogStr += '' + acct + '' + blogStr += '

' - 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 = '

' + \ translate['Write your post text below.'] + '

' @@ -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 += \ '

' + iconsPath + '/calendar.png"/>' dateAndLocation += \ '' dateAndLocation += '' @@ -794,7 +780,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {}, dateAndLocation += '' dateAndLocation += '' - editBlogForm = htmlHeader(cssFilename, editBlogCSS) + editBlogForm = htmlHeaderWithExternalStyle(cssFilename) editBlogForm += \ '

' + \ scopeDescription + '' editBlogForm += ' ' diff --git a/content.py b/content.py index 95181a865..532668ffe 100644 --- a/content.py +++ b/content.py @@ -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: diff --git a/daemon.py b/daemon.py index f3ffefb95..3a7fb640b 100644 --- a/daemon.py +++ b/daemon.py @@ -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) diff --git a/epicyon-calendar.css b/epicyon-calendar.css index 8db0a6b54..989f08adb 100644 --- a/epicyon-calendar.css +++ b/epicyon-calendar.css @@ -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); + } +} diff --git a/epicyon-profile.css b/epicyon-profile.css index 10495edb3..c93286079 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -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); diff --git a/epicyon-search.css b/epicyon-search.css index 243758e9e..0b04e0458 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -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); diff --git a/img/banner_light.png b/img/banner_light.png deleted file mode 100644 index f80f39371..000000000 Binary files a/img/banner_light.png and /dev/null differ diff --git a/img/icons/night/pagedown.png b/img/icons/night/pagedown.png deleted file mode 100644 index da7b236d8..000000000 Binary files a/img/icons/night/pagedown.png and /dev/null differ diff --git a/img/icons/night/pageup.png b/img/icons/night/pageup.png deleted file mode 100644 index 8adf6a8b8..000000000 Binary files a/img/icons/night/pageup.png and /dev/null differ diff --git a/img/icons/night/prev.png b/img/icons/night/prev.png deleted file mode 100644 index daa14c763..000000000 Binary files a/img/icons/night/prev.png and /dev/null differ diff --git a/img/icons/solidaric/pagedown.png b/img/icons/solidaric/pagedown.png deleted file mode 100644 index ae671efd1..000000000 Binary files a/img/icons/solidaric/pagedown.png and /dev/null differ diff --git a/img/icons/solidaric/pageup.png b/img/icons/solidaric/pageup.png deleted file mode 100644 index d32dbdc3f..000000000 Binary files a/img/icons/solidaric/pageup.png and /dev/null differ diff --git a/img/icons/solidaric/prev.png b/img/icons/solidaric/prev.png deleted file mode 100644 index b8d6a40f8..000000000 Binary files a/img/icons/solidaric/prev.png and /dev/null differ diff --git a/img/login_background_solidaric.jpg b/img/login_background_solidaric.jpg deleted file mode 100644 index 39f8528d1..000000000 Binary files a/img/login_background_solidaric.jpg and /dev/null differ diff --git a/img/screenshot_light.jpg b/img/screenshot_light.jpg index 5d2aeee7c..cfcd654b2 100644 Binary files a/img/screenshot_light.jpg and b/img/screenshot_light.jpg differ diff --git a/img/screenshot_media.jpg b/img/screenshot_media.jpg index f507d1158..b76ffca22 100644 Binary files a/img/screenshot_media.jpg and b/img/screenshot_media.jpg differ diff --git a/img/screenshot_profile.jpg b/img/screenshot_profile.jpg new file mode 100644 index 000000000..0cbae9099 Binary files /dev/null and b/img/screenshot_profile.jpg differ diff --git a/img/screenshot_starlight.jpg b/img/screenshot_starlight.jpg index 516662b2c..52cfd6e27 100644 Binary files a/img/screenshot_starlight.jpg and b/img/screenshot_starlight.jpg differ diff --git a/img/search_banner_light.png b/img/search_banner_light.png deleted file mode 100644 index f80f39371..000000000 Binary files a/img/search_banner_light.png and /dev/null differ diff --git a/inbox.py b/inbox.py index 5deb64b2a..42616d809 100644 --- a/inbox.py +++ b/inbox.py @@ -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, diff --git a/person.py b/person.py index 76f3d31e5..76ced36c7 100644 --- a/person.py +++ b/person.py @@ -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') diff --git a/tests.py b/tests.py index f835378c0..0431387a0 100644 --- a/tests.py +++ b/tests.py @@ -1943,32 +1943,52 @@ def testDangerousMarkup(): print('testDangerousMarkup') content = '

This is a valid message

' assert(not dangerousMarkup(content)) + content = 'This is a valid message without markup' assert(not dangerousMarkup(content)) + content = '

This is a valid-looking message. But wait... ' + \ '

' assert(dangerousMarkup(content)) + content = '

This is a valid-looking message. But wait... ' + \ '