color = {
{
hex, rgb,
text = "Main FG",
var = "main_fg"
},
{
hex, rgb,
text = "Secondary FG",
var = "secondary_fg"
},
{
hex, rgb,
text = "Main BG",
var = "main_bg"
},
{
hex, rgb,
text = "Sidebar BG, player BG",
var = "sidebar_and_player_bg"
},
{
hex, rgb,
text = "Cover overlay, Shadow",
var = "cover_overlay_and_shadow"
},
{
hex, rgb,
text = "Indicator FG, Button BG",
var = "indicator_fg_and_button_bg"
},
{
hex, rgb,
text = "Pressing FG",
var = "pressing_fg"
},
{
hex, rgb,
text = "Slider BG",
var = "slider_bg"
},
{
hex, rgb,
text = "Sidebar indicator, Hover button BG",
var = "sidebar_indicator_and_hover_button_bg"
},
{
hex, rgb,
text = "Scrollbar FG, Selected row BG",
var = "scrollbar_fg_and_selected_row_bg"
},
{
hex, rgb,
text = "Pressing button FG",
var = "pressing_button_fg"
},
{
hex, rgb,
text = "Pressing button BG",
var = "pressing_button_bg"
},
{
hex, rgb,
text = "Selected button",
var = "selected_button"
},
{
hex, rgb,
text = "Miscellaneous BG",
var = "miscellaneous_bg"
},
{
hex, rgb,
text = "Miscellaneous hover BG",
var = "miscellaneous_hover_bg"
},
{
hex, rgb,
text = "Preserve",
var = "preserve_1"
}
}
defaultSpotifyColor_Inc = table.concat({
'[Variables]',
'Main_FG = ffffff',
'Secondary_FG = c0c0c0',
'Main_BG = 282828',
'Sidebar_And_Player_BG = 000000',
'Cover_Overlay_And_Shadow = 000000',
'Indicator_FG_And_Button_BG = 1db954',
'Pressing_FG = cdcdcd',
'Slider_BG = 404040',
'Sidebar_Indicator_And_Hover_Button_BG = 1ed660',
'Scrollbar_FG_And_Selected_Row_BG = 333333',
'Pressing_Button_FG = cccccc',
'Pressing_Button_BG = 179443',
'Selected_Button = 18ac4d',
'Miscellaneous_BG = 4687d6',
'Miscellaneous_Hover_BG = 2e77d0',
'Preserve_1 = ffffff'
}, '\n')
defaultSpotifyColorList = {
"ffffff",
"c0c0c0",
"282828",
"000000",
"000000",
"1db954",
"cdcdcd",
"404040",
"1ed660",
"333333",
"cccccc",
"179443",
"18ac4d",
"4687d6",
"2e77d0",
"ffffff"
}
function Initialize()
-- Default Spotify colors
defaultSpotifyColor = {}
for i = 1, 16 do
defaultSpotifyColor[i] = {
hex = defaultSpotifyColorList[i],
rgb = hexToRGB(defaultSpotifyColorList[i]),
var = color[i].var
}
end
-- Parsing color from skin variables
local meter = 1
for k, v in ipairs(color) do
local rawColor = SKIN:GetVariable(v.var)
if (not rawColor or rawColor == '') then
local colorFilePath = SKIN:ReplaceVariables("#ROOTCONFIGPATH#Themes\\#CurrentTheme#\\color.inc")
local file = io.open(colorFilePath, 'r')
if (file) then
file:close()
SKIN:Bang('!WriteKeyValue', 'Variables', v.var, '8a4fff', colorFilePath)
SKIN:Bang('!SetVariable', v.var, '8a4fff')
rawColor = '8a4fff'
else
file = io.open(colorFilePath, 'w+')
file:write(defaultSpotifyColor_Inc)
file:close()
SKIN:Bang('!Refresh')
return
end
end
color[k].hex = parseColor(rawColor)
color[k].rgb = hexToRGB(color[k].hex)
SKIN:Bang('!SetOption', 'Box' .. meter, 'Color', 'Fill Color ' .. color[k].hex)
SKIN:Bang('!SetOption', 'Box' .. meter, 'LeftMouseUpAction', table.concat({'["#@#RainRGB4.exe" "VarName=', v.var, '" "FileName=#ROOTCONFIGPATH#Themes\\#CurrentTheme#\\color.inc\\" "RefreshConfig=#CURRENTCONFIG#"]'}))
local t = 'Text' .. meter
local tM = SKIN:GetMeter(t)
SKIN:Bang('!SetOption', t, 'Text', color[k].text)
SKIN:Bang('!UpdateMeter', t)
local s = 13
while tM:GetH() > 40 do
s = s - 1
SKIN:Bang('!SetOption', t, 'FontSize', s)
SKIN:Bang('!UpdateMeter', t)
end
meter = meter + 1
end
currentTheme = SKIN:GetVariable("CurrentTheme")
spotifyVer = SKIN:GetVariable("LastSpotifyVersion")
ParseCoreSetting()
backupCount = SKIN:GetMeasure("BackupFileCount")
backupName = SKIN:GetMeasure("BackupFileName")
spotifyCount = SKIN:GetMeasure("SpotifyFileCount")
cssName = SKIN:GetMeasure("CSSFileName")
jsName = SKIN:GetMeasure("JSFileName")
htmlName = SKIN:GetMeasure("HTMlFileName")
UpdateStatus('Please wait')
nC = 0
curSpa = 0
userCSSFile = SKIN:ReplaceVariables('#ROOTCONFIGPATH#Themes\\#CurrentTheme#\\user.css')
local tryToReadCSS = io.open(userCSSFile, 'r')
if (not tryToReadCSS) then
tryToReadCSS:close()
local f = io.open(userCSSFile, 'w+')
f:write('')
f:close()
end
DevTool_Button()
end
function ParseCoreSetting()
injectCSS = SKIN:GetVariable("Inject_CSS") == '1'
theme = SKIN:GetVariable("Replace_Colors") == '1'
radio = SKIN:GetVariable("Radio") == '1'
home = SKIN:GetVariable("Home") == '1'
sentry = SKIN:GetVariable("DisableSentry") == '1'
lyric_alwaysShow = SKIN:GetVariable("LyricAlwaysShow") == '1'
lyric_noSync = SKIN:GetVariable("LyricForceNoSync") == '1'
vis_highFramerate = SKIN:GetVariable("VisualizationHighFramerate") == '1'
experimentalFeatures = SKIN:GetVariable("ExperimentalFeatures") == '1'
fastUserSwitching = SKIN:GetVariable("FastUserSwitching") == '1'
logging = SKIN:GetVariable("DisableUILogging") == '1'
removeRTLRule = SKIN:GetVariable("RemoveRTLRule") == '1'
madeForYouHub = SKIN:GetVariable("MadeForYouHub") == '1'
songPage = SKIN:GetVariable("SongPage") == '1'
end
liveUpdate = false
initLive = true
liveUserCSS = ''
oldLiveUserCSS = ''
function Update()
if (totalSpa and not firstTimeMod) then
if (liveUpdate) then
local f = io.open(userCSSFile, 'r')
liveUserCSS = f:read('*a')
f:close()
if (initLive) then
oldLiveUserCSS = liveUserCSS
initLive = false
end
if (oldLiveUserCSS ~= liveUserCSS) then
oldLiveUserCSS = liveUserCSS
UpdateUserCSS()
end
end
if (#extensionWatch > 0) then
for k,v in pairs(extensionWatch) do
local f = io.open(SKIN:ReplaceVariables('#ROOTCONFIGPATH#Extensions\\' .. extensionTable[v.index].file), 'r')
local cur = f:read('*a')
f:close()
if (v.init) then
extensionWatch[k].old = cur
extensionWatch[k].init = false
end
if (extensionWatch[k].old ~= cur) then
extensionWatch[k].old = cur
Extension_Update(v.index)
end
end
end
end
liveUpdate = SKIN:GetVariable("LiveUpdate") == '1'
return status
end
function UpdateInitStatus()
totalSpa = backupCount:GetValue()
if (totalSpa == 0) then
SKIN:Bang('!HideMeterGroup', 'ClearBackupGroup')
SKIN:Bang('!SetOptionGroup', 'Button_Disabled', 'StrokeColor', 'Stroke Color 909090')
SKIN:Bang('!SetOptionGroup', 'Button_Disabled', 'LeftMouseUpAction', '')
SKIN:Bang('!SetOptionGroup', 'Button_Disabled', 'MouseOverAction', ' ')
SKIN:Bang('!SetOptionGroup', 'Button_Disabled', 'MouseLeaveAction', ' ')
SKIN:Bang('!SetOptionGroup', 'ButtonText_Disabled', 'FontColor', '909090')
if (UpdateSpotifyFolderStatus()) then
UpdateStatus('Something is wrong. Please reinstall Spotify before backup.', 'warn')
else
SKIN:Bang('!ShowMeterGroup', 'BackupButton')
SKIN:Bang('!ShowMeterGroup', 'Preprocess')
SKIN:Bang('!HideMeterGroup', 'CoreSettings')
UpdateStatus('Please backup first', 'ok')
end
else
SKIN:Bang('!HideMeterGroup', 'BackupButton')
SKIN:Bang('!HideMeterGroup', 'Preprocess')
SKIN:Bang('!ShowMeterGroup', 'CoreSettings')
if (UpdateSpotifyFolderStatus()) then
SKIN:Bang('!SetOption', 'ApplyButtonText', 'Text', 'Re-apply')
UpdateStatus('Ready', 'ok')
else
SKIN:Bang('!SetOptionGroup', 'Apply_Disabled', 'StrokeColor', 'Stroke Color 909090')
SKIN:Bang('!SetOptionGroup', 'Apply_Disabled', 'Color', 'Fill Color 0,0,0,0')
SKIN:Bang('!SetOptionGroup', 'Apply_Disabled', 'LeftMouseUpAction', '')
SKIN:Bang('!SetOptionGroup', 'Apply_Disabled', 'MouseOverAction', ' ')
SKIN:Bang('!SetOptionGroup', 'Apply_Disabled', 'MouseLeaveAction', ' ')
SKIN:Bang('!SetOptionGroup', 'ApplyText_Disabled', 'FontColor', '909090')
SKIN:Bang('!SetOption', 'ApplyButtonText', 'Text', 'Apply')
UpdateStatus('Ready. Please Apply at least one time.', 'ok')
firstTimeMod = true
end
local pref = io.open(SKIN:ReplaceVariables("%appdata%\\Spotify\\prefs"), 'r')
local currSpotifyVer = pref:read("*a"):match("app%.last%-launched%-version=\"(.-)\"")
pref:close()
if (currSpotifyVer ~= spotifyVer) then
UpdateStatus('Spotify version and backup version are mismatched. Please Clear Backup and Backup again.', 'warn')
end
spaList = {}
for i = 1, totalSpa do
SKIN:Bang('!SetOption', 'BackupFileName', 'Index', i)
SKIN:Bang('!UpdateMeasure', 'BackupFileName')
local name = backupName:GetStringValue():gsub("%.spa$", "")
table.insert(spaList, name)
end
end
SKIN:Bang('!UpdateMeter', '*')
SKIN:Bang('!Redraw')
end
function UpdateSpotifyFolderStatus()
SKIN:Bang('!UpdateMeasure', 'SpotifyFileCount')
return spotifyCount:GetValue() == 0
end
-- For progress bar
percent = 0
function GetPercent()
return percent
end
processSum = 0
curProcess = 0
function UpdatePercent(total)
if (total) then
processSum = total
curProcess = 0
return
end
curProcess = curProcess + 1
if (processSum == 0) then
percent = 1
else
percent = curProcess / processSum
end
SKIN:Bang('!UpdateMeter', 'PercentBar')
SKIN:Bang('!Redraw')
end
function UpdateStatus(text, kind)
if (kind == "warn") then
kind = "[\\xf057] "
elseif (kind == "ok") then
kind = "[\\xf118] "
elseif (kind == "done") then
kind = "[\\xf164] "
else
kind = "[\\xf254] "
end
status = text
SKIN:Bang('!SetOption', 'Status', 'Prefix', kind)
SKIN:Bang('!UpdateMeasure', 'Script')
SKIN:Bang('!UpdateMeter', 'Status')
SKIN:Bang('!Redraw')
end
function Init_Unzip()
bC = 0
SKIN:Bang('!SetOption', 'BackupFileCount', 'FinishAction', '[!UpdateMeasure BackupFileCount][!CommandMeasure Script "totalSpa=backupCount:GetValue();UpdatePercent(totalSpa);Unzip()"]')
SKIN:Bang('!UpdateMeasure', 'BackupFileCount')
SKIN:Bang('!CommandMeasure', 'BackupFileCount', 'Update')
end
function Unzip()
--Pre-process
if (nX) then
local p = "#@#Extracted\\Raw\\" .. nX .. "\\" .. nX .. ".bundle.js"
if (sentry) then
UpdateStatus("Removing Sentry of " .. n)
fileUtil(p, disableSentry)
end
if (logging) then
UpdateStatus("Removing UI logger of " .. n)
fileUtil(p, disableLogging)
end
if (removeRTLRule) then
UpdateStatus("Removing Right-To-Left style rules of " .. n)
local mainCSS = "#@#Extracted\\Raw\\" .. nX .. "\\css\\main.css"
fileUtil(mainCSS, removeRTL)
if (nX == "zlink" or nX == "login") then
local glueCSS = "#@#Extracted\\Raw\\" .. nX .. "\\css\\glue.css"
fileUtil(glueCSS, removeRTL)
end
end
fileUtil("#@#Extracted\\Raw\\" .. nX .. "\\index.html", function (data)
if (nX ~= "zlink" and nX ~= "login") then
data = data:gsub('css/glue%.css', 'https://zlink.app.spotify.com/css/glue.css', 1)
:gsub('', '%1', 1)
else
data = data:gsub('', '%1', 1)
end
return data
end
)
if (nX ~= "zlink" and nX ~= "login") then
local path = SKIN:ReplaceVariables("#@#Extracted\\Raw\\" .. nX .. "\\css\\glue.css")
local glue = io.open(path, 'r')
if (glue) then
glue:close()
os.remove(path)
end
end
end
bC = bC + 1
SKIN:Bang('!SetOption', 'BackupFileName', 'Index', bC)
SKIN:Bang('!UpdateMeasure', 'BackupFileName')
n = backupName:GetStringValue()
if not n or n == '' then
Duplicate()
nx = nil
return
end
nX = n:gsub('%.spa','')
UpdateStatus("Unzipping " .. n)
UpdatePercent()
SKIN:Bang('!SetOption', 'Unzip', 'Parameter', '"7z.exe x "Backup\\' .. n .. '" -oExtracted\\Raw\\' .. nX .. '\\ -r"')
SKIN:Bang('!UpdateMeasure', 'Unzip')
SKIN:Bang('!CommandMeasure', 'Unzip', 'Run')
end
function disableSentry(d)
d =d:gsub('sentry%.install%(%)[,;]', '', 1)
:gsub('"https://%w+@sentry.io/%d+"', '"https://NO@TELEMETRY.IS/BAD"')
return d
end
function disableLogging(d)
d = d:gsub('data%-log%-click="[%w%-]+"', '')
d = d:gsub('data%-log%-context="[%w%-]+"', '')
if (nX == "browse" or nX == "collection" or
nX == "genre" or nX == "hub") then
d =d:gsub('logUIInteraction5%([%w_]+,[%w_]+%)%{', '%1return;', 1)
:gsub('logUIImpression5%([%w_]+,[%w_]+%)%{', '%1return;', 1)
:gsub('_logUIInteraction5%([%w_]+%)%{', '%1return;', 1)
:gsub('_logUIImpression5%([%w_]+%)%{', '%1return;', 1)
:gsub('this%._documentFragment%.query%(\'%[data%-log%-click%]\'%)', 'return;%1')
:gsub('_onClickDataLogClick%([%w_]+%)%{', '%1return;')
:gsub('_setUpStandardImpressionLogging%(%)%{', '%1return;')
end
if (nX == "zlink") then
d = d:gsub('prototype._logUIInteraction5=function%(.-%){', '%1return;')
end
if (nX == "lyrics") then
d = d:gsub('LoggingService%.prototype%..-%{', '%1return;')
end
if (nX == "playlist") then
d = d:gsub('(exports%.logPlaylistImpression=)[%w_]+', '%1()=>{}', 1)
:gsub('(exports%.logEndOfListImpression=)[%w_]+', '%1()=>{}', 1)
:gsub('(exports%.logListQuickJump=)[%w_]+', '%1()=>{}', 1)
:gsub('(exports%.logListItemSelected=)[%w_]+', '%1()=>{}', 1)
:gsub('(exports%.logFeedbackInteraction=)[%w_]+', '%1()=>{}', 1)
end
return d
end
function removeRTL(d)
d =d:gsub(",%s?%[dir=rtl%].-(%{.-%})", "%1")
:gsub("%[dir=rtl%].-%{.-%}", "")
:gsub("%[dir=ltr%]", "")
:gsub("%[dir%]", "")
:gsub(",%s?%[lang=ar%].-(%{.-%})", "%1")
:gsub("%[lang=ar%].-%{.-%}", "")
:gsub("%[dir=\"rtl\"%].-%{.-%}", "")
:gsub("html%[dir=\"rtl\"%].-%{.-%}", "")
return d
end
function Duplicate()
SKIN:Bang('!SetOption', 'Duplicate', 'Parameter', 'robocopy "Raw" "Themed" *.css *.js *.html /S /COPY:D /R:50 /W:1 /NJH /NS')
SKIN:Bang('!UpdateMeasure', 'Duplicate')
SKIN:Bang('!CommandMeasure', 'Duplicate', 'Run')
UpdateStatus("Copying @Resource\\Raw to @Resource\\Themed")
end
function Init_PrepareCSS()
pC = 1
glue = nil
UpdateStatus("Preparing CSS files - Skin might be irresponsive, don't freak out.")
SKIN:Bang('!SetOption', 'CSSFileView', 'Path', '#@#Extracted\\Themed')
SKIN:Bang('!SetOption', 'CSSFileView', 'FinishAction', '[!CommandMeasure Script "PrepareCSS()"]')
SKIN:Bang('!UpdateMeasure', 'CSSFileView')
SKIN:Bang('!CommandMeasure', 'CSSFileView', 'Update')
end
function PrepareCSS()
SKIN:Bang('!SetOption', 'CSSFileName', 'Index', pC)
SKIN:Bang('!UpdateMeasure', 'CSSFileName')
local nP = cssName:GetStringValue()
if not nP or nP == '' then
local pref = io.open(SKIN:ReplaceVariables("%appdata%\\Spotify\\prefs"), 'r')
local currSpotifyVer = pref:read("*a"):match("app%.last%-launched%-version=\"(.-)\"")
pref:close()
SKIN:Bang('!WriteKeyValue', 'Variables', 'LastSpotifyVersion', currSpotifyVer)
SKIN:Bang('!Refresh')
return
end
local d = ''
if glue and nP:match("glus%.css$") then
d = glue
else
local f = io.open(nP, 'r')
d = f:read("*a")
f:close()
-- Replace default color scheme with our keywords.
-- When we apply custom color scheme, we just find
-- and replace keywords, no need to search color
-- again and again.
d = d:gsub("#1ed660", "var(--modspotify_sidebar_indicator_and_hover_button_bg)")
d = d:gsub("#1ed760", "var(--modspotify_sidebar_indicator_and_hover_button_bg)")
d = d:gsub("#1db954", "var(--modspotify_indicator_fg_and_button_bg)")
d = d:gsub("#1df369", "var(--modspotify_indicator_fg_and_button_bg)")
d = d:gsub("#1df269", "var(--modspotify_indicator_fg_and_button_bg)")
d = d:gsub("#1cd85e", "var(--modspotify_indicator_fg_and_button_bg)")
d = d:gsub("#1bd85e", "var(--modspotify_indicator_fg_and_button_bg)")
d = d:gsub("#18ac4d", "var(--modspotify_selected_button)")
d = d:gsub("#18ab4d", "var(--modspotify_selected_button)")
d = d:gsub("#179443", "var(--modspotify_pressing_button_bg)")
d = d:gsub("#14833b", "var(--modspotify_pressing_button_bg)")
d = d:gsub("#282828", "var(--modspotify_main_bg)")
d = d:gsub("#121212", "var(--modspotify_main_bg)")
d = d:gsub("#999999", "var(--modspotify_main_bg)")
d = d:gsub("#606060", "var(--modspotify_main_bg)")
d = d:gsub("rgba%(18,%s?18,%s?18,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("#181818", "var(--modspotify_sidebar_and_player_bg)")
d = d:gsub("rgba%(18,%s?19,%s?20,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("#000000", "var(--modspotify_sidebar_and_player_bg)")
d = d:gsub("#333333", "var(--modspotify_scrollbar_fg_and_selected_row_bg)")
d = d:gsub("#3f3f3f", "var(--modspotify_scrollbar_fg_and_selected_row_bg)")
d = d:gsub("#535353", "var(--modspotify_scrollbar_fg_and_selected_row_bg)")
d = d:gsub("#404040", "var(--modspotify_slider_bg)")
d = d:gsub("rgba%(80,%s?55,%s?80,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("rgba%(40,%s?40,%s?40,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("rgba%(40,%s?40,%s?40,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("rgba%(24,%s?24,%s?24,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("rgba%(18,%s?19,%s?20,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("#000011", "var(--modspotify_sidebar_and_player_bg)")
d = d:gsub("#0a1a2d", "var(--modspotify_sidebar_and_player_bg)")
d = d:gsub("#ffffff", "var(--modspotify_main_fg)")
d = d:gsub("#f8f8f7", "var(--modspotify_pressing_fg)")
d = d:gsub("#fcfcfc", "var(--modspotify_pressing_fg)")
d = d:gsub("#d9d9d9", "var(--modspotify_pressing_fg)")
d = d:gsub("#cdcdcd", "var(--modspotify_pressing_fg)")
d = d:gsub("#e6e6e6", "var(--modspotify_pressing_fg)")
d = d:gsub("#e5e5e5", "var(--modspotify_pressing_fg)")
d = d:gsub("#adafb2", "var(--modspotify_secondary_fg)")
d = d:gsub("#c8c8c8", "var(--modspotify_secondary_fg)")
d = d:gsub("#a0a0a0", "var(--modspotify_secondary_fg)")
d = d:gsub("#bec0bb", "var(--modspotify_secondary_fg)")
d = d:gsub("#bababa", "var(--modspotify_secondary_fg)")
d = d:gsub("#b3b3b3", "var(--modspotify_secondary_fg)")
d = d:gsub("#c0c0c0", "var(--modspotify_secondary_fg)")
d = d:gsub("rgba%(179,%s?179,%s?179,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_secondary_fg),%1)")
d = d:gsub("#cccccc", "var(--modspotify_pressing_button_fg)")
d = d:gsub("#ededed", "var(--modspotify_pressing_button_fg)")
d = d:gsub("#4687d6", "var(--modspotify_miscellaneous_bg)")
d = d:gsub("rgba%(70,%s?135,%s?214,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_miscellaneous_bg),%1)")
d = d:gsub("#2e77d0", "var(--modspotify_miscellaneous_hover_bg)")
d = d:gsub("rgba%(51,%s?153,%s?255,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_miscellaneous_hover_bg),%1)")
d = d:gsub("rgba%(30,%s?50,%s?100,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_miscellaneous_hover_bg),%1)")
d = d:gsub("rgba%(24,%s?24,%s?24,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("rgba%(25,%s?20,%s?20,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_sidebar_and_player_bg),%1)")
d = d:gsub("rgba%(160,%s?160,%s?160,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_pressing_button_fg),%1)")
d = d:gsub("rgba%(255,%s?255,%s?255,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_pressing_button_fg),%1)")
d = d:gsub("#ddd", "var(--modspotify_pressing_button_fg)")
d = d:gsub("#000", "var(--modspotify_sidebar_and_player_bg)")
d = d:gsub("#333", "var(--modspotify_scrollbar_fg_and_selected_row_bg)")
d = d:gsub("#444", "var(--modspotify_slider_bg)")
d = d:gsub("#fff", "var(--modspotify_main_fg)")
d = d:gsub("black;", " var(--modspotify_sidebar_and_player_bg)")
d = d:gsub("gray;", " var(--modspotify_main_bg)")
d = d:gsub("lightgray;", " var(--modspotify_pressing_button_fg)")
d = d:gsub("white;", " var(--modspotify_main_fg)")
d = d:gsub("rgba%(0,%s?0,%s?0,%s?([%d%.]+)%)", "rgba(var(--modspotify_rgb_cover_overlay_and_shadow),%1)")
d = d:gsub("#fff", "var(--modspotify_main_fg)")
d = d:gsub("#000", "var(--modspotify_sidebar_and_player_bg)")
--Because all glue.css in all spa are the same, so
--just store modded glue.css and we can apply to all remaining glue.css
if not glue and nP:match("glue%.css$") then
glue = d
end
end
f = io.open(nP, 'w')
f:write(d)
f:close()
pC = pC + 1
PrepareCSS()
end
function StartMod()
local totalApply = 3
+ (injectCSS and 1 or 0)
+ (theme and 1 or 0)
+ (radio and 1 or 0)
+ (home and 1 or 0)
+ (lyric_alwaysShow and 1 or 0)
+ (lyric_noSync and 1 or 0)
+ (experimentalFeatures and 1 or 0)
+ (fastUserSwitching and 1 or 0)
+ (vis_highFramerate and 1 or 0)
+ (madeForYouHub and 1 or 0)
+ (songPage and 1 or 0)
UpdatePercent(totalApply)
UpdatePercent()
CheckSpotifyFolder()
end
function CheckSpotifyFolder()
SKIN:Bang('!SetOption', 'SpotifyFileCount', 'FinishAction', '!CommandMeasure Script "DoesSpotifyNeedDelete()"')
SKIN:Bang('!UpdateMeasure', 'SpotifyFileCount')
SKIN:Bang('!CommandMeasure', 'SpotifyFileCount', 'Update')
end
function DoesSpotifyNeedDelete()
if (not UpdateSpotifyFolderStatus()) then
UpdateStatus("Removing Spotify\\Apps")
SKIN:Bang('!CommandMeasure', 'Remove', 'Run')
else
Transfer()
end
end
function Transfer()
UpdatePercent()
if theme then
UpdateStatus("Transferring Extracted\\Themed to Spotify")
SKIN:Bang('!SetOption', 'TransferMod', 'Parameter', 'robocopy "#@#Extracted\\Themed" "%appdata%\\Spotify\\Apps" *.css *.js *.html /S /COPY:D /R:10 /W:1 /NS /LOG:"#@#robocopy_transfer_log.txt"')
else
UpdateStatus("Transferring Extracted\\Raw to Spotify")
SKIN:Bang('!SetOption', 'TransferMod', 'Parameter', 'robocopy "#@#Extracted\\Raw" "%appdata%\\Spotify\\Apps" *.css *.js *.html /S /COPY:D /R:10 /W:1 /NS /LOG:"#@#robocopy_transfer_log.txt"')
end
SKIN:Bang('!UpdateMeasure', 'TransferMod')
SKIN:Bang('!CommandMeasure', 'TransferMod', 'Run')
end
function Restored()
UpdateStatus('Restore succeeded')
SKIN:Bang('!SetOption', 'SpotifyFileCount', 'FinishAction', '!CommandMeasure Script "UpdateInitStatus()"')
SKIN:Bang('!UpdateMeasure', 'SpotifyFileCount')
SKIN:Bang('!CommandMeasure', 'SpotifyFileCount', 'Update')
end
function parseColor(raw)
local hex = ''
--RRR,GGG,BBB
if raw:find(',') then
for c in raw:gmatch('%d+') do
c = string.format("%x", c)
hex = hex .. c
end
else
hex = raw
end
local r = 6 - hex:len()
--Less than 6 hex
if r > 0 then
for i = 1, r do
hex = hex .. 'f'
end
-- More than 6 hex
elseif r < 0 then
while hex:len() ~= 6 do
hex = hex:sub(1, -2)
end
end
return hex
end
function hexToRGB(hex)
local rgb = {}
for h in hex:gmatch("..") do
table.insert(rgb, tonumber(h, 16))
end
return table.concat(rgb, ',')
end
--Open file and return file handle in write mode + file data
function fileUtil(path, callback)
path = SKIN:ReplaceVariables(path)
local f = io.open(path, 'r')
local d = nil
if (f) then
d = f:read('*a')
f:close()
f = io.open(path, 'w')
d = callback(d)
f:write(d)
f:close()
return true
end
return false
end
function NameToIndex(nameTable, name)
for i = 1, #nameTable do
if name == nameTable[i] then
return i
end
end
return nil
end
themeTable = {}
function UpdateTheme()
local themeCount = SKIN:GetMeasure("ThemeFolderCount"):GetValue()
if (themeCount == 0) then
print('No theme found in Themes folder.')
return
end
themeName = SKIN:GetMeasure("ThemeFolderName")
for i = 1, themeCount do
SKIN:Bang('!SetOption', 'ThemeFolderName', 'Index', i)
SKIN:Bang('!UpdateMeasure', 'ThemeFolderName')
table.insert(themeTable, themeName:GetStringValue())
end
currentThemeIndex = NameToIndex(themeTable, currentTheme)
if not currentThemeIndex then
--Fallback to first theme if cannot find current theme name in Themes folder.
SKIN:Bang('!WriteKeyValue', 'Variables', 'CurrentTheme', themeTable[1])
SKIN:Bang('!Refresh')
return
end
if currentThemeIndex > 1 then
SKIN:Bang('!ShowMeter', 'ThemeBack')
SKIN:Bang('!HideMeter', 'ThemeBack_Disabled')
end
if (themeCount - currentThemeIndex) > 0 then
SKIN:Bang('!ShowMeter', 'ThemeNext')
SKIN:Bang('!HideMeter', 'ThemeNext_Disabled')
end
end
function ThemeChange(dir)
currentThemeIndex = currentThemeIndex + dir
SKIN:Bang('!SetOption', 'ThemeFolderName', 'Index', currentThemeIndex)
SKIN:Bang('!UpdateMeasure', 'ThemeFolderName')
SKIN:Bang('!WriteKeyValue', 'Variables', 'CurrentTheme', themeName:GetStringValue())
SKIN:Bang('!Refresh')
end
function ThemeNew()
local name = "New_Theme"
local n = 1
while NameToIndex(themeTable, name) do
n = n + 1
name = "New_Theme_" .. n
end
newThemeFolder = SKIN:ReplaceVariables('#ROOTCONFIGPATH#Themes\\' .. name)
SKIN:Bang('!WriteKeyValue', 'Variables', 'CurrentTheme', name)
SKIN:Bang('!SetOption', 'ThemeRunCommand', 'Parameter', 'mkdir "' .. newThemeFolder .. '"')
SKIN:Bang('!SetOption', 'ThemeRunCommand', 'FinishAction', '!CommandMeasure Script "ThemeNewContent()"')
SKIN:Bang('!UpdateMeasure', 'ThemeRunCommand')
SKIN:Bang('!CommandMeasure', 'ThemeRunCommand', 'Run')
end
function ThemeNewContent()
local file = io.open(newThemeFolder .. '\\color.inc', 'w+')
file:write(defaultSpotifyColor_Inc)
file:close()
file = io.open(newThemeFolder .. '\\user.css', 'w+')
file:write()
file:close()
SKIN:Bang('!Refresh')
end
function ThemeDuplicate()
local name = currentTheme .. '_2'
local n = 2
while NameToIndex(themeTable, name) do
n = n + 1
name = currentTheme .. '_' .. n
end
SKIN:Bang('!SetOption', 'ThemeRunCommand', 'Parameter', table.concat(
{'robocopy "', currentTheme, '" "', name, '"'}
))
SKIN:Bang('!SetOption', 'ThemeRunCommand', 'FinishAction', '[!WriteKeyValue Variables CurrentTheme "' .. name .. '"][!Refresh]')
SKIN:Bang('!UpdateMeasure', 'ThemeRunCommand')
SKIN:Bang('!CommandMeasure', 'ThemeRunCommand', 'Run')
end
--Mod JS file
function ModJS(packName, jsFile, replaceTable, modded)
local jsSpo = SKIN:ReplaceVariables("%appdata%\\Spotify\\Apps\\" .. packName .. "\\" .. jsFile .. ".js")
fileUtil(jsSpo,
function (d)
for _,v in pairs(replaceTable) do
d = d:gsub(v[1], v[2], v[3] and v[3] or nil)
end
return d
end
)
end
--Mod HTML file
function ModHTML(packName, replaceTable, modded)
local htmlSpo = "%appdata%\\Spotify\\Apps\\" .. packName .. "\\index.html"
fileUtil(htmlSpo,
function(d)
for _,v in pairs(replaceTable) do
d = d:gsub(v[1], v[2], v[3] and v[3] or nil)
end
return d
end
)
end
--Inject extensions
function ModInjectExtension(packName, extensionFolder, extensionFile)
local extension = io.open(SKIN:ReplaceVariables(extensionFolder .. extensionFile), 'r')
if (extension) then
local extensionData = extension:read('*a')
extension:close()
extension = io.open(SKIN:ReplaceVariables("%appdata%\\Spotify\\Apps\\" .. packName .. "\\" .. extensionFile), 'w+')
if (extension) then
extension:write(extensionData)
extension:close()
else
print("Cannot inject extension" .. extensionFile .. ". Check %appdata%\\Spotify\\Apps\\" .. packName .. " folder to see if it exists.")
end
end
end
function CopyUserCSS()
UpdateStatus('Transferring user.css')
local d = {}
UpdatePercent()
table.insert(d, ':root {')
local colorTable = theme and color or defaultSpotifyColor
for k,v in ipairs(colorTable) do
table.insert(d, table.concat({
' --modspotify_', v.var, ':#', v.hex, ';\n',
' --modspotify_rgb_', v.var, ':', v.rgb, ';'
}))
end
table.insert(d, '}')
if injectCSS then
UpdatePercent()
local css = io.open(SKIN:ReplaceVariables("#ROOTCONFIGPATH#Themes\\#CurrentTheme#\\user.css"),'r')
local u = ''
if (css) then
u = css:read('*a')
css:close()
else
print('user.css is not found in theme folder. Please make one.')
end
table.insert(d, u)
end
for _, v in pairs({"zlink", "login"}) do
local f = io.open(SKIN:ReplaceVariables("%appdata%\\Spotify\\Apps\\" .. v .. "\\css\\user.css"), 'w+')
if (f) then
f:write(table.concat(d, '\n'))
f:close()
end
end
end
function UpdateUserCSS()
CopyUserCSS()
UpdateStatus('user.css is updated at ' .. os.date("%X %x"), 'done')
SKIN:Bang('!CommandMeasure', 'WebSocket', 'reloadspotify')
end
function findSymbol(info, package, key)
local p = SKIN:ReplaceVariables("%appdata%\\Spotify\\Apps\\" .. package .. "\\" .. package .. ".bundle.js")
local f = io.open(p, "r")
local d = f:read("*a")
f:close()
local k
for i = 1, #key do
k = {d:match(key[i])}
if (#k ~= 0) then
return unpack(k)
end
end
print("Cannot find symbol for " .. info .. " in package " .. package)
return nil
end
function Finishing()
CopyUserCSS()
local modTable = {}
if (radio) then
UpdateStatus('Enabling Radio')
table.insert(modTable, {'radioIsVisible=', '%1true||', 1})
UpdatePercent()
end
if (home) then
UpdateStatus('Enabling Home')
table.insert(modTable, {'this%._initialState%.isHomeEnabled', 'true', 1})
table.insert(modTable, {'[%w_]+(&&[%w_]+%.default%.createElement%([%w_]+%.default,{isActive:%/%^spotify:app:home%/)', 'true%1', 1})
table.insert(modTable, {'[%w_]+%.isHomeEnabled', 'true'})
UpdatePercent()
end
if (lyric_alwaysShow) then
UpdateStatus('Enabling Always show lyrics button')
table.insert(modTable, {'(lyricsEnabled%()[%w_]+&&%(.-%)', '%1true', 1})
UpdatePercent()
end
if (vis_highFramerate) then
ModJS('lyrics', 'bundle', {
{'[%w_]+%.highVisualizationFrameRate=','%1true||', 1}
})
UpdatePercent()
end
if (lyric_noSync) then
ModJS('lyrics', 'bundle', {
{'[%w_]+%.forceNoSyncLyrics=','%1true||', 1}
})
UpdatePercent()
end
if (experimentalFeatures) then
UpdateStatus('Enabling Experimental Features')
table.insert(modTable, {'[%w_]+(&&[%w_]+%.default.createElement%([%w_]+%.default,%{name:"experiments)', 'true%1', 1})
UpdatePercent()
end
if (fastUserSwitching) then
UpdateStatus('Enabling Fast user switching')
table.insert(modTable, {'[%w_]+(&&[%w_]+%.default.createElement%([%w_]+%.default,%{name:"switch%-user)', 'true%1', 1})
UpdatePercent()
end
if (madeForYouHub) then
UpdateStatus('Enabling Made For You hub')
table.insert(modTable, {'[%w_]+(&&[%w_]+%.default.createElement%([%w_]+%.default,%{isActive:/%^spotify:app:made%-for%-you)', 'true%1', 1})
UpdatePercent()
end
if (songPage) then
UpdateStatus('Enabling Song Page')
table.insert(modTable, {'window%.initialState%.isSongPageEnabled', 'true'})
UpdatePercent()
end
UpdateStatus('Injecting a websocket and jquery 3.3.1')
ModHTML('zlink', {
{'()', '%1'}
})
ModInjectExtension('zlink', "#@#JavascriptInject\\", 'spicetifyWrapper.js')
ModInjectExtension('zlink', "#@#JavascriptInject\\", 'jquery-3.3.1.min.js')
UpdateStatus('Leaking useful functions, objects');
-- Find PlayerUI symbol:
local playerUI = findSymbol("playerUI", "zlink", {
"([%w_]+)%.prototype.updateProgressBarLabels",
"([%w_]+)%.prototype._onConnectionStateChange"
})
if (playerUI ~= nil) then
table.insert(modTable, {playerUI .. '%.prototype%.setup=function%(%){', table.concat({'%1',
'Spicetify.Player.seek=(p)=>{if(p<=1)p=Math.round(p*(Spicetify.Player.data?Spicetify.Player.data.track.metadata.duration:0));this.seek(p)};',
'Spicetify.Player.getProgressMs=()=>this.progressbar.getRealValue();',
'Spicetify.Player.getProgressPercent=()=>this.progressbar.getPercentage();',
'Spicetify.Player.getDuration=()=>this.progressbar.getMaxValue();',
'Spicetify.Player.skipForward=(a=15e3)=>Spicetify.Player.seek(Spicetify.Player.getProgressMs()+a);',
'Spicetify.Player.skipBack=(a=15e3)=>Spicetify.Player.seek(Spicetify.Player.getProgressMs()-a);',
'Spicetify.Player.setVolume=(v)=>this.changeVolume(v, false);',
'Spicetify.Player.increaseVolume=()=>this.increaseVolume();',
'Spicetify.Player.decreaseVolume=()=>this.decreaseVolume();',
'Spicetify.Player.getVolume=()=>this.volumebar.getValue();',
'Spicetify.Player.next=()=>this._doSkipToNext();',
'Spicetify.Player.back=()=>this._doSkipToPrevious();',
'Spicetify.Player.togglePlay=()=>this._doTogglePlay();',
'Spicetify.Player.play=()=>{!this.playing() && this._doTogglePlay();};',
'Spicetify.Player.pause=()=>{this.playing() && this._doTogglePlay();};',
'Spicetify.Player.isPlaying=()=>this.progressbar.isPlaying();',
'Spicetify.Player.toggleShuffle=()=>this.toggleShuffle();',
'Spicetify.Player.getShuffle=()=>this.shuffle();',
'Spicetify.Player.setShuffle=(b)=>{this.shuffle(b)};',
'Spicetify.Player.toggleRepeat=()=>this.toggleRepeat();',
'Spicetify.Player.getRepeat=()=>this.repeat();',
'Spicetify.Player.setRepeat=(r)=>{this.repeat(r)};',
'Spicetify.Player.getMute=()=>this.mute();',
'Spicetify.Player.toggleMute=()=>this._doToggleMute();',
'Spicetify.Player.setMute=(b)=>{this.volumeEnabled()&&this.changeVolume(this._unmutedVolume,b)};',
'Spicetify.Player.thumbUp=()=>this.thumbUp();',
'Spicetify.Player.getThumbUp=()=>this.trackThumbedUp();',
'Spicetify.Player.thumbDown=()=>this.thumbDown();',
'Spicetify.Player.getThumbDown=()=>this.trackThumbedDown();',
'Spicetify.Player.formatTime=(ms)=>this._formatTime(ms);',
'Spicetify.Player.eventListeners={};',
'Spicetify.Player.addEventListener=(type,callback)=>{if(!(type in Spicetify.Player.eventListeners)){Spicetify.Player.eventListeners[type]=[]}Spicetify.Player.eventListeners[type].push(callback)};',
'Spicetify.Player.removeEventListener=(type,callback)=>{if(!(type in Spicetify.Player.eventListeners)){return}var stack=Spicetify.Player.eventListeners[type];for(var i=0,l=stack.length;i{if(!(event.type in Spicetify.Player.eventListeners)){return true}var stack=Spicetify.Player.eventListeners[event.type];for(var i=0,l=stack.length;i{if(e){console.log(e);return;}Spicetify.Queue=r.getJSONBody();});%1', 1})
end
table.insert(modTable, {'(const [%w_]+=function%([%w_]+,[%w_]+%){)(this%._bridge)', table.concat({'%1',
'Spicetify.addToQueue=(uri,callback)=>{uri=Spicetify.LibURI.from(uri);if(uri.type===Spicetify.LibURI.Type.ALBUM){this.getAlbumTracks(uri,(err,tracks)=>{if(err){console.log("Spicetify.addToQueue",err);return};this.queueTracks(tracks,callback)})}else if(uri.type===Spicetify.LibURI.Type.TRACK||uri.type===Spicetify.LibURI.Type.EPISODE){this.queueTracks([uri],callback)}else{console.log("Spicetify.addToQueue: Only Track, Album, Episode URIs are accepted")}};',
'Spicetify.removeFromQueue=(uri,callback)=>{if(Spicetify.Queue){var indices=[],uriObj=Spicetify.LibURI.from(uri);if(uriObj.type===Spicetify.LibURI.Type.ALBUM){this.getAlbumTracks(uriObj,(err,tracks)=>{if(err){console.log(err);return}tracks.forEach(t=>Spicetify.Queue.next_tracks.forEach((nt,index)=>t==nt.uri&&indices.push(index)))})}else if(uriObj.type===Spicetify.LibURI.Type.TRACK||uriObj.type===Spicetify.LibURI.Type.EPISODE){Spicetify.Queue.next_tracks.forEach((track,index)=>track.uri==uri&&indices.push(index))}else{console.log("Spicetify.removeFromQueue: Only Album, Track and Episode URIs are accepted")}indices=indices.reduce((a,b)=>{if(a.indexOf(b)<0){a.push(b)}return a},[]);this.removeTracksFromQueue(indices,callback)}};',
';%2'
}), 1})
-- Register play/pause state change event
table.insert(modTable, {'this%.playing%([%w_]+%.is_playing&&![%w_]+%.is_paused%).-;', '%1(this.playing()!==this._isPlaying)&&(this._isPlaying=this.playing(),Spicetify.Player.dispatchEvent&&Spicetify.Player.dispatchEvent(new Event("onplaypause")));', 1})
-- Register song change event
table.insert(modTable, {'this%._uri=[%w_]+%.uri,this%._trackMetadata=[%w_]+%.metadata', '%1,Spicetify.Player.dispatchEvent&&Spicetify.Player.dispatchEvent(new Event("songchange"))', 1})
-- Leak playbackControl to Spicetify.PlaybackControl
table.insert(modTable, {',(([%w_]+)%.playFromPlaylistResolver=)', ';Spicetify.PlaybackControl = %2;%1', 1})
-- Disable expose function restriction
table.insert(modTable, {'(expose=function.-)([%w_]+%.__spotify&&[%w_]+%.__spotify%.developer_mode&&)', '%1', 1})
ModJS('zlink', 'zlink.bundle', modTable)
local actExtension = Extension_ParseActivated()
for k, v in pairs(extensionTable) do
if (actExtension[v.file]) then
UpdateStatus('Injecting extension ' .. v.name)
ModHTML('zlink', {
{'(