2020-04-04 12:03:28 +00:00
|
|
|
__filename__ = "theme.py"
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
__license__ = "AGPL3+"
|
2021-01-26 10:07:42 +00:00
|
|
|
__version__ = "1.2.0"
|
2020-04-04 12:03:28 +00:00
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
__email__ = "bob@freedombone.net"
|
|
|
|
__status__ = "Production"
|
2019-11-23 13:04:11 +00:00
|
|
|
|
|
|
|
import os
|
|
|
|
from utils import loadJson
|
|
|
|
from utils import saveJson
|
2020-11-21 11:54:29 +00:00
|
|
|
from utils import getImageExtensions
|
2020-05-28 21:30:40 +00:00
|
|
|
from shutil import copyfile
|
2020-11-15 11:01:05 +00:00
|
|
|
from content import dangerousCSS
|
2020-11-15 10:33:11 +00:00
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _getThemeFiles() -> []:
|
2021-02-07 15:22:06 +00:00
|
|
|
"""Gets the list of theme style sheets
|
|
|
|
"""
|
2020-07-11 09:30:07 +00:00
|
|
|
return ('epicyon.css', 'login.css', 'follow.css',
|
2020-07-25 22:12:59 +00:00
|
|
|
'suspended.css', 'calendar.css', 'blog.css',
|
2021-02-25 12:42:05 +00:00
|
|
|
'options.css', 'search.css', 'links.css',
|
|
|
|
'welcome.css')
|
2020-07-11 09:30:07 +00:00
|
|
|
|
|
|
|
|
2021-02-27 11:44:50 +00:00
|
|
|
def isNewsThemeName(baseDir: str, themeName: str) -> bool:
|
|
|
|
"""Returns true if the given theme is a news instance
|
|
|
|
"""
|
|
|
|
themeDir = baseDir + '/theme/' + themeName
|
|
|
|
if os.path.isfile(themeDir + '/is_news_instance'):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2020-11-14 15:28:29 +00:00
|
|
|
def getThemesList(baseDir: str) -> []:
|
2020-05-28 09:11:21 +00:00
|
|
|
"""Returns the list of available themes
|
|
|
|
Note that these should be capitalized, since they're
|
|
|
|
also used to create the web interface dropdown list
|
|
|
|
and to lookup function names
|
|
|
|
"""
|
2020-11-14 15:28:29 +00:00
|
|
|
themes = []
|
2020-11-14 15:37:20 +00:00
|
|
|
for subdir, dirs, files in os.walk(baseDir + '/theme'):
|
2020-11-14 15:28:29 +00:00
|
|
|
for themeName in dirs:
|
2020-11-15 09:55:49 +00:00
|
|
|
if '~' not in themeName and \
|
|
|
|
themeName != 'icons' and themeName != 'fonts':
|
2020-11-14 15:34:13 +00:00
|
|
|
themes.append(themeName.title())
|
2020-11-14 15:37:20 +00:00
|
|
|
break
|
2020-11-16 15:11:11 +00:00
|
|
|
themes.sort()
|
2020-11-14 15:28:29 +00:00
|
|
|
print('Themes available: ' + str(themes))
|
|
|
|
return themes
|
2020-05-28 09:11:21 +00:00
|
|
|
|
|
|
|
|
2021-02-27 12:36:14 +00:00
|
|
|
def _copyThemeHelpFiles(baseDir: str, themeName: str,
|
|
|
|
systemLanguage: str) -> None:
|
|
|
|
"""Copies any theme specific help files from the welcome subdirectory
|
|
|
|
"""
|
2021-02-27 12:40:14 +00:00
|
|
|
if not systemLanguage:
|
|
|
|
systemLanguage = 'en'
|
2021-02-27 12:36:14 +00:00
|
|
|
themeDir = baseDir + '/theme/' + themeName + '/welcome'
|
|
|
|
if not os.path.isdir(themeDir):
|
|
|
|
themeDir = baseDir + '/defaultwelcome'
|
|
|
|
for subdir, dirs, files in os.walk(themeDir):
|
|
|
|
for helpMarkdownFile in files:
|
2021-02-27 14:39:35 +00:00
|
|
|
if not helpMarkdownFile.endswith('_' + systemLanguage + '.md'):
|
|
|
|
continue
|
|
|
|
destHelpMarkdownFile = \
|
|
|
|
helpMarkdownFile.replace('_' + systemLanguage + '.md', '.md')
|
2021-02-27 14:44:47 +00:00
|
|
|
if destHelpMarkdownFile == 'profile.md' or \
|
|
|
|
destHelpMarkdownFile == 'final.md':
|
|
|
|
destHelpMarkdownFile = 'welcome_' + destHelpMarkdownFile
|
2021-03-03 13:39:39 +00:00
|
|
|
if os.path.isdir(baseDir + '/accounts'):
|
|
|
|
copyfile(themeDir + '/' + helpMarkdownFile,
|
|
|
|
baseDir + '/accounts/' + destHelpMarkdownFile)
|
2021-02-27 12:36:14 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setThemeInConfig(baseDir: str, name: str) -> bool:
|
2021-02-07 15:22:06 +00:00
|
|
|
"""Sets the theme with the given name within config.json
|
|
|
|
"""
|
2020-04-04 12:03:28 +00:00
|
|
|
configFilename = baseDir + '/config.json'
|
2019-11-23 13:04:11 +00:00
|
|
|
if not os.path.isfile(configFilename):
|
|
|
|
return False
|
2020-04-04 12:03:28 +00:00
|
|
|
configJson = loadJson(configFilename, 0)
|
2019-11-23 13:04:11 +00:00
|
|
|
if not configJson:
|
|
|
|
return False
|
2020-04-04 12:03:28 +00:00
|
|
|
configJson['theme'] = name
|
|
|
|
return saveJson(configJson, configFilename)
|
|
|
|
|
2019-11-23 13:04:11 +00:00
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setNewswirePublishAsIcon(baseDir: str, useIcon: bool) -> bool:
|
2020-10-24 16:37:37 +00:00
|
|
|
"""Shows the newswire publish action as an icon or a button
|
|
|
|
"""
|
|
|
|
configFilename = baseDir + '/config.json'
|
|
|
|
if not os.path.isfile(configFilename):
|
|
|
|
return False
|
|
|
|
configJson = loadJson(configFilename, 0)
|
|
|
|
if not configJson:
|
|
|
|
return False
|
|
|
|
configJson['showPublishAsIcon'] = useIcon
|
|
|
|
return saveJson(configJson, configFilename)
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setIconsAsButtons(baseDir: str, useButtons: bool) -> bool:
|
2020-10-25 20:38:01 +00:00
|
|
|
"""Whether to show icons in the header (inbox, outbox, etc)
|
|
|
|
as buttons
|
|
|
|
"""
|
|
|
|
configFilename = baseDir + '/config.json'
|
|
|
|
if not os.path.isfile(configFilename):
|
|
|
|
return False
|
|
|
|
configJson = loadJson(configFilename, 0)
|
|
|
|
if not configJson:
|
|
|
|
return False
|
|
|
|
configJson['iconsAsButtons'] = useButtons
|
|
|
|
return saveJson(configJson, configFilename)
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setRssIconAtTop(baseDir: str, atTop: bool) -> bool:
|
2020-10-26 20:32:01 +00:00
|
|
|
"""Whether to show RSS icon at the top of the timeline
|
|
|
|
"""
|
|
|
|
configFilename = baseDir + '/config.json'
|
|
|
|
if not os.path.isfile(configFilename):
|
|
|
|
return False
|
|
|
|
configJson = loadJson(configFilename, 0)
|
|
|
|
if not configJson:
|
|
|
|
return False
|
|
|
|
configJson['rssIconAtTop'] = atTop
|
|
|
|
return saveJson(configJson, configFilename)
|
|
|
|
|
2020-10-26 21:33:40 +00:00
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setPublishButtonAtTop(baseDir: str, atTop: bool) -> bool:
|
2020-10-26 21:33:40 +00:00
|
|
|
"""Whether to show the publish button above the title image
|
|
|
|
in the newswire column
|
2020-10-26 21:32:08 +00:00
|
|
|
"""
|
|
|
|
configFilename = baseDir + '/config.json'
|
|
|
|
if not os.path.isfile(configFilename):
|
|
|
|
return False
|
|
|
|
configJson = loadJson(configFilename, 0)
|
|
|
|
if not configJson:
|
|
|
|
return False
|
|
|
|
configJson['publishButtonAtTop'] = atTop
|
|
|
|
return saveJson(configJson, configFilename)
|
|
|
|
|
2020-10-26 20:32:01 +00:00
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setFullWidthTimelineButtonHeader(baseDir: str, fullWidth: bool) -> bool:
|
2020-10-24 17:51:00 +00:00
|
|
|
"""Shows the timeline button header containing inbox, outbox,
|
|
|
|
calendar, etc as full width
|
|
|
|
"""
|
|
|
|
configFilename = baseDir + '/config.json'
|
|
|
|
if not os.path.isfile(configFilename):
|
|
|
|
return False
|
|
|
|
configJson = loadJson(configFilename, 0)
|
|
|
|
if not configJson:
|
|
|
|
return False
|
|
|
|
configJson['fullWidthTimelineButtonHeader'] = fullWidth
|
|
|
|
return saveJson(configJson, configFilename)
|
|
|
|
|
|
|
|
|
2020-05-26 20:17:16 +00:00
|
|
|
def getTheme(baseDir: str) -> str:
|
2021-02-07 15:22:06 +00:00
|
|
|
"""Gets the current theme name from config.json
|
|
|
|
"""
|
2020-05-26 20:17:16 +00:00
|
|
|
configFilename = baseDir + '/config.json'
|
2020-05-27 13:05:23 +00:00
|
|
|
if os.path.isfile(configFilename):
|
|
|
|
configJson = loadJson(configFilename, 0)
|
|
|
|
if configJson:
|
|
|
|
if configJson.get('theme'):
|
|
|
|
return configJson['theme']
|
2020-05-26 20:17:16 +00:00
|
|
|
return 'default'
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _removeTheme(baseDir: str):
|
2021-02-07 15:22:06 +00:00
|
|
|
"""Removes the current theme style sheets
|
|
|
|
"""
|
2020-12-22 18:06:23 +00:00
|
|
|
themeFiles = _getThemeFiles()
|
2019-11-23 13:04:11 +00:00
|
|
|
for filename in themeFiles:
|
2020-04-04 12:03:28 +00:00
|
|
|
if os.path.isfile(baseDir + '/' + filename):
|
|
|
|
os.remove(baseDir + '/' + filename)
|
|
|
|
|
2019-11-23 13:04:11 +00:00
|
|
|
|
2020-04-04 12:03:28 +00:00
|
|
|
def setCSSparam(css: str, param: str, value: str) -> str:
|
2019-11-23 13:04:11 +00:00
|
|
|
"""Sets a CSS parameter to a given value
|
|
|
|
"""
|
|
|
|
# is this just a simple string replacement?
|
|
|
|
if ';' in param:
|
2020-04-04 12:03:28 +00:00
|
|
|
return css.replace(param, value)
|
2019-11-23 13:04:11 +00:00
|
|
|
# color replacement
|
|
|
|
if param.startswith('rgba('):
|
2020-04-04 12:03:28 +00:00
|
|
|
return css.replace(param, value)
|
2019-11-23 13:04:11 +00:00
|
|
|
# if the parameter begins with * then don't prepend --
|
2020-09-14 12:58:24 +00:00
|
|
|
onceOnly = False
|
2019-11-23 13:04:11 +00:00
|
|
|
if param.startswith('*'):
|
2020-09-14 12:52:22 +00:00
|
|
|
if param.startswith('**'):
|
2020-09-14 12:58:24 +00:00
|
|
|
onceOnly = True
|
|
|
|
searchStr = param.replace('**', '') + ':'
|
2020-09-14 12:52:22 +00:00
|
|
|
else:
|
|
|
|
searchStr = param.replace('*', '') + ':'
|
2019-11-23 13:04:11 +00:00
|
|
|
else:
|
2020-04-04 12:03:28 +00:00
|
|
|
searchStr = '--' + param + ':'
|
2019-11-23 13:04:11 +00:00
|
|
|
if searchStr not in css:
|
|
|
|
return css
|
2020-09-14 13:05:12 +00:00
|
|
|
if onceOnly:
|
|
|
|
s = css.split(searchStr, 1)
|
|
|
|
else:
|
|
|
|
s = css.split(searchStr)
|
2020-04-04 12:03:28 +00:00
|
|
|
newcss = ''
|
2019-11-23 13:04:11 +00:00
|
|
|
for sectionStr in s:
|
2020-11-16 20:13:48 +00:00
|
|
|
# handle font-family which is a variable
|
|
|
|
nextSection = sectionStr
|
|
|
|
if ';' in nextSection:
|
|
|
|
nextSection = nextSection.split(';')[0] + ';'
|
|
|
|
if searchStr == 'font-family:' and "var(--" in nextSection:
|
|
|
|
newcss += searchStr + ' ' + sectionStr
|
|
|
|
continue
|
|
|
|
|
2019-11-23 13:04:11 +00:00
|
|
|
if not newcss:
|
|
|
|
if sectionStr:
|
2020-04-04 12:03:28 +00:00
|
|
|
newcss = sectionStr
|
2019-11-23 13:04:11 +00:00
|
|
|
else:
|
2020-04-04 12:03:28 +00:00
|
|
|
newcss = ' '
|
2019-11-23 13:04:11 +00:00
|
|
|
else:
|
|
|
|
if ';' in sectionStr:
|
2020-04-04 12:03:28 +00:00
|
|
|
newcss += \
|
|
|
|
searchStr + ' ' + value + ';' + sectionStr.split(';', 1)[1]
|
2019-11-23 13:04:11 +00:00
|
|
|
else:
|
2020-04-04 12:03:28 +00:00
|
|
|
newcss += searchStr + ' ' + sectionStr
|
2019-11-23 13:04:11 +00:00
|
|
|
return newcss.strip()
|
2020-03-22 21:16:02 +00:00
|
|
|
|
2020-04-04 12:03:28 +00:00
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setThemeFromDict(baseDir: str, name: str,
|
|
|
|
themeParams: {}, bgParams: {},
|
|
|
|
allowLocalNetworkAccess: bool) -> None:
|
2019-11-23 13:04:11 +00:00
|
|
|
"""Uses a dictionary to set a theme
|
|
|
|
"""
|
2020-07-10 18:08:45 +00:00
|
|
|
if name:
|
2020-12-22 18:06:23 +00:00
|
|
|
_setThemeInConfig(baseDir, name)
|
|
|
|
themeFiles = _getThemeFiles()
|
2019-11-23 13:04:11 +00:00
|
|
|
for filename in themeFiles:
|
2020-11-14 13:18:11 +00:00
|
|
|
# check for custom css within the theme directory
|
|
|
|
templateFilename = baseDir + '/theme/' + name + '/epicyon-' + filename
|
2020-04-04 12:03:28 +00:00
|
|
|
if filename == 'epicyon.css':
|
2020-11-14 13:18:11 +00:00
|
|
|
templateFilename = \
|
|
|
|
baseDir + '/theme/' + name + '/epicyon-profile.css'
|
|
|
|
|
2020-11-15 11:07:12 +00:00
|
|
|
# Ensure that any custom CSS is mostly harmless.
|
|
|
|
# If not then just use the defaults
|
2020-11-20 10:58:49 +00:00
|
|
|
if dangerousCSS(templateFilename, allowLocalNetworkAccess) or \
|
2020-11-15 10:33:11 +00:00
|
|
|
not os.path.isfile(templateFilename):
|
2020-11-14 13:18:11 +00:00
|
|
|
# use default css
|
|
|
|
templateFilename = baseDir + '/epicyon-' + filename
|
|
|
|
if filename == 'epicyon.css':
|
|
|
|
templateFilename = baseDir + '/epicyon-profile.css'
|
|
|
|
|
2019-11-23 13:04:11 +00:00
|
|
|
if not os.path.isfile(templateFilename):
|
|
|
|
continue
|
2020-11-14 13:18:11 +00:00
|
|
|
|
2019-11-23 13:04:11 +00:00
|
|
|
with open(templateFilename, 'r') as cssfile:
|
2020-04-04 12:03:28 +00:00
|
|
|
css = cssfile.read()
|
|
|
|
for paramName, paramValue in themeParams.items():
|
2020-11-14 18:08:46 +00:00
|
|
|
if paramName == 'newswire-publish-icon':
|
2020-11-14 19:21:11 +00:00
|
|
|
if paramValue.lower() == 'true':
|
2020-12-22 18:06:23 +00:00
|
|
|
_setNewswirePublishAsIcon(baseDir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2020-12-22 18:06:23 +00:00
|
|
|
_setNewswirePublishAsIcon(baseDir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
|
|
|
elif paramName == 'full-width-timeline-buttons':
|
2020-11-14 19:21:11 +00:00
|
|
|
if paramValue.lower() == 'true':
|
2020-12-22 18:06:23 +00:00
|
|
|
_setFullWidthTimelineButtonHeader(baseDir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2020-12-22 18:06:23 +00:00
|
|
|
_setFullWidthTimelineButtonHeader(baseDir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
|
|
|
elif paramName == 'icons-as-buttons':
|
2020-11-14 19:21:11 +00:00
|
|
|
if paramValue.lower() == 'true':
|
2020-12-22 18:06:23 +00:00
|
|
|
_setIconsAsButtons(baseDir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2020-12-22 18:06:23 +00:00
|
|
|
_setIconsAsButtons(baseDir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
|
|
|
elif paramName == 'rss-icon-at-top':
|
2020-11-14 19:21:11 +00:00
|
|
|
if paramValue.lower() == 'true':
|
2020-12-22 18:06:23 +00:00
|
|
|
_setRssIconAtTop(baseDir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2020-12-22 18:06:23 +00:00
|
|
|
_setRssIconAtTop(baseDir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
|
|
|
elif paramName == 'publish-button-at-top':
|
2020-11-14 19:21:11 +00:00
|
|
|
if paramValue.lower() == 'true':
|
2020-12-22 18:06:23 +00:00
|
|
|
_setPublishButtonAtTop(baseDir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2020-12-22 18:06:23 +00:00
|
|
|
_setPublishButtonAtTop(baseDir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
2020-04-04 12:03:28 +00:00
|
|
|
css = setCSSparam(css, paramName, paramValue)
|
|
|
|
filename = baseDir + '/' + filename
|
2020-07-12 20:04:58 +00:00
|
|
|
with open(filename, 'w+') as cssfile:
|
2019-11-23 13:04:11 +00:00
|
|
|
cssfile.write(css)
|
|
|
|
|
2020-07-26 11:59:48 +00:00
|
|
|
if bgParams.get('login'):
|
2020-12-22 18:06:23 +00:00
|
|
|
_setBackgroundFormat(baseDir, name, 'login', bgParams['login'])
|
2020-07-26 11:59:48 +00:00
|
|
|
if bgParams.get('follow'):
|
2020-12-22 18:06:23 +00:00
|
|
|
_setBackgroundFormat(baseDir, name, 'follow', bgParams['follow'])
|
2020-07-26 11:59:48 +00:00
|
|
|
if bgParams.get('options'):
|
2020-12-22 18:06:23 +00:00
|
|
|
_setBackgroundFormat(baseDir, name, 'options', bgParams['options'])
|
2020-07-27 09:49:54 +00:00
|
|
|
if bgParams.get('search'):
|
2020-12-22 18:06:23 +00:00
|
|
|
_setBackgroundFormat(baseDir, name, 'search', bgParams['search'])
|
2021-02-25 19:07:45 +00:00
|
|
|
if bgParams.get('welcome'):
|
|
|
|
_setBackgroundFormat(baseDir, name, 'welcome', bgParams['welcome'])
|
2020-04-04 12:03:28 +00:00
|
|
|
|
2020-07-26 11:59:48 +00:00
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setBackgroundFormat(baseDir: str, name: str,
|
|
|
|
backgroundType: str, extension: str) -> None:
|
2020-07-25 16:25:04 +00:00
|
|
|
"""Sets the background file extension
|
|
|
|
"""
|
2020-07-25 19:07:06 +00:00
|
|
|
if extension == 'jpg':
|
2020-07-25 16:25:04 +00:00
|
|
|
return
|
2020-07-26 11:59:48 +00:00
|
|
|
cssFilename = baseDir + '/' + backgroundType + '.css'
|
|
|
|
if not os.path.isfile(cssFilename):
|
|
|
|
return
|
|
|
|
with open(cssFilename, 'r') as cssfile:
|
|
|
|
css = cssfile.read()
|
|
|
|
css = css.replace('background.jpg', 'background.' + extension)
|
|
|
|
with open(cssFilename, 'w+') as cssfile2:
|
|
|
|
cssfile2.write(css)
|
2020-07-25 16:25:04 +00:00
|
|
|
|
|
|
|
|
2020-07-11 09:22:06 +00:00
|
|
|
def enableGrayscale(baseDir: str) -> None:
|
|
|
|
"""Enables grayscale for the current theme
|
2020-07-10 18:16:33 +00:00
|
|
|
"""
|
2020-12-22 18:06:23 +00:00
|
|
|
themeFiles = _getThemeFiles()
|
2020-07-10 18:16:33 +00:00
|
|
|
for filename in themeFiles:
|
|
|
|
templateFilename = baseDir + '/' + filename
|
|
|
|
if not os.path.isfile(templateFilename):
|
|
|
|
continue
|
|
|
|
with open(templateFilename, 'r') as cssfile:
|
|
|
|
css = cssfile.read()
|
2020-07-11 09:22:06 +00:00
|
|
|
if 'grayscale' not in css:
|
|
|
|
css = \
|
|
|
|
css.replace('body, html {',
|
|
|
|
'body, html {\n filter: grayscale(100%);')
|
|
|
|
filename = baseDir + '/' + filename
|
2020-07-12 20:04:58 +00:00
|
|
|
with open(filename, 'w+') as cssfile:
|
2020-07-11 09:22:06 +00:00
|
|
|
cssfile.write(css)
|
|
|
|
grayscaleFilename = baseDir + '/accounts/.grayscale'
|
|
|
|
if not os.path.isfile(grayscaleFilename):
|
2020-07-12 20:04:58 +00:00
|
|
|
with open(grayscaleFilename, 'w+') as grayfile:
|
2020-07-11 09:22:06 +00:00
|
|
|
grayfile.write(' ')
|
|
|
|
|
|
|
|
|
|
|
|
def disableGrayscale(baseDir: str) -> None:
|
|
|
|
"""Disables grayscale for the current theme
|
|
|
|
"""
|
2020-12-22 18:06:23 +00:00
|
|
|
themeFiles = _getThemeFiles()
|
2020-07-11 09:22:06 +00:00
|
|
|
for filename in themeFiles:
|
|
|
|
templateFilename = baseDir + '/' + filename
|
|
|
|
if not os.path.isfile(templateFilename):
|
|
|
|
continue
|
|
|
|
with open(templateFilename, 'r') as cssfile:
|
|
|
|
css = cssfile.read()
|
|
|
|
if 'grayscale' in css:
|
|
|
|
css = \
|
|
|
|
css.replace('\n filter: grayscale(100%);', '')
|
|
|
|
filename = baseDir + '/' + filename
|
2020-07-12 20:04:58 +00:00
|
|
|
with open(filename, 'w+') as cssfile:
|
2020-07-11 09:22:06 +00:00
|
|
|
cssfile.write(css)
|
|
|
|
grayscaleFilename = baseDir + '/accounts/.grayscale'
|
|
|
|
if os.path.isfile(grayscaleFilename):
|
|
|
|
os.remove(grayscaleFilename)
|
2020-07-10 18:16:33 +00:00
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setCustomFont(baseDir: str):
|
2020-05-26 20:17:16 +00:00
|
|
|
"""Uses a dictionary to set a theme
|
|
|
|
"""
|
|
|
|
customFontExt = None
|
|
|
|
customFontType = None
|
|
|
|
fontExtension = {
|
|
|
|
'woff': 'woff',
|
|
|
|
'woff2': 'woff2',
|
|
|
|
'otf': 'opentype',
|
|
|
|
'ttf': 'truetype'
|
|
|
|
}
|
|
|
|
for ext, extType in fontExtension.items():
|
|
|
|
filename = baseDir + '/fonts/custom.' + ext
|
|
|
|
if os.path.isfile(filename):
|
|
|
|
customFontExt = ext
|
|
|
|
customFontType = extType
|
|
|
|
if not customFontExt:
|
|
|
|
return
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
themeFiles = _getThemeFiles()
|
2020-05-26 20:17:16 +00:00
|
|
|
for filename in themeFiles:
|
2020-05-26 21:35:48 +00:00
|
|
|
templateFilename = baseDir + '/' + filename
|
2020-05-26 20:17:16 +00:00
|
|
|
if not os.path.isfile(templateFilename):
|
|
|
|
continue
|
|
|
|
with open(templateFilename, 'r') as cssfile:
|
|
|
|
css = cssfile.read()
|
|
|
|
css = \
|
|
|
|
setCSSparam(css, "*src",
|
|
|
|
"url('./fonts/custom." +
|
|
|
|
customFontExt +
|
|
|
|
"') format('" +
|
|
|
|
customFontType + "')")
|
2020-05-26 20:23:38 +00:00
|
|
|
css = setCSSparam(css, "*font-family", "'CustomFont'")
|
2020-05-26 20:17:16 +00:00
|
|
|
filename = baseDir + '/' + filename
|
2020-07-12 20:04:58 +00:00
|
|
|
with open(filename, 'w+') as cssfile:
|
2020-05-26 20:17:16 +00:00
|
|
|
cssfile.write(css)
|
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _readVariablesFile(baseDir: str, themeName: str,
|
|
|
|
variablesFile: str,
|
|
|
|
allowLocalNetworkAccess: bool) -> None:
|
2020-11-14 17:34:11 +00:00
|
|
|
"""Reads variables from a file in the theme directory
|
|
|
|
"""
|
|
|
|
themeParams = loadJson(variablesFile, 0)
|
|
|
|
if not themeParams:
|
|
|
|
return
|
|
|
|
bgParams = {
|
|
|
|
"login": "jpg",
|
|
|
|
"follow": "jpg",
|
|
|
|
"options": "jpg",
|
|
|
|
"search": "jpg"
|
|
|
|
}
|
2020-12-22 18:06:23 +00:00
|
|
|
_setThemeFromDict(baseDir, themeName, themeParams, bgParams,
|
|
|
|
allowLocalNetworkAccess)
|
2020-11-14 17:34:11 +00:00
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setThemeDefault(baseDir: str, allowLocalNetworkAccess: bool):
|
2020-07-26 11:45:09 +00:00
|
|
|
name = 'default'
|
2020-12-22 18:06:23 +00:00
|
|
|
_removeTheme(baseDir)
|
|
|
|
_setThemeInConfig(baseDir, name)
|
2020-07-26 11:59:48 +00:00
|
|
|
bgParams = {
|
2020-07-27 09:49:54 +00:00
|
|
|
"login": "jpg",
|
|
|
|
"follow": "jpg",
|
|
|
|
"options": "jpg",
|
|
|
|
"search": "jpg"
|
2020-07-26 11:59:48 +00:00
|
|
|
}
|
2020-08-31 11:51:31 +00:00
|
|
|
themeParams = {
|
2020-11-14 18:08:46 +00:00
|
|
|
"newswire-publish-icon": True,
|
|
|
|
"full-width-timeline-buttons": False,
|
|
|
|
"icons-as-buttons": False,
|
|
|
|
"rss-icon-at-top": True,
|
|
|
|
"publish-button-at-top": False,
|
2020-10-28 22:32:13 +00:00
|
|
|
"banner-height": "20vh",
|
2020-11-07 20:14:46 +00:00
|
|
|
"banner-height-mobile": "10vh",
|
|
|
|
"search-banner-height-mobile": "15vh"
|
2020-08-31 11:51:31 +00:00
|
|
|
}
|
2020-12-22 18:06:23 +00:00
|
|
|
_setThemeFromDict(baseDir, name, themeParams, bgParams,
|
|
|
|
allowLocalNetworkAccess)
|
2020-05-29 10:00:05 +00:00
|
|
|
|
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
def _setThemeFonts(baseDir: str, themeName: str) -> None:
|
2020-11-15 09:55:49 +00:00
|
|
|
"""Adds custom theme fonts
|
|
|
|
"""
|
|
|
|
themeNameLower = themeName.lower()
|
|
|
|
fontsDir = baseDir + '/fonts'
|
|
|
|
themeFontsDir = \
|
|
|
|
baseDir + '/theme/' + themeNameLower + '/fonts'
|
|
|
|
if not os.path.isdir(themeFontsDir):
|
|
|
|
return
|
|
|
|
for subdir, dirs, files in os.walk(themeFontsDir):
|
|
|
|
for filename in files:
|
|
|
|
if filename.endswith('.woff2') or \
|
|
|
|
filename.endswith('.woff') or \
|
|
|
|
filename.endswith('.ttf') or \
|
|
|
|
filename.endswith('.otf'):
|
|
|
|
destFilename = fontsDir + '/' + filename
|
|
|
|
if os.path.isfile(destFilename):
|
|
|
|
# font already exists in the destination location
|
|
|
|
continue
|
|
|
|
copyfile(themeFontsDir + '/' + filename,
|
|
|
|
destFilename)
|
|
|
|
break
|
|
|
|
|
|
|
|
|
2021-02-05 19:50:09 +00:00
|
|
|
def getTextModeBanner(baseDir: str) -> str:
|
|
|
|
"""Returns the banner used for shell browsers, like Lynx
|
|
|
|
"""
|
|
|
|
textModeBannerFilename = baseDir + '/accounts/banner.txt'
|
|
|
|
if os.path.isfile(textModeBannerFilename):
|
|
|
|
with open(textModeBannerFilename, 'r') as fp:
|
|
|
|
bannerStr = fp.read()
|
|
|
|
if bannerStr:
|
2021-02-15 12:57:33 +00:00
|
|
|
return bannerStr.replace('\n', '<br>')
|
2021-02-05 19:50:09 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-02-06 14:30:18 +00:00
|
|
|
def getTextModeLogo(baseDir: str) -> str:
|
|
|
|
"""Returns the login screen logo used for shell browsers, like Lynx
|
|
|
|
"""
|
|
|
|
textModeLogoFilename = baseDir + '/accounts/logo.txt'
|
|
|
|
if not os.path.isfile(textModeLogoFilename):
|
|
|
|
textModeLogoFilename = baseDir + '/img/logo.txt'
|
|
|
|
|
|
|
|
with open(textModeLogoFilename, 'r') as fp:
|
|
|
|
logoStr = fp.read()
|
|
|
|
if logoStr:
|
|
|
|
return logoStr.replace('\n', '<br>')
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-02-07 15:32:15 +00:00
|
|
|
def _setTextModeTheme(baseDir: str, name: str) -> None:
|
|
|
|
# set the text mode logo which appears on the login screen
|
|
|
|
# in browsers such as Lynx
|
2021-02-06 18:51:36 +00:00
|
|
|
textModeLogoFilename = \
|
2021-02-07 15:32:15 +00:00
|
|
|
baseDir + '/theme/' + name + '/logo.txt'
|
2021-02-06 18:51:36 +00:00
|
|
|
if os.path.isfile(textModeLogoFilename):
|
|
|
|
try:
|
|
|
|
copyfile(textModeLogoFilename,
|
|
|
|
baseDir + '/accounts/logo.txt')
|
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
copyfile(baseDir + '/img/logo.txt',
|
|
|
|
baseDir + '/accounts/logo.txt')
|
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
|
2021-02-07 15:32:15 +00:00
|
|
|
# set the text mode banner which appears in browsers such as Lynx
|
2021-02-05 19:39:21 +00:00
|
|
|
textModeBannerFilename = \
|
2021-02-07 15:32:15 +00:00
|
|
|
baseDir + '/theme/' + name + '/banner.txt'
|
2021-02-05 19:42:51 +00:00
|
|
|
if os.path.isfile(baseDir + '/accounts/banner.txt'):
|
|
|
|
os.remove(baseDir + '/accounts/banner.txt')
|
2021-02-05 19:39:21 +00:00
|
|
|
if os.path.isfile(textModeBannerFilename):
|
|
|
|
try:
|
|
|
|
copyfile(textModeBannerFilename,
|
|
|
|
baseDir + '/accounts/banner.txt')
|
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
|
2021-02-07 15:32:15 +00:00
|
|
|
|
|
|
|
def _setThemeImages(baseDir: str, name: str) -> None:
|
|
|
|
"""Changes the profile background image
|
|
|
|
and banner to the defaults
|
|
|
|
"""
|
|
|
|
themeNameLower = name.lower()
|
|
|
|
|
|
|
|
profileImageFilename = \
|
|
|
|
baseDir + '/theme/' + themeNameLower + '/image.png'
|
|
|
|
bannerFilename = \
|
|
|
|
baseDir + '/theme/' + themeNameLower + '/banner.png'
|
|
|
|
searchBannerFilename = \
|
|
|
|
baseDir + '/theme/' + themeNameLower + '/search_banner.png'
|
|
|
|
leftColImageFilename = \
|
|
|
|
baseDir + '/theme/' + themeNameLower + '/left_col_image.png'
|
|
|
|
rightColImageFilename = \
|
|
|
|
baseDir + '/theme/' + themeNameLower + '/right_col_image.png'
|
|
|
|
|
|
|
|
_setTextModeTheme(baseDir, themeNameLower)
|
|
|
|
|
2020-07-25 14:03:09 +00:00
|
|
|
backgroundNames = ('login', 'shares', 'delete', 'follow',
|
2021-02-25 19:07:45 +00:00
|
|
|
'options', 'block', 'search', 'calendar',
|
|
|
|
'welcome')
|
2020-11-21 11:54:29 +00:00
|
|
|
extensions = getImageExtensions()
|
2020-07-25 14:03:09 +00:00
|
|
|
|
|
|
|
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
|
2020-07-25 09:55:11 +00:00
|
|
|
for acct in dirs:
|
|
|
|
if '@' not in acct:
|
|
|
|
continue
|
2021-04-21 16:09:56 +00:00
|
|
|
if acct.startswith('inbox@'):
|
|
|
|
continue
|
|
|
|
elif acct.startswith('news@'):
|
2020-07-25 09:55:11 +00:00
|
|
|
continue
|
|
|
|
accountDir = \
|
|
|
|
os.path.join(baseDir + '/accounts', acct)
|
|
|
|
|
2020-07-25 10:13:24 +00:00
|
|
|
for backgroundType in backgroundNames:
|
2020-07-25 16:12:14 +00:00
|
|
|
for ext in extensions:
|
|
|
|
if themeNameLower == 'default':
|
|
|
|
backgroundImageFilename = \
|
2020-11-14 12:44:24 +00:00
|
|
|
baseDir + '/theme/default/' + \
|
|
|
|
backgroundType + '_background.' + ext
|
2020-07-25 16:12:14 +00:00
|
|
|
else:
|
|
|
|
backgroundImageFilename = \
|
2020-11-14 12:44:24 +00:00
|
|
|
baseDir + '/theme/' + themeNameLower + '/' + \
|
|
|
|
backgroundType + '_background' + '.' + ext
|
2020-07-25 16:12:14 +00:00
|
|
|
|
|
|
|
if os.path.isfile(backgroundImageFilename):
|
|
|
|
try:
|
|
|
|
copyfile(backgroundImageFilename,
|
2020-11-14 12:44:24 +00:00
|
|
|
baseDir + '/accounts/' +
|
|
|
|
backgroundType + '-background.' + ext)
|
2020-07-25 17:56:06 +00:00
|
|
|
continue
|
2020-07-25 16:12:14 +00:00
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
# background image was not found
|
|
|
|
# so remove any existing file
|
|
|
|
if os.path.isfile(baseDir + '/accounts/' +
|
2020-11-14 12:44:24 +00:00
|
|
|
backgroundType + '-background.' + ext):
|
2020-07-25 16:12:14 +00:00
|
|
|
try:
|
|
|
|
os.remove(baseDir + '/accounts/' +
|
2020-11-14 12:44:24 +00:00
|
|
|
backgroundType + '-background.' + ext)
|
2020-07-25 16:12:14 +00:00
|
|
|
except BaseException:
|
|
|
|
pass
|
2020-07-25 10:03:03 +00:00
|
|
|
|
2020-07-25 09:55:11 +00:00
|
|
|
if os.path.isfile(profileImageFilename) and \
|
|
|
|
os.path.isfile(bannerFilename):
|
2020-05-28 21:39:41 +00:00
|
|
|
try:
|
|
|
|
copyfile(profileImageFilename,
|
|
|
|
accountDir + '/image.png')
|
2020-06-10 15:09:14 +00:00
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
2020-05-28 21:39:41 +00:00
|
|
|
copyfile(bannerFilename,
|
|
|
|
accountDir + '/banner.png')
|
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
|
2020-06-10 15:09:14 +00:00
|
|
|
try:
|
|
|
|
if os.path.isfile(searchBannerFilename):
|
|
|
|
copyfile(searchBannerFilename,
|
|
|
|
accountDir + '/search_banner.png')
|
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
|
2020-10-03 10:09:21 +00:00
|
|
|
try:
|
|
|
|
if os.path.isfile(leftColImageFilename):
|
|
|
|
copyfile(leftColImageFilename,
|
|
|
|
accountDir + '/left_col_image.png')
|
2020-10-03 10:11:26 +00:00
|
|
|
else:
|
|
|
|
if os.path.isfile(accountDir +
|
|
|
|
'/left_col_image.png'):
|
|
|
|
os.remove(accountDir + '/left_col_image.png')
|
|
|
|
|
2020-10-03 10:09:21 +00:00
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
if os.path.isfile(rightColImageFilename):
|
|
|
|
copyfile(rightColImageFilename,
|
|
|
|
accountDir + '/right_col_image.png')
|
2020-10-03 10:11:26 +00:00
|
|
|
else:
|
|
|
|
if os.path.isfile(accountDir +
|
|
|
|
'/right_col_image.png'):
|
|
|
|
os.remove(accountDir + '/right_col_image.png')
|
2020-10-03 10:09:21 +00:00
|
|
|
except BaseException:
|
|
|
|
pass
|
2020-12-13 22:13:45 +00:00
|
|
|
break
|
2020-10-03 10:09:21 +00:00
|
|
|
|
2020-05-28 21:39:41 +00:00
|
|
|
|
2020-10-13 21:38:19 +00:00
|
|
|
def setNewsAvatar(baseDir: str, name: str,
|
|
|
|
httpPrefix: str,
|
|
|
|
domain: str, domainFull: str) -> None:
|
|
|
|
"""Sets the avatar for the news account
|
|
|
|
"""
|
|
|
|
nickname = 'news'
|
2020-11-14 11:49:29 +00:00
|
|
|
newFilename = baseDir + '/theme/' + name + '/icons/avatar_news.png'
|
2020-10-15 18:21:05 +00:00
|
|
|
if not os.path.isfile(newFilename):
|
2020-11-14 11:49:29 +00:00
|
|
|
newFilename = baseDir + '/theme/default/icons/avatar_news.png'
|
2020-10-13 21:38:19 +00:00
|
|
|
if not os.path.isfile(newFilename):
|
|
|
|
return
|
2020-10-13 21:45:53 +00:00
|
|
|
avatarFilename = \
|
|
|
|
httpPrefix + '://' + domainFull + '/users/' + nickname + '.png'
|
|
|
|
avatarFilename = avatarFilename.replace('/', '-')
|
2020-10-13 21:51:06 +00:00
|
|
|
filename = baseDir + '/cache/avatars/' + avatarFilename
|
2020-10-13 21:45:53 +00:00
|
|
|
|
2020-10-13 21:38:19 +00:00
|
|
|
if os.path.isfile(filename):
|
|
|
|
os.remove(filename)
|
2020-10-13 21:55:52 +00:00
|
|
|
if os.path.isdir(baseDir + '/cache/avatars'):
|
2020-10-13 21:38:19 +00:00
|
|
|
copyfile(newFilename, filename)
|
|
|
|
copyfile(newFilename,
|
|
|
|
baseDir + '/accounts/' +
|
|
|
|
nickname + '@' + domain + '/avatar.png')
|
|
|
|
|
|
|
|
|
2021-02-23 14:01:22 +00:00
|
|
|
def _setClearCacheFlag(baseDir: str) -> None:
|
|
|
|
"""Sets a flag which can be used by an external system
|
|
|
|
(eg. a script in a cron job) to clear the browser cache
|
|
|
|
"""
|
2021-03-03 13:39:39 +00:00
|
|
|
if not os.path.isdir(baseDir + '/accounts'):
|
|
|
|
return
|
2021-02-23 14:01:22 +00:00
|
|
|
flagFilename = baseDir + '/accounts/.clear_cache'
|
|
|
|
with open(flagFilename, 'w+') as flagFile:
|
|
|
|
flagFile.write('\n')
|
|
|
|
|
|
|
|
|
2020-11-20 10:58:49 +00:00
|
|
|
def setTheme(baseDir: str, name: str, domain: str,
|
2021-02-27 12:36:14 +00:00
|
|
|
allowLocalNetworkAccess: bool, systemLanguage: str) -> bool:
|
2021-02-07 15:22:06 +00:00
|
|
|
"""Sets the theme with the given name as the current theme
|
|
|
|
"""
|
2020-05-26 20:17:16 +00:00
|
|
|
result = False
|
2020-05-28 09:11:21 +00:00
|
|
|
|
2020-05-28 21:30:40 +00:00
|
|
|
prevThemeName = getTheme(baseDir)
|
2020-12-22 18:06:23 +00:00
|
|
|
_removeTheme(baseDir)
|
2020-05-28 21:30:40 +00:00
|
|
|
|
2020-11-14 15:31:49 +00:00
|
|
|
themes = getThemesList(baseDir)
|
2020-05-28 09:11:21 +00:00
|
|
|
for themeName in themes:
|
2020-05-28 21:30:40 +00:00
|
|
|
themeNameLower = themeName.lower()
|
|
|
|
if name == themeNameLower:
|
2020-11-14 17:01:25 +00:00
|
|
|
try:
|
2020-11-20 10:58:49 +00:00
|
|
|
globals()['setTheme' + themeName](baseDir,
|
|
|
|
allowLocalNetworkAccess)
|
2020-11-14 17:01:25 +00:00
|
|
|
except BaseException:
|
|
|
|
pass
|
|
|
|
|
2020-05-28 21:30:40 +00:00
|
|
|
if prevThemeName:
|
|
|
|
if prevThemeName.lower() != themeNameLower:
|
|
|
|
# change the banner and profile image
|
|
|
|
# to the default for the theme
|
2020-12-22 18:06:23 +00:00
|
|
|
_setThemeImages(baseDir, name)
|
|
|
|
_setThemeFonts(baseDir, name)
|
2020-05-28 09:11:21 +00:00
|
|
|
result = True
|
|
|
|
|
|
|
|
if not result:
|
2020-05-27 11:02:51 +00:00
|
|
|
# default
|
2020-12-22 21:24:46 +00:00
|
|
|
_setThemeDefault(baseDir, allowLocalNetworkAccess)
|
2020-05-27 11:02:51 +00:00
|
|
|
result = True
|
2020-05-28 09:11:21 +00:00
|
|
|
|
2020-11-14 17:40:11 +00:00
|
|
|
variablesFile = baseDir + '/theme/' + name + '/theme.json'
|
2020-11-14 17:34:11 +00:00
|
|
|
if os.path.isfile(variablesFile):
|
2020-12-22 18:06:23 +00:00
|
|
|
_readVariablesFile(baseDir, name, variablesFile,
|
|
|
|
allowLocalNetworkAccess)
|
2020-11-14 17:34:11 +00:00
|
|
|
|
2020-12-22 18:06:23 +00:00
|
|
|
_setCustomFont(baseDir)
|
2020-10-09 19:28:09 +00:00
|
|
|
|
|
|
|
# set the news avatar
|
|
|
|
newsAvatarThemeFilename = \
|
2020-11-14 11:49:29 +00:00
|
|
|
baseDir + '/theme/' + name + '/icons/avatar_news.png'
|
2020-11-20 09:34:31 +00:00
|
|
|
if os.path.isdir(baseDir + '/accounts/news@' + domain):
|
|
|
|
if os.path.isfile(newsAvatarThemeFilename):
|
|
|
|
newsAvatarFilename = \
|
|
|
|
baseDir + '/accounts/news@' + domain + '/avatar.png'
|
|
|
|
copyfile(newsAvatarThemeFilename, newsAvatarFilename)
|
2020-10-09 19:28:09 +00:00
|
|
|
|
2020-07-10 18:08:45 +00:00
|
|
|
grayscaleFilename = baseDir + '/accounts/.grayscale'
|
|
|
|
if os.path.isfile(grayscaleFilename):
|
|
|
|
enableGrayscale(baseDir)
|
|
|
|
else:
|
|
|
|
disableGrayscale(baseDir)
|
2020-11-14 18:32:34 +00:00
|
|
|
|
2021-02-27 12:36:14 +00:00
|
|
|
_copyThemeHelpFiles(baseDir, name, systemLanguage)
|
2020-12-22 18:06:23 +00:00
|
|
|
_setThemeInConfig(baseDir, name)
|
2021-02-23 14:01:22 +00:00
|
|
|
_setClearCacheFlag(baseDir)
|
2020-05-26 20:17:16 +00:00
|
|
|
return result
|