From 3f3cf318be1e1c64825e56c82cc7478750238e69 Mon Sep 17 00:00:00 2001 From: amirchev Date: Fri, 10 Sep 2021 02:29:10 -0700 Subject: [PATCH 001/105] implement new text status control known bug: new song when made active will not be visible immediately --- lyrics.lua | 466 +++++++++++++++++++++++++++++------------------------ 1 file changed, 254 insertions(+), 212 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 494a960..387f2a9 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -112,7 +112,7 @@ useStatic = false link_text = false display_lines = 0 ensure_lines = true -visible = false +--visible = false displayed_song = "" lyrics = {} refrain = {} @@ -133,16 +133,28 @@ hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID script_sets = nil script_props = nil +TEXT_VISIBLE = 0 --text is visible +TEXT_HIDDEN = 1 --text is hidden +TEXT_SHOWING = 3 --going from hidden -> visible +TEXT_HIDING = 4 --going from visible -> hidden +TEXT_TRANSITION_OUT = 5 --fade out transition to next lyric +TEXT_TRANSITION_IN = 6 --fade in transition after lyric change +text_status = TEXT_VISIBLE text_opacity = 100 -text_fade_dir = 0 text_fade_speed = 1 text_fade_enabled = false scene_load_complete = false -update_lyrics_in_fade = false +--update_lyrics_in_fade = false load_scene = "" FirstTransition = false -------------------------------------------------------------------------- EVENTS + +-------- +---------------- +------------------------ CALLBACKS +---------------- +-------- + function sourceShowing() local source = obs.obs_get_source_by_name(source_name) local showing = false @@ -256,14 +268,14 @@ function next_lyric(pressed) else next_prepared(true) end - fade_lyrics_display() + transition_lyric_text(false) elseif #alternate>0 and alternateShowing() then -- Alternate is driving paging if display_index + 1 <= #alternate then display_index = display_index + 1 else next_prepared(true) end - fade_lyrics_display() + transition_lyric_text(false) else return end @@ -280,46 +292,36 @@ function prev_lyric(pressed) else prev_prepared(true) end - fade_lyrics_display() - elseif #alternate>0 and alternateShowing() then -- Alternate is driving paging + transition_lyric_text(false) + elseif #alternate > 0 and alternateShowing() then -- Alternate is driving paging if display_index > 1 then display_index = display_index - 1 else prev_prepared(true) end - fade_lyrics_display() + transition_lyric_text(false) else return end end -function clear_lyric(pressed) +function toggle_lyrics_visibility(pressed) if not pressed then return end - if #lyrics>0 and not sourceShowing() then + if #lyrics > 0 and not sourceShowing() then return end - if #alternate>0 and not alternateShowing() then + if #alternate > 0 and not alternateShowing() then return end - visible = not visible + --visible = not visible showHelp = not showHelp - fade_lyrics_display() -end - - -function fade_lyrics_display() - if not text_fade_enabled then - text_opacity = 100 - text_fade_dir = 2 - update_lyrics_display() + if text_status ~= TEXT_HIDDEN then + set_text_visiblity(TEXT_HIDDEN) else - update_lyrics_in_fade = true; - text_opacity = 99 - text_fade_dir = 1 -- fade out + set_text_visiblity(TEXT_VISIBLE) end - end function next_prepared(pressed) @@ -364,7 +366,8 @@ end function home_prepared(pressed) if not pressed then return false end - visible = true + --visible = true + set_text_visiblity(TEXT_VISIBLE) display_index = 0 prepared_index = 0 if #prepared_songs > 0 then @@ -377,7 +380,8 @@ end function home_song(pressed) if not pressed then return false end - visible = true + --visible = true + set_text_visiblity(TEXT_VISIBLE) if #prepared_songs > 0 then display_index = 1 end @@ -401,8 +405,8 @@ function prev_button_clicked(props, p) return true end -function clear_button_clicked(props, p) - clear_lyric(true) +function toggle_button_clicked(props, p) + toggle_lyrics_visibility(true) return true end @@ -416,68 +420,6 @@ function reset_button_clicked(props, p) return true end -function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) - local tableback = "#000000" - local text = "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "
" - text = text .. "
Prepared Song: " .. prepared_index - text = text .. " of " .. #prepared_songs .. "
" - text = text .. "
Lyric Page: " .. display_index - text = text .. " of " .. #lyrics .."
" - text = text .. "
" - if sourceActive() or alternateActive() or titleActive() or staticActive() then - if scene_load_complete and (prepared_index == 1) then - text = text .. "From: " - if load_scene ~= nil then - text = text .. load_scene .. "" - else - text = text .. "Prepared" - end - end - else - tableback = "#440000" - end - text = text .. "
" - if song ~= "" then - text = text .. "" - text = text .. "" - end - if lyric ~= "" then - text = text .. "" - text = text .. "" - end - if nextlyric ~= "" then - text = text .. "" - text = text .. "" - end - if alt ~= "" then - text = text .. "" - text = text .. "" - end - if nextalt ~= "" then - text = text .. "" - text = text .. "" - end - if nextsong ~= "" then - text = text .. "" - text = text .. "" - end - text = text .. "
Song
Title
" .. song .. "
Current
Page
• " .. lyric .. "
Next
Page
• " .. nextlyric .. "
Alt
Lyric
• " .. alt .. "
Next
Alt
• " .. nextalt .. "
Next
Song:
" .. nextsong .. "
" - local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") - file:write(text) - file:close() - return true -end - function save_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") @@ -488,7 +430,7 @@ function save_song_clicked(props, p) obs.obs_properties_apply_settings(props, script_sets) elseif displayed_song == name then prepare_lyrics(name) - fade_lyrics_display() + transition_lyric_text(false) end return true end @@ -527,30 +469,6 @@ function delete_song_clicked(props, p) return true end -function scene_prepare_selected(name) - if name == nil then return end - if name == "" then return end - if name == displayed_song then return end - if name == prepared_songs[1] then return end - local prop_prep_list = obs.obs_properties_get(script_props, "prop_prepared_list") - if scene_load_complete then - obs.obs_property_list_item_remove(prop_prep_list, 0) - table.remove(prepared_songs,1) -- clear older scene loaded song - end - obs.obs_property_list_insert_string(prop_prep_list,0, name,name) - table.insert(prepared_songs,1,name) - scene_load_complete = true - obs.obs_data_set_string(script_sets, "prop_prepared_list", name) - obs.obs_properties_apply_settings(script_props, script_sets) - prepare_lyrics(name) - save_prepared() - displayed_song = name - display_index = 1 - visible = true - fade_lyrics_display() - return true -end - -- prepare song button clicked function prepare_song_clicked(props, p) prepared_songs[#prepared_songs+1] = obs.obs_data_get_string(script_sets, "prop_directory_list") @@ -615,7 +533,8 @@ function prepare_selected(name) prepare_lyrics(name) if displayed_song ~= name then display_index = 1 - visible = true + --visible = true + set_text_visiblity(TEXT_VISIBLE) displayed_song = name update_lyrics_display() end @@ -650,7 +569,7 @@ function clear_prepared_clicked(props, p) lyrics = {} alternate = {} static = "" - fade_lyrics_display() + set_text_visiblity(TEXT_HIDDEN) local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) obs.obs_data_set_string(script_sets, "prop_prepared_list", "") @@ -686,11 +605,16 @@ function open_button_clicked(props, p) os.execute("xdg-open \"" .. path .. "\"") end end --------------------------------------------------------------- PROGRAM FUNCTIONS + +-------- +---------------- +------------------------ PROGRAM FUNCTIONS +---------------- +-------- -- updates the displayed lyrics function update_lyrics_display() - + print("Update lyrics display") local text = "" local alttext = "" local next_lyric = "" @@ -702,21 +626,21 @@ function update_lyrics_display() local source = obs.obs_get_source_by_name(source_name) local alt_source = obs.obs_get_source_by_name(alternate_source_name) - if visible then - text_fade_dir = 2 - init_opacity = 100 - if #lyrics > 0 and sourceShowing() then - if lyrics[display_index] ~= nil then - text = lyrics[display_index] - end + --if visible then + --text_fade_dir = 2 + --init_opacity = 100 + if #lyrics > 0 and sourceShowing() then + if lyrics[display_index] ~= nil then + text = lyrics[display_index] end - if #alternate > 0 and alternateShowing() then - if alternate[display_index] ~= nil then - alttext = alternate[display_index] - end - end + end + if #alternate > 0 and alternateShowing() then + if alternate[display_index] ~= nil then + alttext = alternate[display_index] + end + end - end + --end if link_text then if string.len(text) == 0 and string.len(alttext) == 0 then @@ -747,7 +671,7 @@ function update_lyrics_display() obs.obs_source_update(source, settings) obs.obs_data_release(settings) - next_lyric = lyrics[display_index+1] + next_lyric = lyrics[display_index + 1] if (next_lyric == nil) then next_lyric = "" end @@ -784,53 +708,96 @@ function update_lyrics_display() end end --- text_fade_dir = 1 to fade out and 2 to fade in +function set_text_visiblity(end_status) + --if already at desired visibility, then exit + if text_status == end_status then return end + --if fade is disabled, change visibility immediately + if not text_fade_enabled then + if end_status == TEXT_HIDDEN then + opacity = 0 + elseif end_status == TEXT_VISIBLE then + opacity = 100 + end + text_status = end_status + apply_source_opacity() + else + --if fade enabled, begin fade in or out + if end_status == TEXT_HIDDEN then + text_status = TEXT_HIDING + elseif end_status == TEXT_VISIBLE then + text_status = TEXT_SHOWING + end + end +end + +--transition to the next lyrics, use fade if enabled +--if lyrics are hidden, force_show set to true will make them visible +function transition_lyric_text(force_show) + --update the lyrics display immediately on 2 conditions + -- a) the text is hidden or hiding, and we will not force it to show + -- b) text fade is not enabled + -- otherwise, start text transition out and update the lyrics once + -- fade out transition is complete + if ((text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show) or not text_fade_enabled then + update_lyrics_display() + else + text_status = TEXT_TRANSITION_OUT + end +end + +function apply_source_opacity() + local source = obs.obs_get_source_by_name(source_name) + if source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + end + obs.obs_source_release(source) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + if alt_source ~= nil then + local alt_settings = obs.obs_data_create() + obs.obs_data_set_int(alt_settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(alt_settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(alt_source, alt_settings) + obs.obs_data_release(alt_settings) + end + obs.obs_source_release(alt_source) +end + function timer_callback() - if not in_timer and not pause_timer then - in_timer = true - if text_fade_dir > 0 then - local real_fade_speed = 1 + (text_fade_speed * 2) - if text_fade_dir == 1 then - if text_opacity > real_fade_speed then - text_opacity = text_opacity - real_fade_speed - else - text_fade_dir = 0 -- stop fading - text_opacity = 0 -- set to 0% - if update_lyrics_in_fade then - update_lyrics_display() - update_lyrics_in_fade = false - end - end - else - if text_opacity < 100 - real_fade_speed then - text_opacity = text_opacity + real_fade_speed - else - text_fade_dir = 0 -- stop fading - text_opacity = 100 -- set to 100% (TODO: REad initial text/outline opacity and scale it from there to zero instead) - end - end - local source = obs.obs_get_source_by_name(source_name) - if source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) - end - obs.obs_source_release(source) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - if alt_source ~= nil then - local Asettings = obs.obs_data_create() - obs.obs_data_set_int(Asettings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(Asettings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(alt_source, Asettings) - obs.obs_data_release(Asettings) + --if not in a transitory state, exit callback + if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then return end + --the amount we want to change opacity by + local opacity_delta = 1 + (text_fade_speed * 2) + --change opacity in the direction of transitory state + if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then + local new_opacity = text_opacity - opacity_delta + if new_opacity > 0 then + text_opacity = new_opacity + else + --completed fade out, determine next move + text_opacity = 0 + if text_status == TEXT_TRANSITION_OUT then + update_lyrics_display() + text_status = TEXT_TRANSITION_IN + else + text_status = TEXT_HIDDEN end - obs.obs_source_release(alt_source) end - in_timer = false + elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then + local new_opacity = text_opacity + opacity_delta + if new_opacity < 100 then + text_opacity = new_opacity + else + --completed fade in + text_opacity = 100 + text_status = TEXT_VISIBLE + end end - return + --apply the new opacity + apply_source_opacity() end -- prepares lyrics of the song @@ -1127,6 +1094,20 @@ function prepare_lyrics(name) pause_timer = false end +-- finds the index of a song in the directory +function get_index_in_list(list, q_item) + for index, item in ipairs(list) do + if item == q_item then return index end + end + return nil +end + +-------- +---------------- +------------------------ FILE FUNCTIONS +---------------- +-------- + -- loads the song directory function load_song_directory() pause_timer = true @@ -1256,16 +1237,68 @@ function load_prepared() return true end --- finds the index of a song in the directory -function get_index_in_list(list, q_item) - for index, item in ipairs(list) do - if item == q_item then return index end +function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) + local tableback = "#000000" + local text = "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "
" + text = text .. "
Prepared Song: " .. prepared_index + text = text .. " of " .. #prepared_songs .. "
" + text = text .. "
Lyric Page: " .. display_index + text = text .. " of " .. #lyrics .."
" + text = text .. "
" + if sourceActive() or alternateActive() or titleActive() or staticActive() then + if scene_load_complete and (prepared_index == 1) then + text = text .. "From: " + if load_scene ~= nil then + text = text .. load_scene .. "" + else + text = text .. "Prepared" + end + end + else + tableback = "#440000" + end + text = text .. "
" + if song ~= "" then + text = text .. "" + text = text .. "" end - return nil + if lyric ~= "" then + text = text .. "" + text = text .. "" + end + if nextlyric ~= "" then + text = text .. "" + text = text .. "" + end + if alt ~= "" then + text = text .. "" + text = text .. "" + end + if nextalt ~= "" then + text = text .. "" + text = text .. "" + end + if nextsong ~= "" then + text = text .. "" + text = text .. "" + end + text = text .. "
Song
Title
" .. song .. "
Current
Page
• " .. lyric .. "
Next
Page
• " .. nextlyric .. "
Alt
Lyric
• " .. alt .. "
Next
Alt
• " .. nextalt .. "
Next
Song:
" .. nextsong .. "
" + local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") + file:write(text) + file:close() + return true end ------------------------------------------------------------------ FILE FUNCTIONS - -- returns path of the given song name function get_song_file_path(name, suffix) if name == nil then return nil end @@ -1304,28 +1337,14 @@ function get_song_text(name) return song_lines end ----------------------------------------------------------- OBS DEFAULT FUNCTIONS - --------- UI --- song title textbox --- song text textarea --- save button --- song directory list --- preview song button --- prepare song button --- delete song button --- lines to display counter --- text source list --- prepared songs list --- clear prepared button --- advance lyric button --- go back lyric button --- show/hide lyrics button +-------- +---------------- +------------------------ OBS DEFAULT FUNCTIONS +---------------- +-------- -- A function named script_properties defines the properties that the user -- can change for the entire script module itself - - function script_properties() script_props = obs.obs_properties_create() obs.obs_properties_add_text(script_props, "prop_edit_song_title", "Song Title", obs.OBS_TEXT_DEFAULT) @@ -1399,7 +1418,7 @@ function script_properties() obs.obs_properties_add_button(script_props, "prop_clear_button", "Clear Prepared Songs", clear_prepared_clicked) obs.obs_properties_add_button(script_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(script_props, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(script_props, "prop_hide_button", "Show/Hide Lyrics", clear_button_clicked) + obs.obs_properties_add_button(script_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) obs.obs_properties_add_button(script_props, "prop_home_button", "Reset to Song Start", home_button_clicked) obs.obs_properties_add_button(script_props, "prop_reset_button", "Reset to First Song", reset_button_clicked) obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) @@ -1549,7 +1568,7 @@ function script_load(settings) obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) - hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", clear_lyric) + hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) @@ -1583,9 +1602,15 @@ function script_load(settings) prepare_selected(prepared_songs[1]) end obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for Source * Marker (WZ) - obs.timer_add(timer_callback, 100) -- Setup callback for text fade effect + obs.timer_add(timer_callback, 50) -- Setup callback for text fade effect end +-------- +---------------- +------------------------ SOURCE FUNCTIONS +---------------- +-------- + -- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) function rename_prepareLyric() pause_timer = true @@ -1715,15 +1740,32 @@ function loadSong(source, preview) if not preview or (preview and obs.obs_data_get_bool(settings, "inPreview")) then local song = obs.obs_data_get_string(settings, "songs") if song ~= displayed_song then - scene_prepare_selected(song) - prepared_index = 1 + print("Loading song from source") + if song == nil then return end + if song == "" then return end + if song == displayed_song then return end + if song == prepared_songs[1] then return end + local prop_prep_list = obs.obs_properties_get(script_props, "prop_prepared_list") + if scene_load_complete then + obs.obs_property_list_item_remove(prop_prep_list, 0) + table.remove(prepared_songs , 1) -- clear older scene loaded song + end + obs.obs_property_list_insert_string(prop_prep_list, 0, song, song) + table.insert(prepared_songs, 1, song) + scene_load_complete = true + obs.obs_data_set_string(script_sets, "prop_prepared_list", song) + obs.obs_properties_apply_settings(script_props, script_sets) + prepare_lyrics(song) + save_prepared() displayed_song = song + display_index = 1 + prepared_index = 1 set_current_scene_name() load_scene = current_scene update_lyrics_display() - text_opacity = 99 - text_fade_dir = 2 - fade_lyrics_display() + --text_opacity = 99 + --text_fade_dir = 2 + transition_lyric_text(true) end if obs.obs_data_get_bool(settings, "autoHome") then home_prepared(true) From f2a6107a9ded561496230992df9653ad5e7e678f Mon Sep 17 00:00:00 2001 From: amirchev Date: Wed, 15 Sep 2021 01:39:45 -0700 Subject: [PATCH 002/105] numerous changes systemic changes - text visibility - source - transition between paging - show/hide --- lyrics.lua | 906 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 511 insertions(+), 395 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 387f2a9..c550441 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -86,42 +86,50 @@ obs = obslua bit = require("bit") - +--source definitions source_data = {} source_def = {} source_def.id = "Prepare_Lyrics" source_def.type = OBS_SOURCE_TYPE_INPUT; -source_def.output_flags = bit.bor(obs.OBS_SOURCE_CUSTOM_DRAW ) +source_def.output_flags = bit.bor(obs.OBS_SOURCE_CUSTOM_DRAW) -obs = obslua +--text sources source_name = "" alternate_source_name = "" static_source_name = "" static_text = "" -current_scene = "" -preview_scene = "" +--current_scene = "" +--preview_scene = "" title_source_name = "" + +--settings windows_os = false first_open = true -in_timer = false -in_Load = false -in_directory = false -pause_timer = false -useAlternate = false -useStatic = false -link_text = false +--in_timer = false +--in_Load = false +--in_directory = false +--pause_timer = false display_lines = 0 ensure_lines = true --visible = false -displayed_song = "" + +--lyrics status +--TODO: removed displayed_song and use prepared_songs[prepared_index] +--displayed_song = "" lyrics = {} -refrain = {} +--refrain = {} alternate = {} -display_index = 0 -prepared_index = 0 +page_index = 0 +prepared_index = 0 --TODO: avoid setting prepared_index directly, use prepare_song_by_index song_directory = {} prepared_songs = {} -TextSources = {} +link_text = false +source_song_title = "" +using_source = false + +timer_exists = false + +--hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID hotkey_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_c_id = obs.OBS_INVALID_HOTKEY_ID @@ -130,9 +138,11 @@ hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID +--script placeholders script_sets = nil script_props = nil +--text status & fade TEXT_VISIBLE = 0 --text is visible TEXT_HIDDEN = 1 --text is hidden TEXT_SHOWING = 3 --going from hidden -> visible @@ -143,11 +153,18 @@ text_status = TEXT_VISIBLE text_opacity = 100 text_fade_speed = 1 text_fade_enabled = false -scene_load_complete = false + +--scene_load_complete = false --update_lyrics_in_fade = false -load_scene = "" +--load_scene = "" -FirstTransition = false +first_transition = false + +--simple debugging/print mechanism +DEBUG = true --on/off switch for entire debugging mechanism +DEBUG_METHODS = true --print method names +DEBUG_INNER = true --print inner method breakpoints +DEBUG_CUSTOM = true --print custom debugging messages -------- ---------------- @@ -155,196 +172,178 @@ FirstTransition = false ---------------- -------- -function sourceShowing() - local source = obs.obs_get_source_by_name(source_name) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function alternateShowing() - local source = obs.obs_get_source_by_name(alternate_source_name) - local showing = false - if source ~= nil then - obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function titleShowing() - local source = obs.obs_get_source_by_name(title_source_name) - local showing = false - if source ~= nil then - obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function staticShowing() - local source = obs.obs_get_source_by_name(static_source_name) - local showing = false - if source ~= nil then - obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function anythingActive() - return sourceActive() or alternateActive() or titleActive() or staticActive() -end - -function sourceActive() - - local source = obs.obs_get_source_by_name(source_name) - local active = false - if source ~= nil then - obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - active = obs.obs_source_active(source) - obs.obs_source_release(source) - end - return active -end - -function alternateActive() - - local source = obs.obs_get_source_by_name(alternate_source_name) - local active = false - if source ~= nil then - obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - active = obs.obs_source_active(source) - obs.obs_source_release(source) - end - return active -end - -function titleActive() - - local source = obs.obs_get_source_by_name(title_source_name) - local active = false - if source ~= nil then - obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - active = obs.obs_source_active(source) - obs.obs_source_release(source) - end - return active -end - -function staticActive() - - local source = obs.obs_get_source_by_name(static_source_name) - local active = false - if source ~= nil then - obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - active = obs.obs_source_active(source) - obs.obs_source_release(source) - end - return active -end +-- function sourceShowing() + -- local source = obs.obs_get_source_by_name(source_name) + -- local showing = false + -- if source ~= nil then + -- showing = obs.obs_source_showing(source) + -- end + -- obs.obs_source_release(source) + -- return showing +-- end + +-- function alternateShowing() + -- local source = obs.obs_get_source_by_name(alternate_source_name) + -- local showing = false + -- if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + -- showing = obs.obs_source_showing(source) + -- end + -- obs.obs_source_release(source) + -- return showing +-- end + +-- function titleShowing() + -- local source = obs.obs_get_source_by_name(title_source_name) + -- local showing = false + -- if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + -- showing = obs.obs_source_showing(source) + -- end + -- obs.obs_source_release(source) + -- return showing +-- end + +-- function staticShowing() + -- local source = obs.obs_get_source_by_name(static_source_name) + -- local showing = false + -- if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + -- showing = obs.obs_source_showing(source) + -- end + -- obs.obs_source_release(source) + -- return showing +-- end + +-- function anythingActive() + -- return sourceActive() or alternateActive() or titleActive() or staticActive() +-- end + +-- function sourceActive() + + -- local source = obs.obs_get_source_by_name(source_name) + -- local active = false + -- if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + -- active = obs.obs_source_active(source) + -- obs.obs_source_release(source) + -- end + -- return active +-- end + +-- function alternateActive() + + -- local source = obs.obs_get_source_by_name(alternate_source_name) + -- local active = false + -- if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + -- active = obs.obs_source_active(source) + -- obs.obs_source_release(source) + -- end + -- return active +-- end + +-- function titleActive() + + -- local source = obs.obs_get_source_by_name(title_source_name) + -- local active = false + -- if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + -- active = obs.obs_source_active(source) + -- obs.obs_source_release(source) + -- end + -- return active +-- end + +-- function staticActive() + + -- local source = obs.obs_get_source_by_name(static_source_name) + -- local active = false + -- if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + -- active = obs.obs_source_active(source) + -- obs.obs_source_release(source) + -- end + -- return active +-- end function next_lyric(pressed) if not pressed then return end + dbg_method("next_lyric") + --check if transition enabled if obs.obs_data_get_bool(script_sets, "transition_enabled") then - if not FirstTransition then + if not first_transition then obs.obs_frontend_preview_program_trigger_transition() - FirstTransition = true + first_transition = true return end end - if #lyrics > 0 and sourceShowing() then -- Lyrics is driving paging - if display_index + 1 <= #lyrics then - display_index = display_index + 1 - else - next_prepared(true) - end - transition_lyric_text(false) - elseif #alternate>0 and alternateShowing() then -- Alternate is driving paging - if display_index + 1 <= #alternate then - display_index = display_index + 1 - else - next_prepared(true) - end - transition_lyric_text(false) - else - return + if #lyrics > 0 or #alternate > 0 then--and sourceShowing() then -- Lyrics is driving paging + if page_index < #lyrics then + page_index = page_index + 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + next_prepared(true) + end end - end function prev_lyric(pressed) - if not pressed then - return + if not pressed then return end + dbg_method("prev_lyric") + if #lyrics > 0 or #alternate > 0 then --and sourceShowing() then -- Lyrics is driving paging + if page_index > 1 then + page_index = page_index - 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + prev_prepared(true) + end end - if #lyrics > 0 and sourceShowing() then -- Lyrics is driving paging - if display_index > 1 then - display_index = display_index - 1 - else - prev_prepared(true) - end - transition_lyric_text(false) - elseif #alternate > 0 and alternateShowing() then -- Alternate is driving paging - if display_index > 1 then - display_index = display_index - 1 - else - prev_prepared(true) - end - transition_lyric_text(false) - else - return +end + +function prev_prepared(pressed) + if not pressed then return end + if prepared_index > 1 then + using_source = false + prepare_selected(prepared_songs[prepared_index - 1]) + end +end + +function next_prepared(pressed) + if not pressed then return end + if prepared_index < #prepared_songs then + using_source = false + prepare_selected(prepared_songs[prepared_index + 1]) end end function toggle_lyrics_visibility(pressed) + dbg_method("toggle_lyrics_visibility") if not pressed then return end - if #lyrics > 0 and not sourceShowing() then - return - end - if #alternate > 0 and not alternateShowing() then - return - end + -- if #lyrics > 0 and not sourceShowing() then + -- return + -- end + -- if #alternate > 0 and not alternateShowing() then + -- return + -- end --visible = not visible - showHelp = not showHelp + --showHelp = not showHelp if text_status ~= TEXT_HIDDEN then + dbg_inner("hiding") set_text_visiblity(TEXT_HIDDEN) else + dbg_inner("showing") set_text_visiblity(TEXT_VISIBLE) end end -function next_prepared(pressed) - if not pressed then return false end - if prepared_index >= #prepared_songs then - return false - end - prepared_index = prepared_index + 1 - prepare_selected(prepared_songs[prepared_index]) - return true -end - -function prev_prepared(pressed) - if not pressed then return false end - if prepared_index == 1 then - return false - end - - prepared_index = prepared_index - 1 - prepare_selected(prepared_songs[prepared_index]) - return true -end - function get_load_lyric_song() local scene = obs.obs_frontend_get_current_scene() local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene @@ -366,34 +365,42 @@ end function home_prepared(pressed) if not pressed then return false end + dbg_method("home_prepared") --visible = true - set_text_visiblity(TEXT_VISIBLE) - display_index = 0 - prepared_index = 0 + --set_text_visiblity(TEXT_VISIBLE) + --page_index = 0 + --prepared_index = 0 + using_source = false if #prepared_songs > 0 then - display_index = 1 - prepared_index = 1 - prepare_selected(prepared_songs[prepared_index]) -- redundant from above + page_index = 1 + -- prepared_index = 1 + if not prepare_selected(prepared_songs[1]) then -- if song was not prepared, transition lyrics to page 1 manually + transition_lyric_text(false) + end + else + clear_prepared_clicked(true) end return true end function home_song(pressed) if not pressed then return false end + dbg_method("home_song") --visible = true - set_text_visiblity(TEXT_VISIBLE) + --set_text_visiblity(TEXT_VISIBLE) if #prepared_songs > 0 then - display_index = 1 + page_index = 1 + transition_lyric_text(false) end - prepare_selected(prepared_songs[prepared_index]) -- redundant from above + --prepare_selected(prepared_songs[prepared_index]) -- redundant from above return true end -function set_current_scene_name() - local scene = obs.obs_frontend_get_current_preview_scene() - current_scene = obs.obs_source_get_name(scene) - obs.obs_source_release(scene); -end +-- function set_current_scene_name() + -- local scene = obs.obs_frontend_get_current_preview_scene() + -- current_scene = obs.obs_source_get_name(scene) + -- obs.obs_source_release(scene); +-- end function next_button_clicked(props, p) next_lyric(true) @@ -423,21 +430,26 @@ end function save_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") - if save_song(name, text) then -- this is a new song + --if this is a new song, add it to the directory + if save_song(name, text) then local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") obs.obs_property_list_add_string(prop_dir_list, name, name) obs.obs_data_set_string(script_sets, "prop_directory_list", name) obs.obs_properties_apply_settings(props, script_sets) - elseif displayed_song == name then - prepare_lyrics(name) + elseif prepared_songs[prepared_index] == name then + --if this song is being displayed, then prepare it anew + prepare_song_by_name(name) transition_lyric_text(false) end return true end function delete_song_clicked(props, p) + dbg_method("delete_song_clicked") + --call delete song function local name = obs.obs_data_get_string(script_sets, "prop_directory_list") delete_song(name) + --update local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") for i = 0, obs.obs_property_list_item_count(prop_dir_list) do if obs.obs_property_list_item_string(prop_dir_list, i) == name then @@ -471,16 +483,17 @@ end -- prepare song button clicked function prepare_song_clicked(props, p) - prepared_songs[#prepared_songs+1] = obs.obs_data_get_string(script_sets, "prop_directory_list") + dbg_method("prepare_song_clicked") + prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) if #prepared_songs == 1 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - prepared_index = 1 + prepare_song_by_index(#prepared_songs) end obs.obs_properties_apply_settings(props, script_sets) save_prepared() - update_lyrics_display() + --update_source_text() return true end @@ -521,23 +534,30 @@ function refresh_button_clicked(props, p) end function prepare_selection_made(props, prop, settings) + dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") + using_source = false prepare_selected(name) return true end function prepare_selected(name) - if name == nil then return end - if name == "" then return end - if name == displayed_song then return end - prepare_lyrics(name) - if displayed_song ~= name then - display_index = 1 - --visible = true - set_text_visiblity(TEXT_VISIBLE) - displayed_song = name - update_lyrics_display() + dbg_method("prepare_selected: " .. name) + if name == nil then return false end + if name == "" then return false end + if name == prepared_songs[prepared_index] + or (using_source and name == source_song_title) then return false end + prepare_song_by_name(name) + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + transition_lyric_text(false) + else + source_song_title = name + transition_lyric_text(true) end + --visible = true + --set_text_visiblity(TEXT_VISIBLE) return true end @@ -546,7 +566,7 @@ end function preview_selection_made(props, prop, settings) local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - if get_index_in_list(song_directory, name) == nil then return end -- do nothing if invalid name + if get_index_in_list(song_directory, name) == nil then return false end -- do nothing if invalid name obs.obs_data_set_string(settings, "prop_edit_song_title", name) local song_lines = get_song_text(name) @@ -564,20 +584,23 @@ end -- removes prepared songs function clear_prepared_clicked(props, p) - scene_load_complete = false + dbg_method("clear_prepared_clicked") + --scene_load_complete = false prepared_songs = {} - lyrics = {} - alternate = {} - static = "" + -- lyrics = {} + -- alternate = {} + -- static = "" set_text_visiblity(TEXT_HIDDEN) + --clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) obs.obs_data_set_string(script_sets, "prop_prepared_list", "") obs.obs_properties_apply_settings(props, script_sets) save_prepared() - display_index = 0 + page_index = 0 prepared_index = 0 - displayed_song = "" + transition_lyric_text(false) + --displayed_song = "" return true end @@ -613,30 +636,40 @@ end -------- -- updates the displayed lyrics -function update_lyrics_display() - print("Update lyrics display") +function update_source_text() + dbg_method("update_source_text") + if prepared_index == nil or prepared_index == 0 then return end local text = "" local alttext = "" local next_lyric = "" local next_alternate = "" local static = static_text - local title = displayed_song - init_opacity = 0; + local title = "" + + if not using_source then + title = prepared_songs[prepared_index] + else + title = source_song_title + end + --init_opacity = 0; local source = obs.obs_get_source_by_name(source_name) local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) --if visible then --text_fade_dir = 2 --init_opacity = 100 - if #lyrics > 0 and sourceShowing() then - if lyrics[display_index] ~= nil then - text = lyrics[display_index] + --get text + if #lyrics > 0 then --and sourceShowing() then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] end end - if #alternate > 0 and alternateShowing() then - if alternate[display_index] ~= nil then - alttext = alternate[display_index] + if #alternate > 0 then --and alternateShowing() then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] end end @@ -649,66 +682,101 @@ function update_lyrics_display() end end - - if alt_source ~= nil then - local Asettings = obs.obs_data_create() - obs.obs_data_set_string(Asettings, "text", alttext) - obs.obs_data_set_int(Asettings, "opacity", init_opacity) - obs.obs_data_set_int(Asettings, "outline_opacity", init_opacity) - obs.obs_source_update(alt_source, Asettings) - obs.obs_data_release(Asettings) - - next_alternate = alternate[display_index+1] - if (next_alternate == nil) then - next_alternate = "" - end - end + -- update source texts if source ~= nil then local settings = obs.obs_data_create() obs.obs_data_set_string(settings, "text", text) - obs.obs_data_set_int(settings, "opacity", init_opacity) - obs.obs_data_set_int(settings, "outline_opacity", init_opacity) + --obs.obs_data_set_int(settings, "opacity", init_opacity) + --obs.obs_data_set_int(settings, "outline_opacity", init_opacity) obs.obs_source_update(source, settings) obs.obs_data_release(settings) - next_lyric = lyrics[display_index + 1] + next_lyric = lyrics[page_index + 1] if (next_lyric == nil) then next_lyric = "" end end - obs.obs_source_release(source) - obs.obs_source_release(alt_source) - local stat_source = obs.obs_get_source_by_name(static_source_name) + if alt_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", alttext) + --obs.obs_data_set_int(alt_settings, "opacity", init_opacity) + --obs.obs_data_set_int(alt_settings, "outline_opacity", init_opacity) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" + end + end if stat_source ~= nil then - local Xsettings = obs.obs_data_create() - obs.obs_data_set_string(Xsettings, "text", static) - obs.obs_source_update(stat_source, Xsettings) - obs.obs_data_release(Xsettings) + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) end - obs.obs_source_release(stat_source) - local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then - local Tsettings = obs.obs_data_create() - obs.obs_data_set_string(Tsettings, "text", title) - obs.obs_source_update(title_source, Tsettings) - obs.obs_data_release(Tsettings) + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) obs.obs_source_release(title_source) - local next_prepared = prepared_songs[prepared_index+1] + local next_prepared = prepared_songs[prepared_index + 1] if (next_prepared == nil) then next_prepared = "" end - update_monitor(displayed_song, text:gsub("\n","
• "), next_lyric:gsub("\n","
• "), alttext:gsub("\n","
• "), next_alternate:gsub("\n","
• "), next_prepared) + update_monitor(title, text:gsub("\n","
• "), next_lyric:gsub("\n","
• "), alttext:gsub("\n","
• "), next_alternate:gsub("\n","
• "), next_prepared) if obs.obs_data_get_bool(script_sets, "transition_enabled") then - if FirstTransition then + if first_transition then obs.obs_frontend_preview_program_trigger_transition() end end end +function apply_source_opacity() + local settings = obs.obs_data_create() + local source = obs.obs_get_source_by_name(source_name) + if source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(source, settings) + end + obs.obs_source_release(source) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + if alt_source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(alt_source, settings) + end + obs.obs_source_release(alt_source) + if text_status ~= TEXT_TRANSITION_IN and text_status ~= TEXT_TRANSITION_OUT then + local title_source = obs.obs_get_source_by_name(title_source_name) + if title_source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(title_source, settings) + end + obs.obs_source_release(title_source) + local static_source = obs.obs_get_source_by_name(static_source_name) + if static_source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(static_source, settings) + end + obs.obs_source_release(static_source) + end + obs.obs_data_release(settings) +end + function set_text_visiblity(end_status) + dbg_method("set_text_visiblity") --if already at desired visibility, then exit if text_status == end_status then return end --if fade is disabled, change visibility immediately @@ -720,57 +788,58 @@ function set_text_visiblity(end_status) end text_status = end_status apply_source_opacity() + dbg_inner("immediate visibility change") else - --if fade enabled, begin fade in or out + --if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then text_status = TEXT_HIDING elseif end_status == TEXT_VISIBLE then text_status = TEXT_SHOWING end + start_fade_timer() end end --transition to the next lyrics, use fade if enabled --if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) + dbg_method("transition_lyric_text") --update the lyrics display immediately on 2 conditions -- a) the text is hidden or hiding, and we will not force it to show -- b) text fade is not enabled -- otherwise, start text transition out and update the lyrics once -- fade out transition is complete - if ((text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show) or not text_fade_enabled then - update_lyrics_display() + if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then + update_source_text() + dbg_inner("hidden") + elseif not text_fade_enabled then + update_source_text() + set_text_visiblity(TEXT_VISIBLE) + dbg_inner("no text fade") else text_status = TEXT_TRANSITION_OUT + start_fade_timer() end end -function apply_source_opacity() - local source = obs.obs_get_source_by_name(source_name) - if source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) +function start_fade_timer() + if not timer_exists then + timer_exists = true + obs.timer_add(fade_callback, 50) + dbg_inner("started fade timer") end - obs.obs_source_release(source) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - if alt_source ~= nil then - local alt_settings = obs.obs_data_create() - obs.obs_data_set_int(alt_settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(alt_settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(alt_source, alt_settings) - obs.obs_data_release(alt_settings) - end - obs.obs_source_release(alt_source) end -function timer_callback() +function fade_callback() + dbg_method("fade_callback") --if not in a transitory state, exit callback - if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then return end + if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then + timer_exists = false + obs.remove_current_callback() + dbg_inner("ended fade timer") + end --the amount we want to change opacity by - local opacity_delta = 1 + (text_fade_speed * 2) + local opacity_delta = 1 + text_fade_speed --change opacity in the direction of transitory state if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then local new_opacity = text_opacity - opacity_delta @@ -780,7 +849,7 @@ function timer_callback() --completed fade out, determine next move text_opacity = 0 if text_status == TEXT_TRANSITION_OUT then - update_lyrics_display() + update_source_text() text_status = TEXT_TRANSITION_IN else text_status = TEXT_HIDDEN @@ -800,21 +869,29 @@ function timer_callback() apply_source_opacity() end +function prepare_song_by_index(index) + if index <= #prepared_songs then + prepare_song_by_name(prepared_songs[index]) + end +end + -- prepares lyrics of the song -function prepare_lyrics(name) - pause_timer = true - if name == nil then return end - FirstTransition = false +function prepare_song_by_name(name) + --pause_timer = true + if name == nil then return false end + first_transition = false local song_lines = get_song_text(name) local cur_line = 1 local cur_aline = 1 local recordRefrain = false local playRefrain = false + local use_alternate = false + local use_static = false local showText = true local commentBlock = false local singleAlternate = false - refrain = {} - arefrain = {} + local refrain = {} + local arefrain = {} lyrics = {} alternate = {} static_text = "" @@ -844,25 +921,25 @@ function prepare_lyrics(name) end local alternate_index = line:find("#A%[") if alternate_index ~= nil then - useAlternate = true + use_alternate = true line = line:sub(1, alternate_index - 1) new_lines = 0 end alternate_index = line:find("#A]") if alternate_index ~= nil then - useAlternate = false + use_alternate = false line = line:sub(1, alternate_index - 1) new_lines = 0 end local static_index = line:find("#S%[") if static_index ~= nil then - useStatic = true + use_static = true line = line:sub(1, static_index - 1) new_lines = 0 end static_index = line:find("#S]") if static_index ~= nil then - useStatic = false + use_static = false line = line:sub(1, static_index - 1) new_lines = 0 end @@ -871,7 +948,7 @@ function prepare_lyrics(name) if newcount_index ~= nil then local iS,iE = line:find("%d+",newcount_index+3) local newLines = tonumber(line:sub(iS,iE)) - if useAlternate then + if use_alternate then alternate_display_lines = newLines elseif recordRefrain then refrain_display_lines = newLines @@ -919,7 +996,7 @@ function prepare_lyrics(name) line = line:sub(1, refrain_index - 1) new_lines = 0 end - local refrain_index = line:find("#r%[") + refrain_index = line:find("#r%[") if refrain_index ~= nil then if next(refrain) ~= nil then for i, _ in ipairs(refrain) do refrain[i] = nil end @@ -951,12 +1028,12 @@ function prepare_lyrics(name) else playRefrain = false end - local newcount_index = line:find("#P:") + newcount_index = line:find("#P:") if newcount_index ~= nil then new_lines = tonumber(line:sub(newcount_index+3)) line = line:sub(1, newcount_index - 1) end - local newcount_index = line:find("#B:") + newcount_index = line:find("#B:") if newcount_index ~= nil then line = line:sub(1, newcount_index - 1) end @@ -964,20 +1041,20 @@ function prepare_lyrics(name) if phantom_index ~= nil then line = line:sub(1, phantom_index - 1) end - local phantom_index = line:find("##B") + phantom_index = line:find("##B") if phantom_index ~= nil then line = line:gsub("%s*##B%s*", "") .. "\n" --line = line:sub(1, phantom_index - 1) end if line ~= nil then - if useStatic then + if use_static then if static_text == "" then static_text = line else static_text = static_text .. "\n" .. line end else - if useAlternate or singleAlternate then + if use_alternate or singleAlternate then if recordRefrain then displaySize = refrain_display_lines else @@ -1076,7 +1153,7 @@ function prepare_lyrics(name) if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then for i = cur_line, displaySize, 1 do cur_line = i - if useAlternate then + if use_alternate then if showText and alternate[#alternate] ~= nil then alternate[#alternate] = alternate[#alternate] .. "\n" end @@ -1091,10 +1168,12 @@ function prepare_lyrics(name) end end lyrics[#lyrics + 1] = "" - pause_timer = false + --pause_timer = false + return true end -- finds the index of a song in the directory +--if item is not in list, then return nil function get_index_in_list(list, q_item) for index, item in ipairs(list) do if item == q_item then return index end @@ -1110,7 +1189,7 @@ end -- loads the song directory function load_song_directory() - pause_timer = true + --pause_timer = true song_directory = {} local filenames = {} local dir = obs.os_opendir(get_songs_folder_path())--get_songs_folder_path()) @@ -1130,7 +1209,7 @@ function load_song_directory() end until not entry obs.os_closedir(dir) - pause_timer = false + --pause_timer = false end -- delete previewed song @@ -1213,18 +1292,20 @@ end -- saves preprepared songs function save_prepared() + dbg_method("save_prepared") local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") for i, name in ipairs(prepared_songs) do - if not scene_load_complete or i > 1 then --don't save scene prepared songs + -- if not scene_load_complete or i > 1 then --don't save scene prepared songs file:write(name, "\n") - end + -- end end file:close() return true end function load_prepared() - pause_timer = true + dbg_method("load_prepared") + -- pause_timer = true local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") if file ~= nil then for line in file:lines() do @@ -1233,11 +1314,12 @@ function load_prepared() prepared_index = 1 file:close() end - pause_timer = false + -- pause_timer = false return true end function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) + dbg_method("update_monitor") local tableback = "#000000" local text = "" text = text .. "" @@ -1250,23 +1332,21 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) text = text .. "" text = text .. "" text = text .. "
" - text = text .. "
Prepared Song: " .. prepared_index - text = text .. " of " .. #prepared_songs .. "
" - text = text .. "
Lyric Page: " .. display_index + if not using_source then + text = text .. "
Prepared Song: " .. prepared_index + text = text .. " of " .. #prepared_songs .. "
" + end + text = text .. "
Lyric Page: " .. page_index text = text .. " of " .. #lyrics .."
" text = text .. "
" - if sourceActive() or alternateActive() or titleActive() or staticActive() then - if scene_load_complete and (prepared_index == 1) then - text = text .. "From: " - if load_scene ~= nil then - text = text .. load_scene .. "" - else - text = text .. "Prepared" - end - end + -- show if song is from source or prepared songs + text = text .. "From: " + if using_source then + text = text .. "Source" else - tableback = "#440000" - end + text = text .. "Prepared" + end + text = text .. "
" if song ~= "" then text = text .. "" @@ -1307,7 +1387,7 @@ end -- returns path of the lyrics songs folder function get_songs_folder_path() - local sep = package.config:sub(1,1) + local sep = package.config:sub(1, 1) local path = "" if windows_os then path = os.getenv("USERPROFILE") @@ -1346,6 +1426,7 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself function script_properties() + dbg_method("script_properties") script_props = obs.obs_properties_create() obs.obs_properties_add_text(script_props, "prop_edit_song_title", "Song Title", obs.OBS_TEXT_DEFAULT) local lyric_prop = obs.obs_properties_add_text(script_props, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) @@ -1421,7 +1502,9 @@ function script_properties() obs.obs_properties_add_button(script_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) obs.obs_properties_add_button(script_props, "prop_home_button", "Reset to Song Start", home_button_clicked) obs.obs_properties_add_button(script_props, "prop_reset_button", "Reset to First Song", reset_button_clicked) - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + if #prepared_songs > 0 and prepared_index > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[prepared_index]) + end obs.obs_properties_apply_settings(script_props, script_sets) @@ -1491,24 +1574,25 @@ function script_update(settings) end if reload then - if #prepared_songs > 0 and displayed_song ~= "" then - prepare_lyrics(displayed_song) - display_index = 1 - update_lyrics_display() + if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then + prepare_song_by_name(prepared_songs[prepared_index]) + page_index = 1 + transition_lyric_text(false) end end end -- A function named script_defaults will be called to set the default settings function script_defaults(settings) + dbg_method("script_defaults") obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - obs.obs_data_set_default_string(settings, "prop_source_list", prepared_songs[1] ) - if #prepared_songs ~= 0 then - displayed_song = prepared_songs[1] - prepared_index = 1 - else - displayed_song = "" - end + --obs.obs_data_set_default_string(settings, "prop_source_list", prepared_songs[1] ) + --if #prepared_songs ~= 0 then + -- prepared_songs[prepared_index] = prepared_songs[1] + -- prepared_index = 1 + --else + -- prepared_songs[prepared_index] = "" + --end if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions if windows_os then os.execute("mkdir \"" .. get_songs_folder_path() .. "\"") @@ -1558,6 +1642,7 @@ end -- a function named script_load will be called on startup function script_load(settings) + dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) local hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) @@ -1598,11 +1683,8 @@ function script_load(settings) if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions load_song_directory() load_prepared() - if #prepared_songs ~= 0 then - prepare_selected(prepared_songs[1]) - end - obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for Source * Marker (WZ) - obs.timer_add(timer_callback, 50) -- Setup callback for text fade effect + --obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for Source * Marker (WZ) + --obs.timer_add(timer_callback, 50) -- Setup callback for text fade effect end -------- @@ -1612,9 +1694,8 @@ end -------- -- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) -function rename_prepareLyric() - pause_timer = true - TextSources = {} +function rename_source() + --pause_timer = true local sources = obs.obs_enum_sources() if (sources ~= nil) then -- count and index sources @@ -1684,7 +1765,7 @@ function rename_prepareLyric() end end obs.source_list_release(sources) - pause_timer = false + --pause_timer = false end source_def.get_name = function() @@ -1692,7 +1773,7 @@ source_def.get_name = function() end source_def.update = function (data, settings) - rename_prepareLyric() -- Rename and Mark sources instantly on update (WZ) + rename_source() -- Rename and Mark sources instantly on update (WZ) end source_def.get_properties = function (data) @@ -1703,93 +1784,128 @@ source_def.get_properties = function (data) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(source_dir_list, name, name) end - obs.obs_properties_add_bool(props,"inPreview","Change Lyrics in Preview Mode") -- Option to load new lyric in preview mode - obs.obs_properties_add_bool(props,"autoHome","Home Lyrics with Scene") -- Option to home new lyric in preview mode return props + obs.obs_properties_add_bool(props, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode + obs.obs_properties_add_bool(props, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode return props end source_def.create = function(settings, source) data = {} sh = obs.obs_source_get_signal_handler(source) - obs.signal_handler_connect(sh,"activate",active) --Set Active Callback - obs.signal_handler_connect(sh,"show",showing) --Set Preview Callback + obs.signal_handler_connect(sh, "activate", source_active) --Set Active Callback + obs.signal_handler_connect(sh, "show", source_showing) --Set Preview Callback + obs.signal_handler_connect(sh, "hide", source_hidden) --Set Preview Callback return data end source_def.get_defaults = function(settings) - obs.obs_data_set_default_bool(settings, "inPreview", false) - obs.obs_data_set_default_string(settings,"index","0") + obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) + obs.obs_data_set_default_string(settings, "index", "0") end source_def.destroy = function(source) end -function on_event(event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then - set_current_scene_name() - rename_prepareLyric() - update_lyrics_display() - end +-- function on_event(event) + -- if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then + -- set_current_scene_name() + -- rename_source() + -- --update_source_text() + -- transition_lyric_text(false) + -- end -end +-- end -function loadSong(source, preview) +function load_song(source, preview) + dbg_method("load_song") local settings = obs.obs_source_get_settings(source) - if not preview or (preview and obs.obs_data_get_bool(settings, "inPreview")) then + if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - if song ~= displayed_song then - print("Loading song from source") - if song == nil then return end - if song == "" then return end - if song == displayed_song then return end - if song == prepared_songs[1] then return end - local prop_prep_list = obs.obs_properties_get(script_props, "prop_prepared_list") - if scene_load_complete then - obs.obs_property_list_item_remove(prop_prep_list, 0) - table.remove(prepared_songs , 1) -- clear older scene loaded song - end - obs.obs_property_list_insert_string(prop_prep_list, 0, song, song) - table.insert(prepared_songs, 1, song) - scene_load_complete = true - obs.obs_data_set_string(script_sets, "prop_prepared_list", song) - obs.obs_properties_apply_settings(script_props, script_sets) - prepare_lyrics(song) - save_prepared() - displayed_song = song - display_index = 1 - prepared_index = 1 - set_current_scene_name() - load_scene = current_scene - update_lyrics_display() + --if song ~= prepared_songs[prepared_index] then + if song == nil + or song == "" + then return end + dbg_inner("load_song: " .. song) + --local prop_prep_list = obs.obs_properties_get(script_props, "prop_prepared_list") + --if scene_load_complete then + -- obs.obs_property_list_item_remove(prop_prep_list, 0) + -- table.remove(prepared_songs , 1) -- clear older scene loaded song + --end + --obs.obs_property_list_insert_string(prop_prep_list, 0, song, song) + --table.insert(prepared_songs, 1, song) + --scene_load_complete = true + --obs.obs_data_set_string(script_sets, "prop_prepared_list", song) + --obs.obs_properties_apply_settings(script_props, script_sets) + + --update scene info + --set_current_scene_name() + --load_scene = current_scene + + using_source = true + + --prepare song and update lyrics + -- if (song ~= prepared_songs[prepared_index]) then + prepare_selected(song) + -- end + + --save_prepared() + --page_index = 1 + --prepared_index = 1 + + --update_source_text() --text_opacity = 99 --text_fade_dir = 2 - transition_lyric_text(true) - end - if obs.obs_data_get_bool(settings, "autoHome") then + --transition_lyric_text(true) + --end + -- TODO: ensure home on activate working correctly + if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end end obs.obs_data_release(settings) end -function active(cd) - local source = obs.calldata_source(cd,"source") +function source_active(cd) + local source = obs.calldata_source(cd, "source") if source == nil then return end - loadSong(source,false) + load_song(source, false) end -function showing(cd) - local source = obs.calldata_source(cd,"source") +function source_showing(cd) + local source = obs.calldata_source(cd, "source") if source == nil then return end - if sourceActive() then return end - loadSong(source,true) + --if sourceActive() then return end + load_song(source, true) end +function dbg(message) + if DEBUG then + print(message) + end +end + +function dbg_inner(message) + if DEBUG_INNER then + dbg("INNER: " .. message) + end +end + +function dbg_method(message) + if DEBUG_METHODS then + dbg("METHOD: " .. message) + end +end + +function dbg_custom(message) + if DEBUG_CUSTOM then + dbg("CUSTOM: " .. message) + end +end obs.obs_register_source(source_def); From 5bd3d237c32c5ee80a191368f8234246c55cb0c3 Mon Sep 17 00:00:00 2001 From: amirchev Date: Wed, 15 Sep 2021 10:37:55 -0700 Subject: [PATCH 003/105] ensure functionality of auxiliary features transition preview to program on lyric change formatting home song and home prepared functionality --- lyrics.lua | 3165 +++++++++++++++++++++++++++------------------------- 1 file changed, 1668 insertions(+), 1497 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index c550441..488068e 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -12,10 +12,8 @@ -- See the License for the specific language governing permissions and -- limitations under the License. - - ---TODO: refresh properties after next prepared selection ---TODO: add text formatting guide (Done 7/31/21) +-- TODO: refresh properties after next prepared selection +-- TODO: add text formatting guide (Done 7/31/21) -- Source updates by W. Zaggle (DCSTRATO) 12/3/2020 -- Fading Text Out/In with transition option 12/8/2020 @@ -23,14 +21,14 @@ -- Source updates by W. Zaggle (DCSTRATO) 1/24/2021 -- Added ##B as alternative to ##P -- Added #B:n and #P:n as way to add multiple blank lines all at once --- Added #R:n preceding text as a way to Duplicate the following text line n times --- Corrected possible timer recursion where timer function could take longer than 100ms callback interval and hang OBS +-- Added #R:n preceding text as a way to Duplicate the following text line n times +-- Corrected possible timer recursion where timer function could take longer than 100ms callback interval and hang OBS -- Source updates by W. Zaggle (DCSTRATO) 2/4/2021 -- Changed #R:n to #D:n (Duplicate Lines) -- Added #R[ and #R] on lines by themselves to bracket lines of Refrain -- Added ##R to repeat the lines bracketed by #R[ and #R] lines --- Made chage to showing() function maybe work better if not in studio mode +-- Made chage to showing() function maybe work better if not in studio mode -- Source updates by W. Zaggle (DCSTRATO) 2/13/21 -- Stability Issues @@ -42,7 +40,7 @@ -- Added code to instantly show/hide lyrics ignoring fade option (Should fade be optional?) -- New option to modify Title Text object with Song Title -- Added code to allow text to change in Preview mode if preview and active scene are the same (normally active text object prevents this change in preview) --- CLeared up Home and Reset. Home returns to start of current song. Reset goes back to 1st song. +-- CLeared up Home and Reset. Home returns to start of current song. Reset goes back to 1st song. -- Added new button/hot-key to allow for both Home and Reset functions. -- Allow Comment after #L:n markup in Lyrics @@ -54,17 +52,17 @@ -- Added lyric index update on Alternate if number of lyrics is zero, Text Source is not in Scene or Undefined -- Source update by W. Zaggle (DCSTRATO) 7/11/2021 --- Added encoding/decoding of song titles that are invalid file names. Files are encoded and saved as .enc files instead -- .txt files to maintain compatibility with prior versions. Invalid includes Unicoded titles and characters --- /:*?\"<>| which allows for a song title to include prior invalid characters and support other languages. +-- Added encoding/decoding of song titles that are invalid file names. Files are encoded and saved as .enc files instead -- .txt files to maintain compatibility with prior versions. Invalid includes Unicoded titles and characters +-- /:*?\"<>| which allows for a song title to include prior invalid characters and support other languages. -- For example a song title can now be "What Child is This?" or "Ơn lạ lùng" (Vietnamese for Amazing Grace) -- Source update by W. Zaggle (DCSTRATO) 7/31/2021 --- Added ablility to elect to link Title and Static text to blank with Lyrics at end of song (Requested Feature) +-- Added ablility to elect to link Title and Static text to blank with Lyrics at end of song (Requested Feature) -- Added html quick guide table to Script Page (Text Formatting Guide TODO) -- Source update by W. Zaggle (DCSTRATO) 8/6/2021 -- Added html Monitor Page for use in Browser Dock --- Added ##r with same funcation as ##R +-- Added ##r with same funcation as ##R -- Added #A:n Line Where n is number of pages to apply line to in Alternate Text Block -- Added #S: Line that adds a single Static Line to the static block -- #L:n now sets Lyrics, Refrain and Alternate Text block default number of lines per page (If in Alternate block or Refrain block it will override those lines per page) @@ -86,50 +84,51 @@ obs = obslua bit = require("bit") ---source definitions +-- source definitions source_data = {} source_def = {} source_def.id = "Prepare_Lyrics" -source_def.type = OBS_SOURCE_TYPE_INPUT; +source_def.type = OBS_SOURCE_TYPE_INPUT source_def.output_flags = bit.bor(obs.OBS_SOURCE_CUSTOM_DRAW) ---text sources +-- text sources source_name = "" alternate_source_name = "" static_source_name = "" static_text = "" ---current_scene = "" ---preview_scene = "" +-- current_scene = "" +-- preview_scene = "" title_source_name = "" ---settings +-- settings windows_os = false first_open = true ---in_timer = false ---in_Load = false ---in_directory = false ---pause_timer = false +-- in_timer = false +-- in_Load = false +-- in_directory = false +-- pause_timer = false display_lines = 0 ensure_lines = true ---visible = false +-- visible = false ---lyrics status ---TODO: removed displayed_song and use prepared_songs[prepared_index] ---displayed_song = "" +-- lyrics status +-- TODO: removed displayed_song and use prepared_songs[prepared_index] +-- displayed_song = "" lyrics = {} ---refrain = {} +-- refrain = {} alternate = {} -page_index = 0 -prepared_index = 0 --TODO: avoid setting prepared_index directly, use prepare_song_by_index +page_index = 1 +prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected song_directory = {} prepared_songs = {} link_text = false source_song_title = "" using_source = false +transition_enabled = false timer_exists = false ---hotkeys +-- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID hotkey_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_c_id = obs.OBS_INVALID_HOTKEY_ID @@ -138,33 +137,33 @@ hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID ---script placeholders +-- script placeholders script_sets = nil script_props = nil ---text status & fade -TEXT_VISIBLE = 0 --text is visible -TEXT_HIDDEN = 1 --text is hidden -TEXT_SHOWING = 3 --going from hidden -> visible -TEXT_HIDING = 4 --going from visible -> hidden -TEXT_TRANSITION_OUT = 5 --fade out transition to next lyric -TEXT_TRANSITION_IN = 6 --fade in transition after lyric change +-- text status & fade +TEXT_VISIBLE = 0 -- text is visible +TEXT_HIDDEN = 1 -- text is hidden +TEXT_SHOWING = 3 -- going from hidden -> visible +TEXT_HIDING = 4 -- going from visible -> hidden +TEXT_TRANSITION_OUT = 5 -- fade out transition to next lyric +TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change text_status = TEXT_VISIBLE text_opacity = 100 text_fade_speed = 1 text_fade_enabled = false ---scene_load_complete = false ---update_lyrics_in_fade = false ---load_scene = "" +-- scene_load_complete = false +-- update_lyrics_in_fade = false +-- load_scene = "" -first_transition = false +transition_completed = false ---simple debugging/print mechanism -DEBUG = true --on/off switch for entire debugging mechanism -DEBUG_METHODS = true --print method names -DEBUG_INNER = true --print inner method breakpoints -DEBUG_CUSTOM = true --print custom debugging messages +-- simple debugging/print mechanism +DEBUG = true -- on/off switch for entire debugging mechanism +DEBUG_METHODS = true -- print method names +DEBUG_INNER = true -- print inner method breakpoints +DEBUG_CUSTOM = true -- print custom debugging messages -------- ---------------- @@ -173,460 +172,482 @@ DEBUG_CUSTOM = true --print custom debugging messages -------- -- function sourceShowing() - -- local source = obs.obs_get_source_by_name(source_name) - -- local showing = false - -- if source ~= nil then - -- showing = obs.obs_source_showing(source) - -- end - -- obs.obs_source_release(source) - -- return showing +-- local source = obs.obs_get_source_by_name(source_name) +-- local showing = false +-- if source ~= nil then +-- showing = obs.obs_source_showing(source) +-- end +-- obs.obs_source_release(source) +-- return showing -- end -- function alternateShowing() - -- local source = obs.obs_get_source_by_name(alternate_source_name) - -- local showing = false - -- if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - -- showing = obs.obs_source_showing(source) - -- end - -- obs.obs_source_release(source) - -- return showing +-- local source = obs.obs_get_source_by_name(alternate_source_name) +-- local showing = false +-- if source ~= nil then +-- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status +-- showing = obs.obs_source_showing(source) +-- end +-- obs.obs_source_release(source) +-- return showing -- end -- function titleShowing() - -- local source = obs.obs_get_source_by_name(title_source_name) - -- local showing = false - -- if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - -- showing = obs.obs_source_showing(source) - -- end - -- obs.obs_source_release(source) - -- return showing +-- local source = obs.obs_get_source_by_name(title_source_name) +-- local showing = false +-- if source ~= nil then +-- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status +-- showing = obs.obs_source_showing(source) +-- end +-- obs.obs_source_release(source) +-- return showing -- end -- function staticShowing() - -- local source = obs.obs_get_source_by_name(static_source_name) - -- local showing = false - -- if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - -- showing = obs.obs_source_showing(source) - -- end - -- obs.obs_source_release(source) - -- return showing +-- local source = obs.obs_get_source_by_name(static_source_name) +-- local showing = false +-- if source ~= nil then +-- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status +-- showing = obs.obs_source_showing(source) +-- end +-- obs.obs_source_release(source) +-- return showing -- end -- function anythingActive() - -- return sourceActive() or alternateActive() or titleActive() or staticActive() +-- return sourceActive() or alternateActive() or titleActive() or staticActive() -- end -- function sourceActive() - -- local source = obs.obs_get_source_by_name(source_name) - -- local active = false - -- if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - -- active = obs.obs_source_active(source) - -- obs.obs_source_release(source) - -- end - -- return active +-- local source = obs.obs_get_source_by_name(source_name) +-- local active = false +-- if source ~= nil then +-- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status +-- active = obs.obs_source_active(source) +-- obs.obs_source_release(source) +-- end +-- return active -- end -- function alternateActive() - -- local source = obs.obs_get_source_by_name(alternate_source_name) - -- local active = false - -- if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - -- active = obs.obs_source_active(source) - -- obs.obs_source_release(source) - -- end - -- return active +-- local source = obs.obs_get_source_by_name(alternate_source_name) +-- local active = false +-- if source ~= nil then +-- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status +-- active = obs.obs_source_active(source) +-- obs.obs_source_release(source) +-- end +-- return active -- end -- function titleActive() - -- local source = obs.obs_get_source_by_name(title_source_name) - -- local active = false - -- if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - -- active = obs.obs_source_active(source) - -- obs.obs_source_release(source) - -- end - -- return active +-- local source = obs.obs_get_source_by_name(title_source_name) +-- local active = false +-- if source ~= nil then +-- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status +-- active = obs.obs_source_active(source) +-- obs.obs_source_release(source) +-- end +-- return active -- end -- function staticActive() - -- local source = obs.obs_get_source_by_name(static_source_name) - -- local active = false - -- if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - -- active = obs.obs_source_active(source) - -- obs.obs_source_release(source) - -- end - -- return active +-- local source = obs.obs_get_source_by_name(static_source_name) +-- local active = false +-- if source ~= nil then +-- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status +-- active = obs.obs_source_active(source) +-- obs.obs_source_release(source) +-- end +-- return active -- end function next_lyric(pressed) - if not pressed then - return - end - dbg_method("next_lyric") - --check if transition enabled - if obs.obs_data_get_bool(script_sets, "transition_enabled") then - if not first_transition then - obs.obs_frontend_preview_program_trigger_transition() - first_transition = true - return - end - end - - if #lyrics > 0 or #alternate > 0 then--and sourceShowing() then -- Lyrics is driving paging - if page_index < #lyrics then - page_index = page_index + 1 - dbg_inner("page_index: " .. page_index) - transition_lyric_text(false) - else - next_prepared(true) - end - end + if not pressed then + return + end + dbg_method("next_lyric") + -- check if transition enabled + if transition_enabled then + if not transition_completed then + obs.obs_frontend_preview_program_trigger_transition() + transition_completed = true + return + end + end + + if #lyrics > 0 or #alternate > 0 then -- and sourceShowing() then -- Lyrics is driving paging + if page_index < #lyrics then + page_index = page_index + 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + next_prepared(true) + end + end end function prev_lyric(pressed) - if not pressed then return end - dbg_method("prev_lyric") - if #lyrics > 0 or #alternate > 0 then --and sourceShowing() then -- Lyrics is driving paging - if page_index > 1 then - page_index = page_index - 1 - dbg_inner("page_index: " .. page_index) - transition_lyric_text(false) - else - prev_prepared(true) - end - end + if not pressed then + return + end + dbg_method("prev_lyric") + if #lyrics > 0 or #alternate > 0 then -- and sourceShowing() then -- Lyrics is driving paging + if page_index > 1 then + page_index = page_index - 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + prev_prepared(true) + end + end end function prev_prepared(pressed) - if not pressed then return end - if prepared_index > 1 then - using_source = false - prepare_selected(prepared_songs[prepared_index - 1]) - end + if not pressed then + return + end + if prepared_index > 1 then + using_source = false + prepare_selected(prepared_songs[prepared_index - 1]) + end end function next_prepared(pressed) - if not pressed then return end - if prepared_index < #prepared_songs then - using_source = false - prepare_selected(prepared_songs[prepared_index + 1]) - end + if not pressed then + return + end + if prepared_index < #prepared_songs then + using_source = false + prepare_selected(prepared_songs[prepared_index + 1]) + end end function toggle_lyrics_visibility(pressed) - dbg_method("toggle_lyrics_visibility") - if not pressed then - return - end - -- if #lyrics > 0 and not sourceShowing() then - -- return - -- end - -- if #alternate > 0 and not alternateShowing() then - -- return - -- end - --visible = not visible - --showHelp = not showHelp - if text_status ~= TEXT_HIDDEN then - dbg_inner("hiding") - set_text_visiblity(TEXT_HIDDEN) - else - dbg_inner("showing") - set_text_visiblity(TEXT_VISIBLE) - end + dbg_method("toggle_lyrics_visibility") + if not pressed then + return + end + -- if #lyrics > 0 and not sourceShowing() then + -- return + -- end + -- if #alternate > 0 and not alternateShowing() then + -- return + -- end + -- visible = not visible + -- showHelp = not showHelp + if text_status ~= TEXT_HIDDEN then + dbg_inner("hiding") + set_text_visiblity(TEXT_HIDDEN) + else + dbg_inner("showing") + set_text_visiblity(TEXT_VISIBLE) + end end function get_load_lyric_song() - local scene = obs.obs_frontend_get_current_scene() - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - local song = nil - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id - if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - song = obs.obs_data_get_string(settings, "song") -- Get index for this source (set earlier) - obs.obs_data_release(settings) -- release memory - end - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - return song + local scene = obs.obs_frontend_get_current_scene() + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + local song = nil + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id + if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + song = obs.obs_data_get_string(settings, "song") -- Get index for this source (set earlier) + obs.obs_data_release(settings) -- release memory + end + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + return song end function home_prepared(pressed) - if not pressed then return false end - dbg_method("home_prepared") - --visible = true - --set_text_visiblity(TEXT_VISIBLE) - --page_index = 0 - --prepared_index = 0 - using_source = false - if #prepared_songs > 0 then - page_index = 1 - -- prepared_index = 1 - if not prepare_selected(prepared_songs[1]) then -- if song was not prepared, transition lyrics to page 1 manually - transition_lyric_text(false) - end - else - clear_prepared_clicked(true) - end - return true + if not pressed then + return false + end + dbg_method("home_prepared") + -- visible = true + -- set_text_visiblity(TEXT_VISIBLE) + -- page_index = 0 + -- prepared_index = 0 + using_source = false + page_index = 0 + -- prepared_index = 1 + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + prepare_selected(prepared_songs[1]) + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + end + obs.obs_properties_apply_settings(props, script_sets) + + return true end function home_song(pressed) - if not pressed then return false end - dbg_method("home_song") - --visible = true - --set_text_visiblity(TEXT_VISIBLE) - if #prepared_songs > 0 then - page_index = 1 - transition_lyric_text(false) - end - --prepare_selected(prepared_songs[prepared_index]) -- redundant from above - return true + if not pressed then + return false + end + dbg_method("home_song") + -- visible = true + -- set_text_visiblity(TEXT_VISIBLE) + if #prepared_songs > 0 then + page_index = 1 + transition_lyric_text(false) + end + -- prepare_selected(prepared_songs[prepared_index]) -- redundant from above + return true end -- function set_current_scene_name() - -- local scene = obs.obs_frontend_get_current_preview_scene() - -- current_scene = obs.obs_source_get_name(scene) - -- obs.obs_source_release(scene); +-- local scene = obs.obs_frontend_get_current_preview_scene() +-- current_scene = obs.obs_source_get_name(scene) +-- obs.obs_source_release(scene); -- end function next_button_clicked(props, p) - next_lyric(true) - return true + next_lyric(true) + return true end function prev_button_clicked(props, p) - prev_lyric(true) - return true + prev_lyric(true) + return true end function toggle_button_clicked(props, p) - toggle_lyrics_visibility(true) - return true + toggle_lyrics_visibility(true) + return true end function home_button_clicked(props, p) - home_song(true) - return true + home_song(true) + return true end function reset_button_clicked(props, p) - home_prepared(true) - return true + home_prepared(true) + return true end function save_song_clicked(props, p) - local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") - local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") - --if this is a new song, add it to the directory - if save_song(name, text) then - local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") - obs.obs_property_list_add_string(prop_dir_list, name, name) - obs.obs_data_set_string(script_sets, "prop_directory_list", name) - obs.obs_properties_apply_settings(props, script_sets) - elseif prepared_songs[prepared_index] == name then - --if this song is being displayed, then prepare it anew - prepare_song_by_name(name) - transition_lyric_text(false) - end - return true + local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") + local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") + -- if this is a new song, add it to the directory + if save_song(name, text) then + local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") + obs.obs_property_list_add_string(prop_dir_list, name, name) + obs.obs_data_set_string(script_sets, "prop_directory_list", name) + obs.obs_properties_apply_settings(props, script_sets) + elseif prepared_songs[prepared_index] == name then + -- if this song is being displayed, then prepare it anew + prepare_song_by_name(name) + transition_lyric_text(false) + end + return true end function delete_song_clicked(props, p) - dbg_method("delete_song_clicked") - --call delete song function - local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - delete_song(name) - --update - local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") - for i = 0, obs.obs_property_list_item_count(prop_dir_list) do - if obs.obs_property_list_item_string(prop_dir_list, i) == name then - obs.obs_property_list_item_remove(prop_dir_list, i) - if i > 1 then i = i - 1 end - if #song_directory > 0 then - obs.obs_data_set_string(script_sets, "prop_directory_list", song_directory[i]) - else - obs.obs_data_set_string(script_sets, "prop_directory_list", "") - obs.obs_data_set_string(script_sets, "prop_edit_song_title", "") - obs.obs_data_set_string(script_sets, "prop_edit_song_text", "") - end - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - if get_index_in_list(prepared_songs, name) ~= nil then - if obs.obs_property_list_item_string(prop_prep_list, i) == name then - obs.obs_property_list_item_remove(prop_prep_list, i) - if i > 1 then i = i - 1 end - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[i]) - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - end - end - end - obs.obs_properties_apply_settings(props, script_sets) - return true - end - end - return true + dbg_method("delete_song_clicked") + -- call delete song function + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + delete_song(name) + -- update + local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") + for i = 0, obs.obs_property_list_item_count(prop_dir_list) do + if obs.obs_property_list_item_string(prop_dir_list, i) == name then + obs.obs_property_list_item_remove(prop_dir_list, i) + if i > 1 then + i = i - 1 + end + if #song_directory > 0 then + obs.obs_data_set_string(script_sets, "prop_directory_list", song_directory[i]) + else + obs.obs_data_set_string(script_sets, "prop_directory_list", "") + obs.obs_data_set_string(script_sets, "prop_edit_song_title", "") + obs.obs_data_set_string(script_sets, "prop_edit_song_text", "") + end + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + if get_index_in_list(prepared_songs, name) ~= nil then + if obs.obs_property_list_item_string(prop_prep_list, i) == name then + obs.obs_property_list_item_remove(prop_prep_list, i) + if i > 1 then + i = i - 1 + end + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[i]) + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + end + end + end + obs.obs_properties_apply_settings(props, script_sets) + return true + end + end + return true end -- prepare song button clicked function prepare_song_clicked(props, p) - dbg_method("prepare_song_clicked") - prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) - if #prepared_songs == 1 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - prepare_song_by_index(#prepared_songs) - end - obs.obs_properties_apply_settings(props, script_sets) - save_prepared() - --update_source_text() - return true + dbg_method("prepare_song_clicked") + prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) + if #prepared_songs == 1 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) + prepare_song_by_index(#prepared_songs) + end + obs.obs_properties_apply_settings(props, script_sets) + save_prepared() + -- update_source_text() + return true end function refresh_button_clicked(props, p) - local source_prop = obs.obs_properties_get(props,"prop_source_list") - local alternate_source_prop = obs.obs_properties_get(props,"prop_alternate_list") - local static_source_prop = obs.obs_properties_get(props,"prop_static_list") - local title_source_prop = obs.obs_properties_get(props,"prop_title_list") - obs.obs_property_list_clear(source_prop) -- clear current properties list - obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list - obs.obs_property_list_clear(static_source_prop) -- clear current properties list - obs.obs_property_list_clear(title_source_prop) -- clear current properties list - - local sources = obs.obs_enum_sources() - if sources ~= nil then - local n = {} - for _, source in ipairs(sources) do - source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n+1] = obs.obs_source_get_name(source) - end - end - table.sort(n) - obs.obs_property_list_add_string(source_prop, "", "") - obs.obs_property_list_add_string(title_source_prop, "", "") - obs.obs_property_list_add_string(alternate_source_prop, "", "") - obs.obs_property_list_add_string(static_source_prop, "", "") - for _, name in ipairs(n) do - obs.obs_property_list_add_string(source_prop, name, name) - obs.obs_property_list_add_string(title_source_prop, name, name) - obs.obs_property_list_add_string(alternate_source_prop, name, name) - obs.obs_property_list_add_string(static_source_prop, name, name) - end - end - obs.source_list_release(sources) - load_song_directory() - return true + local source_prop = obs.obs_properties_get(props, "prop_source_list") + local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") + local static_source_prop = obs.obs_properties_get(props, "prop_static_list") + local title_source_prop = obs.obs_properties_get(props, "prop_title_list") + obs.obs_property_list_clear(source_prop) -- clear current properties list + obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list + obs.obs_property_list_clear(static_source_prop) -- clear current properties list + obs.obs_property_list_clear(title_source_prop) -- clear current properties list + + local sources = obs.obs_enum_sources() + if sources ~= nil then + local n = {} + for _, source in ipairs(sources) do + source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + n[#n + 1] = obs.obs_source_get_name(source) + end + end + table.sort(n) + obs.obs_property_list_add_string(source_prop, "", "") + obs.obs_property_list_add_string(title_source_prop, "", "") + obs.obs_property_list_add_string(alternate_source_prop, "", "") + obs.obs_property_list_add_string(static_source_prop, "", "") + for _, name in ipairs(n) do + obs.obs_property_list_add_string(source_prop, name, name) + obs.obs_property_list_add_string(title_source_prop, name, name) + obs.obs_property_list_add_string(alternate_source_prop, name, name) + obs.obs_property_list_add_string(static_source_prop, name, name) + end + end + obs.source_list_release(sources) + load_song_directory() + return true end function prepare_selection_made(props, prop, settings) - dbg_method("prepare_selection_made") - local name = obs.obs_data_get_string(settings, "prop_prepared_list") - using_source = false + dbg_method("prepare_selection_made") + local name = obs.obs_data_get_string(settings, "prop_prepared_list") + using_source = false prepare_selected(name) - return true + return true end function prepare_selected(name) - dbg_method("prepare_selected: " .. name) - if name == nil then return false end - if name == "" then return false end - if name == prepared_songs[prepared_index] - or (using_source and name == source_song_title) then return false end - prepare_song_by_name(name) - page_index = 1 - if not using_source then - prepared_index = get_index_in_list(prepared_songs, name) - transition_lyric_text(false) - else - source_song_title = name - transition_lyric_text(true) - end - --visible = true - --set_text_visiblity(TEXT_VISIBLE) - return true + dbg_method("prepare_selected: " .. name) + if name == nil then + return false + end + if name == "" then + return false + end + if name == prepared_songs[prepared_index] or (using_source and name == source_song_title) then + return false + end + prepare_song_by_name(name) + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + transition_lyric_text(false) + else + source_song_title = name + transition_lyric_text(true) + end + -- visible = true + -- set_text_visiblity(TEXT_VISIBLE) + return true end - -- called when selection is made from directory list function preview_selection_made(props, prop, settings) - local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - - if get_index_in_list(song_directory, name) == nil then return false end -- do nothing if invalid name - - obs.obs_data_set_string(settings, "prop_edit_song_title", name) - local song_lines = get_song_text(name) - local combined_text = "" - for i, line in ipairs(song_lines) do - if (i < #song_lines) then - combined_text = combined_text .. line .. "\n" - else - combined_text = combined_text .. line - end - end - obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) - return true + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + + if get_index_in_list(song_directory, name) == nil then + return false + end -- do nothing if invalid name + + obs.obs_data_set_string(settings, "prop_edit_song_title", name) + local song_lines = get_song_text(name) + local combined_text = "" + for i, line in ipairs(song_lines) do + if (i < #song_lines) then + combined_text = combined_text .. line .. "\n" + else + combined_text = combined_text .. line + end + end + obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) + return true end -- removes prepared songs function clear_prepared_clicked(props, p) - dbg_method("clear_prepared_clicked") - --scene_load_complete = false - prepared_songs = {} - -- lyrics = {} - -- alternate = {} - -- static = "" - set_text_visiblity(TEXT_HIDDEN) - --clear the list - local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_clear(prep_prop) - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - obs.obs_properties_apply_settings(props, script_sets) - save_prepared() - page_index = 0 - prepared_index = 0 - transition_lyric_text(false) - --displayed_song = "" - return true + dbg_method("clear_prepared_clicked") + -- scene_load_complete = false + prepared_songs = {} + -- lyrics = {} + -- alternate = {} + -- static = "" + set_text_visiblity(TEXT_HIDDEN) + -- clear the list + local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prep_prop) + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + obs.obs_properties_apply_settings(props, script_sets) + save_prepared() + --page_index = 0 + prepared_index = 0 + transition_lyric_text(false) + -- displayed_song = "" + return true end function open_song_clicked(props, p) - local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - if testValid(name) then - path = get_song_file_path(name,".txt") - else - path = get_song_file_path(enc(name),".enc") - end - print (path) - if windows_os then - os.execute("explorer \"" .. path .. "\"") - else - os.execute("xdg-open \"" .. path .. "\"") - end - return true + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + print(path) + if windows_os then + os.execute('explorer "' .. path .. '"') + else + os.execute('xdg-open "' .. path .. '"') + end + return true end function open_button_clicked(props, p) - local path = get_songs_folder_path() - if windows_os then - os.execute("explorer \"" .. path .. "\"") - else - os.execute("xdg-open \"" .. path .. "\"") - end + local path = get_songs_folder_path() + if windows_os then + os.execute('explorer "' .. path .. '"') + else + os.execute('xdg-open "' .. path .. '"') + end end -------- @@ -637,548 +658,569 @@ end -- updates the displayed lyrics function update_source_text() - dbg_method("update_source_text") - if prepared_index == nil or prepared_index == 0 then return end - local text = "" - local alttext = "" - local next_lyric = "" - local next_alternate = "" - local static = static_text - local title = "" - - if not using_source then - title = prepared_songs[prepared_index] - else - title = source_song_title - end - --init_opacity = 0; - - local source = obs.obs_get_source_by_name(source_name) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - local stat_source = obs.obs_get_source_by_name(static_source_name) - local title_source = obs.obs_get_source_by_name(title_source_name) - - --if visible then - --text_fade_dir = 2 - --init_opacity = 100 - --get text - if #lyrics > 0 then --and sourceShowing() then - if lyrics[page_index] ~= nil then - text = lyrics[page_index] - end - end - if #alternate > 0 then --and alternateShowing() then - if alternate[page_index] ~= nil then - alttext = alternate[page_index] - end - end - - --end - - if link_text then - if string.len(text) == 0 and string.len(alttext) == 0 then - static = "" - title = "" - end - end - - -- update source texts - if source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", text) - --obs.obs_data_set_int(settings, "opacity", init_opacity) - --obs.obs_data_set_int(settings, "outline_opacity", init_opacity) - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) - - next_lyric = lyrics[page_index + 1] - if (next_lyric == nil) then - next_lyric = "" - end - end - if alt_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", alttext) - --obs.obs_data_set_int(alt_settings, "opacity", init_opacity) - --obs.obs_data_set_int(alt_settings, "outline_opacity", init_opacity) - obs.obs_source_update(alt_source, settings) - obs.obs_data_release(settings) - - next_alternate = alternate[page_index + 1] - if (next_alternate == nil) then - next_alternate = "" - end - end - if stat_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", static) - obs.obs_source_update(stat_source, settings) - obs.obs_data_release(settings) - end - if title_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", title) - obs.obs_source_update(title_source, settings) - obs.obs_data_release(settings) - end - -- release source references - obs.obs_source_release(source) - obs.obs_source_release(alt_source) - obs.obs_source_release(stat_source) - obs.obs_source_release(title_source) - - - local next_prepared = prepared_songs[prepared_index + 1] - if (next_prepared == nil) then - next_prepared = "" - end - update_monitor(title, text:gsub("\n","
• "), next_lyric:gsub("\n","
• "), alttext:gsub("\n","
• "), next_alternate:gsub("\n","
• "), next_prepared) - if obs.obs_data_get_bool(script_sets, "transition_enabled") then - if first_transition then - obs.obs_frontend_preview_program_trigger_transition() - end - end + dbg_method("update_source_text") + if prepared_index == nil or prepared_index == 0 then + return + end + local text = "" + local alttext = "" + local next_lyric = "" + local next_alternate = "" + local static = static_text + local title = "" + + if not using_source then + title = prepared_songs[prepared_index] + else + title = source_song_title + end + -- init_opacity = 0; + + local source = obs.obs_get_source_by_name(source_name) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) + + -- if visible then + -- text_fade_dir = 2 + -- init_opacity = 100 + -- get text + if #lyrics > 0 then -- and sourceShowing() then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] + end + end + if #alternate > 0 then -- and alternateShowing() then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] + end + end + + -- end + + if link_text then + if string.len(text) == 0 and string.len(alttext) == 0 then + static = "" + title = "" + end + end + + -- update source texts + if source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + -- obs.obs_data_set_int(settings, "opacity", init_opacity) + -- obs.obs_data_set_int(settings, "outline_opacity", init_opacity) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + + next_lyric = lyrics[page_index + 1] + if (next_lyric == nil) then + next_lyric = "" + end + end + if alt_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", alttext) + -- obs.obs_data_set_int(alt_settings, "opacity", init_opacity) + -- obs.obs_data_set_int(alt_settings, "outline_opacity", init_opacity) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" + end + end + if stat_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) + end + if title_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) + end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) + obs.obs_source_release(title_source) + + local next_prepared = prepared_songs[prepared_index + 1] + if (next_prepared == nil) then + next_prepared = "" + end + update_monitor( + title, + text:gsub("\n", "
• "), + next_lyric:gsub("\n", "
• "), + alttext:gsub("\n", "
• "), + next_alternate:gsub("\n", "
• "), + next_prepared + ) + -- if obs.obs_data_get_bool(script_sets, "transition_enabled") then + -- if transition_completed then + -- obs.obs_frontend_preview_program_trigger_transition() + -- transition_completed = true + -- end + -- end end function apply_source_opacity() - local settings = obs.obs_data_create() - local source = obs.obs_get_source_by_name(source_name) - if source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(source, settings) - end - obs.obs_source_release(source) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - if alt_source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(alt_source, settings) - end - obs.obs_source_release(alt_source) - if text_status ~= TEXT_TRANSITION_IN and text_status ~= TEXT_TRANSITION_OUT then - local title_source = obs.obs_get_source_by_name(title_source_name) - if title_source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(title_source, settings) - end - obs.obs_source_release(title_source) - local static_source = obs.obs_get_source_by_name(static_source_name) - if static_source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(static_source, settings) - end - obs.obs_source_release(static_source) - end - obs.obs_data_release(settings) + local settings = obs.obs_data_create() + local source = obs.obs_get_source_by_name(source_name) + if source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(source, settings) + end + obs.obs_source_release(source) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + if alt_source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(alt_source, settings) + end + obs.obs_source_release(alt_source) + if text_status ~= TEXT_TRANSITION_IN and text_status ~= TEXT_TRANSITION_OUT then + local title_source = obs.obs_get_source_by_name(title_source_name) + if title_source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(title_source, settings) + end + obs.obs_source_release(title_source) + local static_source = obs.obs_get_source_by_name(static_source_name) + if static_source ~= nil then + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(static_source, settings) + end + obs.obs_source_release(static_source) + end + obs.obs_data_release(settings) end function set_text_visiblity(end_status) - dbg_method("set_text_visiblity") - --if already at desired visibility, then exit - if text_status == end_status then return end - --if fade is disabled, change visibility immediately - if not text_fade_enabled then - if end_status == TEXT_HIDDEN then - opacity = 0 - elseif end_status == TEXT_VISIBLE then - opacity = 100 - end - text_status = end_status - apply_source_opacity() - dbg_inner("immediate visibility change") - else - --if fade enabled, begin fade in or out - if end_status == TEXT_HIDDEN then - text_status = TEXT_HIDING - elseif end_status == TEXT_VISIBLE then - text_status = TEXT_SHOWING - end - start_fade_timer() - end -end - ---transition to the next lyrics, use fade if enabled ---if lyrics are hidden, force_show set to true will make them visible + dbg_method("set_text_visiblity") + -- if already at desired visibility, then exit + if text_status == end_status then + return + end + -- if fade is disabled, change visibility immediately + if not text_fade_enabled then + if end_status == TEXT_HIDDEN then + opacity = 0 + elseif end_status == TEXT_VISIBLE then + opacity = 100 + end + text_status = end_status + apply_source_opacity() + dbg_inner("immediate visibility change") + else + -- if fade enabled, begin fade in or out + if end_status == TEXT_HIDDEN then + text_status = TEXT_HIDING + elseif end_status == TEXT_VISIBLE then + text_status = TEXT_SHOWING + end + start_fade_timer() + end +end + +-- transition to the next lyrics, use fade if enabled +-- if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) - dbg_method("transition_lyric_text") - --update the lyrics display immediately on 2 conditions - -- a) the text is hidden or hiding, and we will not force it to show - -- b) text fade is not enabled - -- otherwise, start text transition out and update the lyrics once - -- fade out transition is complete - if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then - update_source_text() - dbg_inner("hidden") - elseif not text_fade_enabled then - update_source_text() - set_text_visiblity(TEXT_VISIBLE) - dbg_inner("no text fade") - else - text_status = TEXT_TRANSITION_OUT - start_fade_timer() - end + dbg_method("transition_lyric_text") + -- update the lyrics display immediately on 2 conditions + -- a) the text is hidden or hiding, and we will not force it to show + -- b) text fade is not enabled + -- otherwise, start text transition out and update the lyrics once + -- fade out transition is complete + if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then + update_source_text() + dbg_inner("hidden") + elseif not text_fade_enabled then + update_source_text() + set_text_visiblity(TEXT_VISIBLE) + dbg_inner("no text fade") + else + text_status = TEXT_TRANSITION_OUT + start_fade_timer() + end end function start_fade_timer() - if not timer_exists then - timer_exists = true - obs.timer_add(fade_callback, 50) - dbg_inner("started fade timer") - end + if not timer_exists then + timer_exists = true + obs.timer_add(fade_callback, 50) + dbg_inner("started fade timer") + end end function fade_callback() - dbg_method("fade_callback") - --if not in a transitory state, exit callback - if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then - timer_exists = false - obs.remove_current_callback() - dbg_inner("ended fade timer") - end - --the amount we want to change opacity by - local opacity_delta = 1 + text_fade_speed - --change opacity in the direction of transitory state - if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then - local new_opacity = text_opacity - opacity_delta - if new_opacity > 0 then - text_opacity = new_opacity - else - --completed fade out, determine next move - text_opacity = 0 - if text_status == TEXT_TRANSITION_OUT then - update_source_text() - text_status = TEXT_TRANSITION_IN - else - text_status = TEXT_HIDDEN - end - end - elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then - local new_opacity = text_opacity + opacity_delta - if new_opacity < 100 then - text_opacity = new_opacity - else - --completed fade in - text_opacity = 100 - text_status = TEXT_VISIBLE - end - end - --apply the new opacity - apply_source_opacity() + dbg_method("fade_callback") + -- if not in a transitory state, exit callback + if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then + timer_exists = false + obs.remove_current_callback() + dbg_inner("ended fade timer") + end + -- the amount we want to change opacity by + local opacity_delta = 1 + text_fade_speed + -- change opacity in the direction of transitory state + if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then + local new_opacity = text_opacity - opacity_delta + if new_opacity > 0 then + text_opacity = new_opacity + else + -- completed fade out, determine next move + text_opacity = 0 + if text_status == TEXT_TRANSITION_OUT then + update_source_text() + text_status = TEXT_TRANSITION_IN + else + text_status = TEXT_HIDDEN + end + end + elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then + local new_opacity = text_opacity + opacity_delta + if new_opacity < 100 then + text_opacity = new_opacity + else + -- completed fade in + text_opacity = 100 + text_status = TEXT_VISIBLE + end + end + -- apply the new opacity + apply_source_opacity() end function prepare_song_by_index(index) - if index <= #prepared_songs then - prepare_song_by_name(prepared_songs[index]) - end + if index <= #prepared_songs then + prepare_song_by_name(prepared_songs[index]) + end end -- prepares lyrics of the song function prepare_song_by_name(name) - --pause_timer = true - if name == nil then return false end - first_transition = false - local song_lines = get_song_text(name) - local cur_line = 1 - local cur_aline = 1 - local recordRefrain = false - local playRefrain = false - local use_alternate = false - local use_static = false - local showText = true - local commentBlock = false - local singleAlternate = false - local refrain = {} - local arefrain = {} - lyrics = {} - alternate = {} - static_text = "" - local adjusted_display_lines = display_lines - local refrain_display_lines = display_lines - local alternate_display_lines = display_lines - local displaySize = display_lines - for _, line in ipairs(song_lines) do - local new_lines = 1 - local single_line = false - local comment_index = line:find("//%[") -- Look for comment block Set - if comment_index ~= nil then - commentBlock = true - line = line:sub(comment_index + 3) - end - comment_index = line:find("//]") -- Look for comment block Clear - if comment_index ~= nil then - commentBlock = false - line = line:sub(1, comment_index - 1) - new_lines = 0 - end - if not commentBlock then - local comment_index = line:find("%s*//") - if comment_index ~= nil then - line = line:sub(1, comment_index - 1) - new_lines = 0 - end - local alternate_index = line:find("#A%[") - if alternate_index ~= nil then - use_alternate = true - line = line:sub(1, alternate_index - 1) - new_lines = 0 - end - alternate_index = line:find("#A]") - if alternate_index ~= nil then - use_alternate = false - line = line:sub(1, alternate_index - 1) - new_lines = 0 - end - local static_index = line:find("#S%[") - if static_index ~= nil then - use_static = true - line = line:sub(1, static_index - 1) - new_lines = 0 - end - static_index = line:find("#S]") - if static_index ~= nil then - use_static = false - line = line:sub(1, static_index - 1) - new_lines = 0 - end - - local newcount_index = line:find("#L:") - if newcount_index ~= nil then - local iS,iE = line:find("%d+",newcount_index+3) - local newLines = tonumber(line:sub(iS,iE)) - if use_alternate then - alternate_display_lines = newLines - elseif recordRefrain then - refrain_display_lines = newLines - else - adjusted_display_lines = newLines - refrain_display_lines = newLines - alternate_display_lines = newLines - end - line = line:sub(1, newcount_index - 1) - new_lines = 0 --ignore line - end - local static_index = line:find("#S:") - if static_index ~= nil then - local static_indexEnd = line:find("%s+",static_index+1) - line = line:sub(static_indexEnd + 1) - static_text = line - new_lines = 0 - end - local alt_index = line:find("#A:") - if alt_index ~= nil then - local alt_indexStart,alt_indexEnd = line:find("%d+",alt_index+3) - new_lines = tonumber(line:sub(alt_indexStart,alt_indexEnd)) - _, alt_indexEnd = line:find("%s+",alt_indexEnd+1) - line = line:sub(alt_indexEnd + 1) - singleAlternate = true - end - if line:find("###") ~= nil then -- Look for single line - line = line:gsub("%s*###%s*", "") - single_line = true - end - local newcount_index = line:find("#D:") - if newcount_index ~= nil then - local newcount_indexStart,newcount_indexEnd = line:find("%d+",newcount_index+3) - new_lines = tonumber(line:sub(newcount_indexStart,newcount_indexEnd)) - _, newcount_indexEnd = line:find("%s+",newcount_indexEnd+1) - line = line:sub(newcount_indexEnd + 1) - end - local refrain_index = line:find("#R%[") - if refrain_index ~= nil then - if next(refrain) ~= nil then - for i, _ in ipairs(refrain) do refrain[i] = nil end - end - recordRefrain = true - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#r%[") - if refrain_index ~= nil then - if next(refrain) ~= nil then - for i, _ in ipairs(refrain) do refrain[i] = nil end - end - recordRefrain = true - showText = false - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#R]") - if refrain_index ~= nil then - recordRefrain = false - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#r]") - if refrain_index ~= nil then - recordRefrain = false - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("##R") - if refrain_index ~= nil then - playRefrain = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - else - playRefrain = false - end - newcount_index = line:find("#P:") - if newcount_index ~= nil then - new_lines = tonumber(line:sub(newcount_index+3)) - line = line:sub(1, newcount_index - 1) - end - newcount_index = line:find("#B:") - if newcount_index ~= nil then - line = line:sub(1, newcount_index - 1) - end - local phantom_index = line:find("##P") - if phantom_index ~= nil then - line = line:sub(1, phantom_index - 1) - end - phantom_index = line:find("##B") - if phantom_index ~= nil then - line = line:gsub("%s*##B%s*", "") .. "\n" - --line = line:sub(1, phantom_index - 1) - end - if line ~= nil then - if use_static then - if static_text == "" then - static_text = line - else - static_text = static_text .. "\n" .. line - end - else - if use_alternate or singleAlternate then - if recordRefrain then - displaySize = refrain_display_lines - else - displaySize = alternate_display_lines - end - if new_lines > 0 then - while (new_lines > 0) do - if recordRefrain then - if (cur_line == 1) then - arefrain[#refrain + 1] = line - else - arefrain[#refrain] = arefrain[#refrain] .. "\n" .. line - end - end - if showText and line ~= nil then - if (cur_aline == 1) then - alternate[#alternate + 1] = line - else - alternate[#alternate] = alternate[#alternate] .. "\n" .. line - end - end - cur_aline = cur_aline + 1 - if single_line or singleAlternate or cur_aline > displaySize then - if ensure_lines then - for i = cur_aline, displaySize, 1 do - cur_aline = i - if showText and alternate[#alternate] ~= nil then - alternate[#alternate] = alternate[#alternate] .. "\n" - end - if recordRefrain then - arefrain[#refrain] = arefrain[#refrain] .. "\n" - end - end - end - cur_aline = 1 - end - new_lines = new_lines - 1 - end - end - if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record - for _, refrain_line in ipairs(arefrain) do - alternate[#alternate + 1] = refrain_line - end - end - singleAlternate = false - else - if recordRefrain then - displaySize = refrain_display_lines - else - displaySize = adjusted_display_lines - end - if new_lines > 0 then - while (new_lines > 0) do - if recordRefrain then - if (cur_line == 1) then - refrain[#refrain + 1] = line - else - refrain[#refrain] = refrain[#refrain] .. "\n" .. line - end - end - if showText and line ~= nil then - if (cur_line == 1) then - lyrics[#lyrics + 1] = line - else - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line - end - end - cur_line = cur_line + 1 - if single_line or cur_line > displaySize then - if ensure_lines then - for i = cur_line, displaySize, 1 do - cur_line = i - if showText and lyrics[#lyrics] ~= nil then - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" - end - if recordRefrain then - refrain[#refrain] = refrain[#refrain] .. "\n" - end - end - end - cur_line = 1 - end - new_lines = new_lines - 1 - end - end - end - if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record - for _, refrain_line in ipairs(refrain) do - lyrics[#lyrics + 1] = refrain_line - end - end - end - end - end - end - if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then - for i = cur_line, displaySize, 1 do - cur_line = i - if use_alternate then - if showText and alternate[#alternate] ~= nil then - alternate[#alternate] = alternate[#alternate] .. "\n" - end - else - if showText and lyrics[#lyrics] ~= nil then - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" - end - end - if recordRefrain then - refrain[#refrain] = refrain[#refrain] .. "\n" - end - end - end - lyrics[#lyrics + 1] = "" - --pause_timer = false - return true + -- pause_timer = true + if name == nil then + return false + end + -- if using transition on lyric change, first transition + -- would be reset with new song prepared + transition_completed = false + local song_lines = get_song_text(name) + local cur_line = 1 + local cur_aline = 1 + local recordRefrain = false + local playRefrain = false + local use_alternate = false + local use_static = false + local showText = true + local commentBlock = false + local singleAlternate = false + local refrain = {} + local arefrain = {} + lyrics = {} + alternate = {} + static_text = "" + local adjusted_display_lines = display_lines + local refrain_display_lines = display_lines + local alternate_display_lines = display_lines + local displaySize = display_lines + for _, line in ipairs(song_lines) do + local new_lines = 1 + local single_line = false + local comment_index = line:find("//%[") -- Look for comment block Set + if comment_index ~= nil then + commentBlock = true + line = line:sub(comment_index + 3) + end + comment_index = line:find("//]") -- Look for comment block Clear + if comment_index ~= nil then + commentBlock = false + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + if not commentBlock then + local comment_index = line:find("%s*//") + if comment_index ~= nil then + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + local alternate_index = line:find("#A%[") + if alternate_index ~= nil then + use_alternate = true + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + alternate_index = line:find("#A]") + if alternate_index ~= nil then + use_alternate = false + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + local static_index = line:find("#S%[") + if static_index ~= nil then + use_static = true + line = line:sub(1, static_index - 1) + new_lines = 0 + end + static_index = line:find("#S]") + if static_index ~= nil then + use_static = false + line = line:sub(1, static_index - 1) + new_lines = 0 + end + + local newcount_index = line:find("#L:") + if newcount_index ~= nil then + local iS, iE = line:find("%d+", newcount_index + 3) + local newLines = tonumber(line:sub(iS, iE)) + if use_alternate then + alternate_display_lines = newLines + elseif recordRefrain then + refrain_display_lines = newLines + else + adjusted_display_lines = newLines + refrain_display_lines = newLines + alternate_display_lines = newLines + end + line = line:sub(1, newcount_index - 1) + new_lines = 0 -- ignore line + end + local static_index = line:find("#S:") + if static_index ~= nil then + local static_indexEnd = line:find("%s+", static_index + 1) + line = line:sub(static_indexEnd + 1) + static_text = line + new_lines = 0 + end + local alt_index = line:find("#A:") + if alt_index ~= nil then + local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) + new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) + _, alt_indexEnd = line:find("%s+", alt_indexEnd + 1) + line = line:sub(alt_indexEnd + 1) + singleAlternate = true + end + if line:find("###") ~= nil then -- Look for single line + line = line:gsub("%s*###%s*", "") + single_line = true + end + local newcount_index = line:find("#D:") + if newcount_index ~= nil then + local newcount_indexStart, newcount_indexEnd = line:find("%d+", newcount_index + 3) + new_lines = tonumber(line:sub(newcount_indexStart, newcount_indexEnd)) + _, newcount_indexEnd = line:find("%s+", newcount_indexEnd + 1) + line = line:sub(newcount_indexEnd + 1) + end + local refrain_index = line:find("#R%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = false + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#R]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("##R") + if refrain_index ~= nil then + playRefrain = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + else + playRefrain = false + end + newcount_index = line:find("#P:") + if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) + line = line:sub(1, newcount_index - 1) + end + newcount_index = line:find("#B:") + if newcount_index ~= nil then + line = line:sub(1, newcount_index - 1) + end + local phantom_index = line:find("##P") + if phantom_index ~= nil then + line = line:sub(1, phantom_index - 1) + end + phantom_index = line:find("##B") + if phantom_index ~= nil then + line = line:gsub("%s*##B%s*", "") .. "\n" + -- line = line:sub(1, phantom_index - 1) + end + if line ~= nil then + if use_static then + if static_text == "" then + static_text = line + else + static_text = static_text .. "\n" .. line + end + else + if use_alternate or singleAlternate then + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = alternate_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + arefrain[#refrain + 1] = line + else + arefrain[#refrain] = arefrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_aline == 1) then + alternate[#alternate + 1] = line + else + alternate[#alternate] = alternate[#alternate] .. "\n" .. line + end + end + cur_aline = cur_aline + 1 + if single_line or singleAlternate or cur_aline > displaySize then + if ensure_lines then + for i = cur_aline, displaySize, 1 do + cur_aline = i + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + if recordRefrain then + arefrain[#refrain] = arefrain[#refrain] .. "\n" + end + end + end + cur_aline = 1 + end + new_lines = new_lines - 1 + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(arefrain) do + alternate[#alternate + 1] = refrain_line + end + end + singleAlternate = false + else + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = adjusted_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + refrain[#refrain + 1] = line + else + refrain[#refrain] = refrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_line == 1) then + lyrics[#lyrics + 1] = line + else + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line + end + end + cur_line = cur_line + 1 + if single_line or cur_line > displaySize then + if ensure_lines then + for i = cur_line, displaySize, 1 do + cur_line = i + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end + end + cur_line = 1 + end + new_lines = new_lines - 1 + end + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(refrain) do + lyrics[#lyrics + 1] = refrain_line + end + end + end + end + end + end + if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then + for i = cur_line, displaySize, 1 do + cur_line = i + if use_alternate then + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + else + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end + end + lyrics[#lyrics + 1] = "" + -- pause_timer = false + return true end -- finds the index of a song in the directory ---if item is not in list, then return nil +-- if item is not in list, then return nil function get_index_in_list(list, q_item) - for index, item in ipairs(list) do - if item == q_item then return index end - end - return nil + for index, item in ipairs(list) do + if item == q_item then + return index + end + end + return nil end -------- @@ -1189,199 +1231,256 @@ end -- loads the song directory function load_song_directory() - --pause_timer = true - song_directory = {} - local filenames = {} - local dir = obs.os_opendir(get_songs_folder_path())--get_songs_folder_path()) - local entry - local songExt - local songTitle - repeat - entry = obs.os_readdir(dir) - if entry and not entry.directory and (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") then - songExt = obs.os_get_path_extension(entry.d_name) - songTitle=string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) - if songExt == ".enc" then - song_directory[#song_directory + 1] = dec(songTitle) - else - song_directory[#song_directory + 1] = songTitle - end - end - until not entry - obs.os_closedir(dir) - --pause_timer = false + -- pause_timer = true + song_directory = {} + local filenames = {} + local dir = obs.os_opendir(get_songs_folder_path()) + -- get_songs_folder_path()) + local entry + local songExt + local songTitle + repeat + entry = obs.os_readdir(dir) + if + entry and not entry.directory and + (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") + then + songExt = obs.os_get_path_extension(entry.d_name) + songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) + if songExt == ".enc" then + song_directory[#song_directory + 1] = dec(songTitle) + else + song_directory[#song_directory + 1] = songTitle + end + end + until not entry + obs.os_closedir(dir) + -- pause_timer = false end -- delete previewed song function delete_song(name) - if testValid(name) then - path = get_song_file_path(name,".txt") - else - path = get_song_file_path(enc(name),".enc") - end - os.remove(path) - table.remove(song_directory, get_index_in_list(song_directory, name)) - load_song_directory() + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + os.remove(path) + table.remove(song_directory, get_index_in_list(song_directory, name)) + load_song_directory() end -local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-' -- encoding alphabet +local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet -- encoding function enc(data) - return ((data:gsub('.', function(x) - local r,b='',x:byte() - for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end - return r; - end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) - if (#x < 6) then return '' end - local c=0 - for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end - return b:sub(c+1,c+1) - end)..({ '', '==', '=' })[#data%3+1]) + return ((data:gsub( + ".", + function(x) + local r, b = "", x:byte() + for i = 8, 1, -1 do + r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ) .. "0000"):gsub( + "%d%d%d?%d?%d?%d?", + function(x) + if (#x < 6) then + return "" + end + local c = 0 + for i = 1, 6 do + c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0) + end + return b:sub(c + 1, c + 1) + end + ) .. ({"", "==", "="})[#data % 3 + 1]) end function dec(data) - data = string.gsub(data, '[^'..b..'=]', '') - return (data:gsub('.', function(x) - if (x == '=') then return '' end - local r,f='',(b:find(x)-1) - for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end - return r; - end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) - if (#x ~= 8) then return '' end - local c=0 - for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end + data = string.gsub(data, "[^" .. b .. "=]", "") + return (data:gsub( + ".", + function(x) + if (x == "=") then + return "" + end + local r, f = "", (b:find(x) - 1) + for i = 6, 1, -1 do + r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ):gsub( + "%d%d%d?%d?%d?%d?%d?%d?", + function(x) + if (#x ~= 8) then + return "" + end + local c = 0 + for i = 1, 8 do + c = c + (x:sub(i, i) == "1" and 2 ^ (8 - i) or 0) + end return string.char(c) - end)) + end + )) end function testValid(filename) - if string.find(filename,'[\128-\255]') ~= nil then - return false - end - if string.find(filename,'[\\\\/:*?\"<>|]') ~= nil then - return false - end - return true + if string.find(filename, "[\128-\255]") ~= nil then + return false + end + if string.find(filename, '[\\\\/:*?"<>|]') ~= nil then + return false + end + return true end -- saves previewed song, return true if new song function save_song(name, text) - local path = {} - if testValid(name) then - path = get_song_file_path(name,".txt") - else - path = get_song_file_path(enc(name),".enc") - end - local file = io.open(path, "w") - if file ~= nil then - for line in text:gmatch("([^\n]+)") do - local trimmed = line:match("%s*(%S-.*%S+)%s*") - if trimmed ~= nil then - file:write(trimmed, "\n") - end - end - file:close() - if get_index_in_list(song_directory, name) == nil then - song_directory[#song_directory + 1] = name - return true - end - end - return false + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "w") + if file ~= nil then + for line in text:gmatch("([^\n]+)") do + local trimmed = line:match("%s*(%S-.*%S+)%s*") + if trimmed ~= nil then + file:write(trimmed, "\n") + end + end + file:close() + if get_index_in_list(song_directory, name) == nil then + song_directory[#song_directory + 1] = name + return true + end + end + return false end -- saves preprepared songs function save_prepared() - dbg_method("save_prepared") - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") + dbg_method("save_prepared") + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") for i, name in ipairs(prepared_songs) do - -- if not scene_load_complete or i > 1 then --don't save scene prepared songs - file:write(name, "\n") - -- end - end - file:close() - return true + -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs + file:write(name, "\n") + -- end + end + file:close() + return true end function load_prepared() - dbg_method("load_prepared") - -- pause_timer = true - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") - if file ~= nil then - for line in file:lines() do - prepared_songs[#prepared_songs + 1] = line - end - prepared_index = 1 - file:close() - end - -- pause_timer = false - return true + dbg_method("load_prepared") + -- pause_timer = true + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") + if file ~= nil then + for line in file:lines() do + prepared_songs[#prepared_songs + 1] = line + end + prepared_index = 1 + file:close() + end + -- pause_timer = false + return true end function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) - dbg_method("update_monitor") + dbg_method("update_monitor") local tableback = "#000000" - local text = "" - text = text .. "" - text = text .. "" - text = text .. "" + local text = "" + text = text .. "" + text = text .. "" + text = text .. "" text = text .. "" text = text .. "" text = text .. "" text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "
" - if not using_source then - text = text .. "
Prepared Song: " .. prepared_index - text = text .. " of " .. #prepared_songs .. "
" - end - text = text .. "
Lyric Page: " .. page_index - text = text .. " of " .. #lyrics .."
" - text = text .. "
" - -- show if song is from source or prepared songs - text = text .. "From: " - if using_source then - text = text .. "Source" - else - text = text .. "Prepared" - end - - text = text .. "
Song
Title
" - if song ~= "" then - text = text .. "" - text = text .. "" - end - if lyric ~= "" then - text = text .. "" - text = text .. "" - end - if nextlyric ~= "" then - text = text .. "" - text = text .. "" - end - if alt ~= "" then - text = text .. "" - text = text .. "" - end - if nextalt ~= "" then - text = text .. "" - text = text .. "" - end - if nextsong ~= "" then - text = text .. "" - text = text .. "" - end - text = text .. "
Song
Title
" .. song .. "
Current
Page
• " .. lyric .. "
Next
Page
• " .. nextlyric .. "
Alt
Lyric
• " .. alt .. "
Next
Alt
• " .. nextalt .. "
Next
Song:
" .. nextsong .. "
" - local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") - file:write(text) - file:close() - return true + text = text .. "" + text = text .. "" + text = + text .. + "
" + if not using_source then + text = + text .. + "
Prepared Song: " .. + prepared_index + text = + text .. + " of " .. #prepared_songs .. "
" + end + text = + text .. + "
Lyric Page: " .. + page_index + text = text .. " of " .. #lyrics .. "
" + text = text .. "
" + -- show if song is from source or prepared songs + text = text .. "From: " + if using_source then + text = text .. "Source" + else + text = text .. "Prepared" + end + + text = + text .. + "
" + if song ~= "" then + text = + text .. + "" + text = text .. "" + end + if lyric ~= "" then + text = + text .. + "" + text = text .. "" + end + if nextlyric ~= "" then + text = + text .. + "" + text = text .. "" + end + if alt ~= "" then + text = + text .. + "" + text = text .. "" + end + if nextalt ~= "" then + text = + text .. + "" + text = text .. "" + end + if nextsong ~= "" then + text = + text .. + "" + text = text .. "" + end + text = text .. "
Song
Title
" .. song .. "
Current
Page
• " .. lyric .. "
Next
Page
• " .. nextlyric .. "
Alt
Lyric
• " .. alt .. "
Next
Alt
• " .. nextalt .. "
Next
Song:
" .. nextsong .. "
" + local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") + file:write(text) + file:close() + return true end -- returns path of the given song name function get_song_file_path(name, suffix) - if name == nil then return nil end + if name == nil then + return nil + end return get_songs_folder_path() .. "\\" .. name .. suffix end @@ -1399,292 +1498,359 @@ end -- gets the text of a song function get_song_text(name) - local song_lines = {} - local path = {} - if testValid(name) then - path = get_song_file_path(name,".txt") - else - path = get_song_file_path(enc(name),".enc") - end - local file = io.open(path, "r") - if file ~= nil then - for line in file:lines() do - song_lines[#song_lines + 1] = line - end - file:close() - end - - return song_lines + local song_lines = {} + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + song_lines[#song_lines + 1] = line + end + file:close() + end + + return song_lines end --------- +-- ------ ---------------- ------------------------ OBS DEFAULT FUNCTIONS ----------------- +-- -------------- -------- -- A function named script_properties defines the properties that the user -- can change for the entire script module itself function script_properties() - dbg_method("script_properties") - script_props = obs.obs_properties_create() - obs.obs_properties_add_text(script_props, "prop_edit_song_title", "Song Title", obs.OBS_TEXT_DEFAULT) - local lyric_prop = obs.obs_properties_add_text(script_props, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) - obs.obs_property_set_long_description(lyric_prop,"Lyric Text with Markup") - obs.obs_properties_add_button(script_props, "prop_save_button", "Save Song", save_song_clicked) - local prop_dir_list = obs.obs_properties_add_list(script_props, "prop_directory_list", "Song Directory", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - - obs.obs_properties_add_button(script_props, "prop_prepare_button", "Prepare Song", prepare_song_clicked) - obs.obs_properties_add_button(script_props, "prop_delete_button", "Delete Song", delete_song_clicked) - obs.obs_properties_add_button(script_props, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) - obs.obs_properties_add_button(script_props, "prop_open_button", "Open Songs Folder", open_button_clicked) - local lines_prop = obs.obs_properties_add_int(script_props, "prop_lines_counter", "Lines to Display", 1, 100, 1) - obs.obs_property_set_long_description(lines_prop,"Sets default lines per page of lyric, overwritten by Markup: #L:n") - - local prop_lines = obs.obs_properties_add_bool(script_props, "prop_lines_bool", "Strictly ensure number of lines") - obs.obs_property_set_long_description(prop_lines,"Guarantees fixed number of lines per page") - - local link_prop = obs.obs_properties_add_bool(script_props, "link_text", "Only show title and static text with lyrics") - obs.obs_property_set_long_description(link_prop,"Hides Title and Static Text at end of Lyrics") - - local transition_prop = obs.obs_properties_add_bool(script_props, "transition_enabled", "Transition Preview to Program on Lyric Change") - obs.obs_property_set_modified_callback(transition_prop, changeTransitionProperty) - obs.obs_property_set_long_description(transition_prop,"Use with Studio Mode,Duplicate Sources, and OBS source transitions") - - local fade_prop = obs.obs_properties_add_bool(script_props, "text_fade_enabled", "Fade Text Out/In for Next Lyric") -- Fade Enable (WZ) - obs.obs_property_set_modified_callback(fade_prop, changeFadeProperty) - obs.obs_properties_add_int_slider(script_props, "text_fade_speed", "Fade Speed", 1, 10, 1) - - local source_prop = obs.obs_properties_add_list(script_props, "prop_source_list", "Text Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - obs.obs_property_set_long_description(source_prop,"Shows main lyric text") - local title_source_prop = obs.obs_properties_add_list(script_props, "prop_title_list", "Title Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - obs.obs_property_set_long_description(title_source_prop,"Shows text from Song Title") - local alternate_source_prop = obs.obs_properties_add_list(script_props, "prop_alternate_list", "Alternate Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - obs.obs_property_set_long_description(alternate_source_prop,"Shows text annotated with #A[ and #A]") - local static_source_prop = obs.obs_properties_add_list(script_props, "prop_static_list", "Static Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - obs.obs_property_set_long_description(static_source_prop,"Shows text annotated with #S[ and #S]") - local sources = obs.obs_enum_sources() - if sources ~= nil then - local n = {} - for _, source in ipairs(sources) do - source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n+1] = obs.obs_source_get_name(source) - end - end - table.sort(n) - obs.obs_property_list_add_string(source_prop, "", "") - obs.obs_property_list_add_string(title_source_prop, "", "") - obs.obs_property_list_add_string(alternate_source_prop, "", "") - obs.obs_property_list_add_string(static_source_prop, "", "") - for _, name in ipairs(n) do - obs.obs_property_list_add_string(source_prop, name, name) - obs.obs_property_list_add_string(title_source_prop, name, name) - obs.obs_property_list_add_string(alternate_source_prop, name, name) - obs.obs_property_list_add_string(static_source_prop, name, name) - end - end - obs.source_list_release(sources) - obs.obs_properties_add_button(script_props, "prop_refresh", "Refresh Sources", refresh_button_clicked) - local prep_prop = obs.obs_properties_add_list(script_props, "prop_prepared_list", "Prepared Songs", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prep_prop, name, name) - end - obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) - obs.obs_properties_add_button(script_props, "prop_clear_button", "Clear Prepared Songs", clear_prepared_clicked) - obs.obs_properties_add_button(script_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) - obs.obs_properties_add_button(script_props, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(script_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) - obs.obs_properties_add_button(script_props, "prop_home_button", "Reset to Song Start", home_button_clicked) - obs.obs_properties_add_button(script_props, "prop_reset_button", "Reset to First Song", reset_button_clicked) - if #prepared_songs > 0 and prepared_index > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[prepared_index]) - end - - obs.obs_properties_apply_settings(script_props, script_sets) - - return script_props + dbg_method("script_properties") + script_props = obs.obs_properties_create() + obs.obs_properties_add_text(script_props, "prop_edit_song_title", "Song Title", obs.OBS_TEXT_DEFAULT) + local lyric_prop = + obs.obs_properties_add_text(script_props, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_property_set_long_description(lyric_prop, "Lyric Text with Markup") + obs.obs_properties_add_button(script_props, "prop_save_button", "Save Song", save_song_clicked) + local prop_dir_list = + obs.obs_properties_add_list( + script_props, + "prop_directory_list", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) + + obs.obs_properties_add_button(script_props, "prop_prepare_button", "Prepare Song", prepare_song_clicked) + obs.obs_properties_add_button(script_props, "prop_delete_button", "Delete Song", delete_song_clicked) + obs.obs_properties_add_button( + script_props, + "prop_opensong_button", + "Edit Song with System Editor", + open_song_clicked + ) + obs.obs_properties_add_button(script_props, "prop_open_button", "Open Songs Folder", open_button_clicked) + local lines_prop = obs.obs_properties_add_int(script_props, "prop_lines_counter", "Lines to Display", 1, 100, 1) + obs.obs_property_set_long_description( + lines_prop, + "Sets default lines per page of lyric, overwritten by Markup: #L:n" + ) + + local prop_lines = obs.obs_properties_add_bool(script_props, "prop_lines_bool", "Strictly ensure number of lines") + obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") + + local link_prop = + obs.obs_properties_add_bool(script_props, "link_text", "Only show title and static text with lyrics") + obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") + + local transition_prop = + obs.obs_properties_add_bool(script_props, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_property_set_modified_callback(transition_prop, change_transition_property) + obs.obs_property_set_long_description( + transition_prop, + "Use with Studio Mode, duplicate sources, and OBS source transitions" + ) + + local fade_prop = obs.obs_properties_add_bool(script_props, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) + obs.obs_property_set_modified_callback(fade_prop, change_fade_property) + obs.obs_properties_add_int_slider(script_props, "text_fade_speed", "Fade Speed", 1, 10, 1) + + local source_prop = + obs.obs_properties_add_list( + script_props, + "prop_source_list", + "Text Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(source_prop, "Shows main lyric text") + local title_source_prop = + obs.obs_properties_add_list( + script_props, + "prop_title_list", + "Title Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(title_source_prop, "Shows text from song title") + local alternate_source_prop = + obs.obs_properties_add_list( + script_props, + "prop_alternate_list", + "Alternate Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(alternate_source_prop, "Shows text annotated with #A[ and #A]") + local static_source_prop = + obs.obs_properties_add_list( + script_props, + "prop_static_list", + "Static Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(static_source_prop, "Shows text annotated with #S[ and #S]") + local sources = obs.obs_enum_sources() + if sources ~= nil then + local n = {} + for _, source in ipairs(sources) do + source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + n[#n + 1] = obs.obs_source_get_name(source) + end + end + table.sort(n) + obs.obs_property_list_add_string(source_prop, "", "") + obs.obs_property_list_add_string(title_source_prop, "", "") + obs.obs_property_list_add_string(alternate_source_prop, "", "") + obs.obs_property_list_add_string(static_source_prop, "", "") + for _, name in ipairs(n) do + obs.obs_property_list_add_string(source_prop, name, name) + obs.obs_property_list_add_string(title_source_prop, name, name) + obs.obs_property_list_add_string(alternate_source_prop, name, name) + obs.obs_property_list_add_string(static_source_prop, name, name) + end + end + obs.source_list_release(sources) + obs.obs_properties_add_button(script_props, "prop_refresh", "Refresh Sources", refresh_button_clicked) + local prep_prop = + obs.obs_properties_add_list( + script_props, + "prop_prepared_list", + "Prepared Songs", + obs.OBS_COMBO_TYPE_EDITABLE, + obs.OBS_COMBO_FORMAT_STRING + ) + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prep_prop, name, name) + end + obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) + obs.obs_properties_add_button(script_props, "prop_clear_button", "Clear Prepared Songs", clear_prepared_clicked) + obs.obs_properties_add_button(script_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(script_props, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(script_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(script_props, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button( + script_props, + "prop_reset_button", + "Reset to First Prepared Song", + reset_button_clicked + ) + if #prepared_songs > 0 and prepared_index > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[prepared_index]) + end + + obs.obs_properties_apply_settings(script_props, script_sets) + + return script_props end -- A function named script_description returns the description shown to -- the user function script_description() - return "Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2) Author: Amirchev & DC Strato; with significant contributions from taxilian.
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
" + return "Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2) Author: Amirchev & DC Strato; with significant contributions from taxilian.
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
" end -function changeFadeProperty(props, prop, settings) - local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") - local transitionProp = obs.obs_properties_get(props, "transition_enabled") - obs.obs_property_set_enabled(transitionProp,not text_fade_set) - return true; +function change_fade_property(props, prop, settings) + local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") + local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") + obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) + return true end -function changeTransitionProperty(props, prop, settings) - local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") - local fadeProp = obs.obs_properties_get(props, "text_fade_enabled") - local fadeSpeedProp = obs.obs_properties_get(props, "text_fade_speed") - obs.obs_property_set_enabled(fadeProp,not transition_set) - obs.obs_property_set_enabled(fadeSpeedProp,not transition_set) - return true; -end +function change_transition_property(props, prop, settings) + local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") + local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") + local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") + obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) + obs.obs_property_set_enabled(fade_speed_prop, not transition_set) + transition_enabled = transition_set + return true +end -- A function named script_update will be called when settings are changed function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") -- Fade Enable (WZ) - text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") -- Fade Speed (WZ) - reload = false - local cur_display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") - if display_lines ~= cur_display_lines then - display_lines = cur_display_lines - reload = true - end - local cur_source_name = obs.obs_data_get_string(settings, "prop_source_list") - if source_name ~= cur_source_name then - source_name = cur_source_name - reload = true - end - local alt_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") - if alternate_source_name ~= alt_source_name then - alternate_source_name = alt_source_name - reload = true - end - local stat_source_name = obs.obs_data_get_string(settings, "prop_static_list") - if static_source_name ~= stat_source_name then - static_source_name = stat_source_name - reload = true - end - local cur_title_source = obs.obs_data_get_string(settings, "prop_title_list") - if title_source_name ~= cur_title_source then - title_source_name = cur_title_source - reload = true - end - local cur_ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") - if cur_ensure_lines ~= ensure_lines then - ensure_lines = cur_ensure_lines - reload = true - end - local cur_link_text = obs.obs_data_get_bool(settings, "link_text") - if cur_link_text ~= link_text then - link_text = cur_link_text - reload = true - end - - if reload then - if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then - prepare_song_by_name(prepared_songs[prepared_index]) - page_index = 1 - transition_lyric_text(false) - end - end + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") -- Fade Enable (WZ) + text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") -- Fade Speed (WZ) + reload = false + local cur_display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + if display_lines ~= cur_display_lines then + display_lines = cur_display_lines + reload = true + end + local cur_source_name = obs.obs_data_get_string(settings, "prop_source_list") + if source_name ~= cur_source_name then + source_name = cur_source_name + reload = true + end + local alt_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") + if alternate_source_name ~= alt_source_name then + alternate_source_name = alt_source_name + reload = true + end + local stat_source_name = obs.obs_data_get_string(settings, "prop_static_list") + if static_source_name ~= stat_source_name then + static_source_name = stat_source_name + reload = true + end + local cur_title_source = obs.obs_data_get_string(settings, "prop_title_list") + if title_source_name ~= cur_title_source then + title_source_name = cur_title_source + reload = true + end + local cur_ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") + if cur_ensure_lines ~= ensure_lines then + ensure_lines = cur_ensure_lines + reload = true + end + local cur_link_text = obs.obs_data_get_bool(settings, "link_text") + if cur_link_text ~= link_text then + link_text = cur_link_text + reload = true + end + + if reload then + if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then + prepare_selected(prepared_songs[prepared_index]) + -- page_index = 1 + -- transition_lyric_text(false) + end + end end -- A function named script_defaults will be called to set the default settings function script_defaults(settings) - dbg_method("script_defaults") - obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - --obs.obs_data_set_default_string(settings, "prop_source_list", prepared_songs[1] ) - --if #prepared_songs ~= 0 then - -- prepared_songs[prepared_index] = prepared_songs[1] - -- prepared_index = 1 - --else - -- prepared_songs[prepared_index] = "" - --end - if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions - if windows_os then - os.execute("mkdir \"" .. get_songs_folder_path() .. "\"") - else - os.execute("mkdir -p \"" .. get_songs_folder_path() .. "\"") - end + dbg_method("script_defaults") + obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) + -- obs.obs_data_set_default_string(settings, "prop_source_list", prepared_songs[1] ) + -- if #prepared_songs ~= 0 then + -- prepared_songs[prepared_index] = prepared_songs[1] + -- prepared_index = 1 + -- else + -- prepared_songs[prepared_index] = "" + -- end + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + if windows_os then + os.execute('mkdir "' .. get_songs_folder_path() .. '"') + else + os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') + end end -- A function named script_save will be called when the script is saved function script_save(settings) save_prepared() - local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) - obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) - --hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) - obs.obs_data_set_array(settings, "lyric_prev_hotkey", hotkey_save_array) - --hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) - obs.obs_data_set_array(settings, "lyric_clear_hotkey", hotkey_save_array) - --hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) - obs.obs_data_set_array(settings, "next_prepared_hotkey", hotkey_save_array) - --hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) - obs.obs_data_set_array(settings, "previous_prepared_hotkey", hotkey_save_array) - --hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) - obs.obs_data_set_array(settings, "home_song_hotkey", hotkey_save_array) - --hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) - obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) - --hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) - obs.obs_data_array_release(hotkey_save_array) + local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) + obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) + -- hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) + obs.obs_data_set_array(settings, "lyric_prev_hotkey", hotkey_save_array) + -- hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) + obs.obs_data_set_array(settings, "lyric_clear_hotkey", hotkey_save_array) + -- hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) + obs.obs_data_set_array(settings, "next_prepared_hotkey", hotkey_save_array) + -- hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) + obs.obs_data_set_array(settings, "previous_prepared_hotkey", hotkey_save_array) + -- hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) + obs.obs_data_set_array(settings, "home_song_hotkey", hotkey_save_array) + -- hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) + obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) + -- hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) + obs.obs_data_array_release(hotkey_save_array) end -- a function named script_load will be called on startup function script_load(settings) - dbg_method("script_load") - hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) - local hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") - obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") - obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") - obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") - obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) - hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") - obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Song", home_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") - obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - script_sets = settings - source_name = obs.obs_data_get_string(settings, "prop_source_list") - if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions - load_song_directory() - load_prepared() - --obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for Source * Marker (WZ) - --obs.timer_add(timer_callback, 50) -- Setup callback for text fade effect + dbg_method("script_load") + hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) + local hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") + obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") + obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") + obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") + obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) + hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") + obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_reset_id = + obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") + obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + script_sets = settings + source_name = obs.obs_data_get_string(settings, "prop_source_list") + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + load_song_directory() + load_prepared() + -- obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for Source * Marker (WZ) + -- obs.timer_add(timer_callback, 50) -- Setup callback for text fade effect end -------- @@ -1694,218 +1860,223 @@ end -------- -- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) -function rename_source() - --pause_timer = true - local sources = obs.obs_enum_sources() - if (sources ~= nil) then - -- count and index sources - local t = 1 - for _, source in ipairs(sources) do - local source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "Prepare_Lyrics" then - local settings = obs.obs_source_get_settings(source) - obs.obs_data_set_string(settings, "index", t) -- add index to source data - t = t + 1 - obs.obs_data_release(settings) -- release memory - end - end - -- Find and mark Duplicates in loadLyric_items table - local loadLyric_items = {} -- Start Table for all load Sources - local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items - if scenes ~= nil then - for _, scenesource in ipairs(scenes) do -- Loop through all scenes - local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer - local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id - if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) - if loadLyric_items[index] == nil then - loadLyric_items[index] = "x" -- First time to find this source so mark with x - else - loadLyric_items[index] = "*" -- Found this source again so mark with * - end - obs.obs_data_release(settings) -- release memory - end - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - end - obs.source_list_release(scenes) -- Free source list - end - - -- Name Source with Song Title - local i = 1 - for _, source in ipairs(sources) do - local source_id = obs.obs_source_get_unversioned_id(source) -- Get source - if source_id == "Prepare_Lyrics" then -- Skip if not a Load Lyric source - local c_name = obs.obs_source_get_name(source) -- Get current Source Name - local settings = obs.obs_source_get_settings(source) -- Get settings for this source - local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load - local index = obs.obs_data_get_string(settings, "index") -- get index - if (song ~= nil) then - local name = t-i .. ". Load lyrics for: " .. song .. "" -- use index for compare - -- Mark Duplicates - if index ~= nil then - if loadLyric_items[index] == "*" then - name = "" .. name .. " * " - end - if (c_name ~= name) then - obs.obs_source_set_name(source, name) - end - end - i = i + 1 - end - obs.obs_data_release(settings) - end - end - end - obs.source_list_release(sources) - --pause_timer = false +function rename_source() + -- pause_timer = true + local sources = obs.obs_enum_sources() + if (sources ~= nil) then + -- count and index sources + local t = 1 + for _, source in ipairs(sources) do + local source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "Prepare_Lyrics" then + local settings = obs.obs_source_get_settings(source) + obs.obs_data_set_string(settings, "index", t) -- add index to source data + t = t + 1 + obs.obs_data_release(settings) -- release memory + end + end + -- Find and mark Duplicates in loadLyric_items table + local loadLyric_items = {} -- Start Table for all load Sources + local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items + if scenes ~= nil then + for _, scenesource in ipairs(scenes) do -- Loop through all scenes + local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer + local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id + if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) + if loadLyric_items[index] == nil then + loadLyric_items[index] = "x" -- First time to find this source so mark with x + else + loadLyric_items[index] = "*" -- Found this source again so mark with * + end + obs.obs_data_release(settings) -- release memory + end + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + end + obs.source_list_release(scenes) -- Free source list + end + + -- Name Source with Song Title + local i = 1 + for _, source in ipairs(sources) do + local source_id = obs.obs_source_get_unversioned_id(source) -- Get source + if source_id == "Prepare_Lyrics" then -- Skip if not a Load Lyric source + local c_name = obs.obs_source_get_name(source) -- Get current Source Name + local settings = obs.obs_source_get_settings(source) -- Get settings for this source + local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load + local index = obs.obs_data_get_string(settings, "index") -- get index + if (song ~= nil) then + local name = t - i .. ". Load lyrics for: " .. song .. "" -- use index for compare + -- Mark Duplicates + if index ~= nil then + if loadLyric_items[index] == "*" then + name = '' .. name .. " * " + end + if (c_name ~= name) then + obs.obs_source_set_name(source, name) + end + end + i = i + 1 + end + obs.obs_data_release(settings) + end + end + end + obs.source_list_release(sources) + -- pause_timer = false end source_def.get_name = function() - return "Prepare Lyric" -end - -source_def.update = function (data, settings) - rename_source() -- Rename and Mark sources instantly on update (WZ) -end - -source_def.get_properties = function (data) - load_song_directory() - local props = obs.obs_properties_create() - local source_dir_list = obs.obs_properties_add_list(props, "songs", "Song Directory", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(source_dir_list, name, name) - end - obs.obs_properties_add_bool(props, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode - obs.obs_properties_add_bool(props, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode - return props + return "Prepare Lyric" +end + +source_def.update = function(data, settings) + rename_source() -- Rename and Mark sources instantly on update (WZ) +end + +source_def.get_properties = function(data) + load_song_directory() + local props = obs.obs_properties_create() + local source_dir_list = + obs.obs_properties_add_list( + props, + "songs", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(source_dir_list, name, name) + end + obs.obs_properties_add_bool(props, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode + obs.obs_properties_add_bool(props, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode + return props end source_def.create = function(settings, source) data = {} - sh = obs.obs_source_get_signal_handler(source) - obs.signal_handler_connect(sh, "activate", source_active) --Set Active Callback - obs.signal_handler_connect(sh, "show", source_showing) --Set Preview Callback - obs.signal_handler_connect(sh, "hide", source_hidden) --Set Preview Callback - return data + sh = obs.obs_source_get_signal_handler(source) + obs.signal_handler_connect(sh, "activate", source_active) -- Set Active Callback + obs.signal_handler_connect(sh, "show", source_showing) -- Set Preview Callback + obs.signal_handler_connect(sh, "hide", source_hidden) -- Set Preview Callback + return data end -source_def.get_defaults = function(settings) - obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) - obs.obs_data_set_default_string(settings, "index", "0") +source_def.get_defaults = function(settings) + obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) + obs.obs_data_set_default_string(settings, "index", "0") end source_def.destroy = function(source) - end -- function on_event(event) - -- if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then - -- set_current_scene_name() - -- rename_source() - -- --update_source_text() - -- transition_lyric_text(false) - -- end - - +-- if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then +-- set_current_scene_name() +-- rename_source() +-- -- update_source_text() +-- transition_lyric_text(false) +-- end + -- end function load_song(source, preview) - dbg_method("load_song") - local settings = obs.obs_source_get_settings(source) - if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then - local song = obs.obs_data_get_string(settings, "songs") - --if song ~= prepared_songs[prepared_index] then - if song == nil - or song == "" - then return end - dbg_inner("load_song: " .. song) - --local prop_prep_list = obs.obs_properties_get(script_props, "prop_prepared_list") - --if scene_load_complete then - -- obs.obs_property_list_item_remove(prop_prep_list, 0) - -- table.remove(prepared_songs , 1) -- clear older scene loaded song - --end - --obs.obs_property_list_insert_string(prop_prep_list, 0, song, song) - --table.insert(prepared_songs, 1, song) - --scene_load_complete = true - --obs.obs_data_set_string(script_sets, "prop_prepared_list", song) - --obs.obs_properties_apply_settings(script_props, script_sets) - - --update scene info - --set_current_scene_name() - --load_scene = current_scene - - using_source = true - - --prepare song and update lyrics - -- if (song ~= prepared_songs[prepared_index]) then - prepare_selected(song) - -- end - - --save_prepared() - --page_index = 1 - --prepared_index = 1 - - --update_source_text() - --text_opacity = 99 - --text_fade_dir = 2 - --transition_lyric_text(true) - --end - -- TODO: ensure home on activate working correctly - if obs.obs_data_get_bool(settings, "source_home_on_active") then - home_prepared(true) - end - end - obs.obs_data_release(settings) + dbg_method("load_song") + local settings = obs.obs_source_get_settings(source) + if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then + local song = obs.obs_data_get_string(settings, "songs") + -- if song ~= prepared_songs[prepared_index] then + if song == nil or song == "" then + return + end + dbg_inner("load_song: " .. song) + -- local prop_prep_list = obs.obs_properties_get(script_props, "prop_prepared_list") + -- if scene_load_complete then + -- obs.obs_property_list_item_remove(prop_prep_list, 0) + -- table.remove(prepared_songs , 1) -- clear older scene loaded song + -- end + -- obs.obs_property_list_insert_string(prop_prep_list, 0, song, song) + -- table.insert(prepared_songs, 1, song) + -- scene_load_complete = true + -- obs.obs_data_set_string(script_sets, "prop_prepared_list", song) + -- obs.obs_properties_apply_settings(script_props, script_sets) + + -- update scene info + -- set_current_scene_name() + -- load_scene = current_scene + + using_source = true + + -- prepare song and update lyrics + -- if (song ~= prepared_songs[prepared_index]) then + prepare_selected(song) + -- end + + -- save_prepared() + -- page_index = 1 + -- prepared_index = 1 + + -- update_source_text() + -- text_opacity = 99 + -- text_fade_dir = 2 + -- transition_lyric_text(true) + -- end + -- TODO: ensure home on activate working correctly + if obs.obs_data_get_bool(settings, "source_home_on_active") then + home_prepared(true) + end + end + obs.obs_data_release(settings) end function source_active(cd) - local source = obs.calldata_source(cd, "source") - if source == nil then - return - end - load_song(source, false) + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + load_song(source, false) end function source_showing(cd) local source = obs.calldata_source(cd, "source") - if source == nil then - return - end - --if sourceActive() then return end - load_song(source, true) + if source == nil then + return + end + -- if sourceActive() then return end + load_song(source, true) end function dbg(message) - if DEBUG then - print(message) - end + if DEBUG then + print(message) + end end function dbg_inner(message) - if DEBUG_INNER then - dbg("INNER: " .. message) - end + if DEBUG_INNER then + dbg("INNER: " .. message) + end end function dbg_method(message) - if DEBUG_METHODS then - dbg("METHOD: " .. message) - end + if DEBUG_METHODS then + dbg("METHOD: " .. message) + end end function dbg_custom(message) - if DEBUG_CUSTOM then - dbg("CUSTOM: " .. message) - end + if DEBUG_CUSTOM then + dbg("CUSTOM: " .. message) + end end -obs.obs_register_source(source_def); +obs.obs_register_source(source_def) From 51f4c94abbb477e7717fa13e31ca013d9199631a Mon Sep 17 00:00:00 2001 From: amirchev Date: Wed, 15 Sep 2021 10:45:59 -0700 Subject: [PATCH 004/105] simplified code preview transition code --- lyrics.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 488068e..1f5451d 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -272,12 +272,10 @@ function next_lyric(pressed) end dbg_method("next_lyric") -- check if transition enabled - if transition_enabled then - if not transition_completed then - obs.obs_frontend_preview_program_trigger_transition() - transition_completed = true - return - end + if transition_enabled and not transition_completed then + obs.obs_frontend_preview_program_trigger_transition() + transition_completed = true + return end if #lyrics > 0 or #alternate > 0 then -- and sourceShowing() then -- Lyrics is driving paging From 559a9de9435d12aec1c47ce45ad312857fdd835e Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 23 Sep 2021 01:42:25 -0600 Subject: [PATCH 005/105] Update lyrics.lua This is working as far as I can tell. I modified the source loaded songs to inject themselves into a preprepared list of songs so both methods work. Next and Prev prepared songs, or just paging through songs will loop out of a source prepared song into the prepared list and back into the source prepared song if still on the same scene. Also added delete single prepared song button requested by a user. --- lyrics.lua | 872 +++++++++++++++++++++++++---------------------------- 1 file changed, 419 insertions(+), 453 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 1f5451d..998a534 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1,4 +1,4 @@ --- Copyright 2020 amirchev +--- Copyright 2020 amirchev -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -12,74 +12,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- TODO: refresh properties after next prepared selection --- TODO: add text formatting guide (Done 7/31/21) - --- Source updates by W. Zaggle (DCSTRATO) 12/3/2020 --- Fading Text Out/In with transition option 12/8/2020 - --- Source updates by W. Zaggle (DCSTRATO) 1/24/2021 --- Added ##B as alternative to ##P --- Added #B:n and #P:n as way to add multiple blank lines all at once --- Added #R:n preceding text as a way to Duplicate the following text line n times --- Corrected possible timer recursion where timer function could take longer than 100ms callback interval and hang OBS - --- Source updates by W. Zaggle (DCSTRATO) 2/4/2021 --- Changed #R:n to #D:n (Duplicate Lines) --- Added #R[ and #R] on lines by themselves to bracket lines of Refrain --- Added ##R to repeat the lines bracketed by #R[ and #R] lines --- Made chage to showing() function maybe work better if not in studio mode - --- Source updates by W. Zaggle (DCSTRATO) 2/13/21 --- Stability Issues --- #r[ loads refrain without showing lines. Used if you want to have the refrain at the top of the text but only use it with ##R - --- Source updates by W. Zaggle (DCSTRATO) 2/17/21 --- Removed auto HOME when using source to prepare Lyric and returning to scene without a lyric change --- Added option to Home lyric when return to scene without a lyric change --- Added code to instantly show/hide lyrics ignoring fade option (Should fade be optional?) --- New option to modify Title Text object with Song Title --- Added code to allow text to change in Preview mode if preview and active scene are the same (normally active text object prevents this change in preview) --- CLeared up Home and Reset. Home returns to start of current song. Reset goes back to 1st song. --- Added new button/hot-key to allow for both Home and Reset functions. --- Allow Comment after #L:n markup in Lyrics - --- Source update by W. Zaggle (DCSTRATO) 3/6/21 --- Added Alternate Text Source that syncs with Lyrics marked with #A[ and #A] --- Added Static Source that loads once with #S[ and #S] - --- Source update by W. Zaggle (DCSTRATO) 5/15/21 --- Added lyric index update on Alternate if number of lyrics is zero, Text Source is not in Scene or Undefined - --- Source update by W. Zaggle (DCSTRATO) 7/11/2021 --- Added encoding/decoding of song titles that are invalid file names. Files are encoded and saved as .enc files instead -- .txt files to maintain compatibility with prior versions. Invalid includes Unicoded titles and characters --- /:*?\"<>| which allows for a song title to include prior invalid characters and support other languages. --- For example a song title can now be "What Child is This?" or "Ơn lạ lùng" (Vietnamese for Amazing Grace) - --- Source update by W. Zaggle (DCSTRATO) 7/31/2021 --- Added ablility to elect to link Title and Static text to blank with Lyrics at end of song (Requested Feature) --- Added html quick guide table to Script Page (Text Formatting Guide TODO) - --- Source update by W. Zaggle (DCSTRATO) 8/6/2021 --- Added html Monitor Page for use in Browser Dock --- Added ##r with same funcation as ##R --- Added #A:n Line Where n is number of pages to apply line to in Alternate Text Block --- Added #S: Line that adds a single Static Line to the static block --- #L:n now sets Lyrics, Refrain and Alternate Text block default number of lines per page (If in Alternate block or Refrain block it will override those lines per page) - --- Source update by W. Zaggle (DCSTRATO) 8/16/2021 --- UPdated HTM monitor page and limited support for duplicate sources in Studio Mode - --- Source update by W. Zaggle (DCSTRATO) 8/28/2021 --- minor bug fix to show/hide lyrics --- added Refresh Sources button to update script with new sources as they are added. (Can't find Callback for new/removed sources to replace button) - --- Source update by W. Zaggle (DCSTRATO) 9/6/2021 --- more work on show/hide and fade to be more compatible --- corrected Refrain to honor page size --- restored blank page at end of song --- added refresh song directory to refresh sources button (songs are sources of a sort) --- added button to edit song text in default system editor for .txt or .enc file types. +-- added delete single prepared song (WZ) obs = obslua bit = require("bit") @@ -121,11 +54,13 @@ page_index = 1 prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected song_directory = {} prepared_songs = {} -link_text = false -source_song_title = "" -using_source = false +link_text = false -- true if Title and Static should fade with text only during hide/show +lyric_change = false -- Text and Static should only fade when lyrics are changing or during show/hide +source_song_title = "" -- The song title from a source loaded song +using_source = false -- true when a lyric load song is being used instead of a pre-prepared song +source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) transition_enabled = false - +load_scene = "" timer_exists = false -- hotkeys @@ -140,7 +75,7 @@ hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID -- script placeholders script_sets = nil script_props = nil - +prepare_props = nil -- text status & fade TEXT_VISIBLE = 0 -- text is visible TEXT_HIDDEN = 1 -- text is hidden @@ -152,119 +87,110 @@ text_status = TEXT_VISIBLE text_opacity = 100 text_fade_speed = 1 text_fade_enabled = false - --- scene_load_complete = false --- update_lyrics_in_fade = false --- load_scene = "" +load_source = nil transition_completed = false -- simple debugging/print mechanism -DEBUG = true -- on/off switch for entire debugging mechanism +DEBUG = false -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints DEBUG_CUSTOM = true -- print custom debugging messages +DEBUG_BOOL = true -- print message with bool state true/false -------- ---------------- ------------------------ CALLBACKS ---------------- -------- +function anythingShowing() + return sourceShowing() or alternateShowing() or titleShowing() or staticShowing() +end + +function sourceShowing() + local source = obs.obs_get_source_by_name(source_name) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function alternateShowing() + local source = obs.obs_get_source_by_name(alternate_source_name) + local showing = false + if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function titleShowing() + local source = obs.obs_get_source_by_name(title_source_name) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function staticShowing() + local source = obs.obs_get_source_by_name(static_source_name) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function anythingActive() + return sourceActive() or alternateActive() or titleActive() or staticActive() +end + +function sourceActive() + local source = obs.obs_get_source_by_name(source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function alternateActive() + local source = obs.obs_get_source_by_name(alternate_source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function titleActive() + local source = obs.obs_get_source_by_name(title_source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end --- function sourceShowing() --- local source = obs.obs_get_source_by_name(source_name) --- local showing = false --- if source ~= nil then --- showing = obs.obs_source_showing(source) --- end --- obs.obs_source_release(source) --- return showing --- end - --- function alternateShowing() --- local source = obs.obs_get_source_by_name(alternate_source_name) --- local showing = false --- if source ~= nil then --- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status --- showing = obs.obs_source_showing(source) --- end --- obs.obs_source_release(source) --- return showing --- end - --- function titleShowing() --- local source = obs.obs_get_source_by_name(title_source_name) --- local showing = false --- if source ~= nil then --- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status --- showing = obs.obs_source_showing(source) --- end --- obs.obs_source_release(source) --- return showing --- end - --- function staticShowing() --- local source = obs.obs_get_source_by_name(static_source_name) --- local showing = false --- if source ~= nil then --- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status --- showing = obs.obs_source_showing(source) --- end --- obs.obs_source_release(source) --- return showing --- end - --- function anythingActive() --- return sourceActive() or alternateActive() or titleActive() or staticActive() --- end - --- function sourceActive() - --- local source = obs.obs_get_source_by_name(source_name) --- local active = false --- if source ~= nil then --- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status --- active = obs.obs_source_active(source) --- obs.obs_source_release(source) --- end --- return active --- end - --- function alternateActive() - --- local source = obs.obs_get_source_by_name(alternate_source_name) --- local active = false --- if source ~= nil then --- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status --- active = obs.obs_source_active(source) --- obs.obs_source_release(source) --- end --- return active --- end - --- function titleActive() - --- local source = obs.obs_get_source_by_name(title_source_name) --- local active = false --- if source ~= nil then --- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status --- active = obs.obs_source_active(source) --- obs.obs_source_release(source) --- end --- return active --- end - --- function staticActive() - --- local source = obs.obs_get_source_by_name(static_source_name) --- local active = false --- if source ~= nil then --- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status --- active = obs.obs_source_active(source) --- obs.obs_source_release(source) --- end --- return active --- end +function staticActive() + local source = obs.obs_get_source_by_name(static_source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end function next_lyric(pressed) if not pressed then @@ -273,12 +199,12 @@ function next_lyric(pressed) dbg_method("next_lyric") -- check if transition enabled if transition_enabled and not transition_completed then - obs.obs_frontend_preview_program_trigger_transition() - transition_completed = true - return + obs.obs_frontend_preview_program_trigger_transition() + transition_completed = true + return end - - if #lyrics > 0 or #alternate > 0 then -- and sourceShowing() then -- Lyrics is driving paging + dbg_inner("next page") + if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing if page_index < #lyrics then page_index = page_index + 1 dbg_inner("page_index: " .. page_index) @@ -294,7 +220,7 @@ function prev_lyric(pressed) return end dbg_method("prev_lyric") - if #lyrics > 0 or #alternate > 0 then -- and sourceShowing() then -- Lyrics is driving paging + if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing if page_index > 1 then page_index = page_index - 1 dbg_inner("page_index: " .. page_index) @@ -309,9 +235,25 @@ function prev_prepared(pressed) if not pressed then return end + + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) + return + end if prepared_index > 1 then using_source = false prepare_selected(prepared_songs[prepared_index - 1]) + return + end + + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared + else + using_source = true + prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source + load_song(load_source, false) end end @@ -319,9 +261,23 @@ function next_prepared(pressed) if not pressed then return end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song + return + end if prepared_index < #prepared_songs then using_source = false - prepare_selected(prepared_songs[prepared_index + 1]) + prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available + else + using_source = true + prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source + load_song(load_source, false) end end @@ -330,14 +286,7 @@ function toggle_lyrics_visibility(pressed) if not pressed then return end - -- if #lyrics > 0 and not sourceShowing() then - -- return - -- end - -- if #alternate > 0 and not alternateShowing() then - -- return - -- end - -- visible = not visible - -- showHelp = not showHelp + lyric_change = true -- This makes sure title and static change with text if selected if text_status ~= TEXT_HIDDEN then dbg_inner("hiding") set_text_visiblity(TEXT_HIDDEN) @@ -371,17 +320,12 @@ function home_prepared(pressed) return false end dbg_method("home_prepared") - -- visible = true - -- set_text_visiblity(TEXT_VISIBLE) - -- page_index = 0 - -- prepared_index = 0 using_source = false page_index = 0 - -- prepared_index = 1 + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) - prepare_selected(prepared_songs[1]) else obs.obs_data_set_string(script_sets, "prop_prepared_list", "") end @@ -395,21 +339,22 @@ function home_song(pressed) return false end dbg_method("home_song") - -- visible = true - -- set_text_visiblity(TEXT_VISIBLE) - if #prepared_songs > 0 then - page_index = 1 - transition_lyric_text(false) - end - -- prepare_selected(prepared_songs[prepared_index]) -- redundant from above + page_index = 1 + transition_lyric_text(false) return true end --- function set_current_scene_name() --- local scene = obs.obs_frontend_get_current_preview_scene() --- current_scene = obs.obs_source_get_name(scene) --- obs.obs_source_release(scene); --- end +function get_current_scene_name() + dbg_method("get_current_scene_name") + local scene = obs.obs_frontend_get_current_scene() + local current_scene = obs.obs_source_get_name(scene) + obs.obs_source_release(scene) + if current_scene ~= nil then + return current_scene + else + return "-" + end +end function next_button_clicked(props, p) next_lyric(true) @@ -435,6 +380,15 @@ function reset_button_clicked(props, p) home_prepared(true) return true end +function prev_prepared_clicked(props, p) + prev_prepared(true) + return true +end + +function next_prepared_clicked(props, p) + next_prepared(true) + return true +end function save_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") @@ -506,7 +460,6 @@ function prepare_song_clicked(props, p) end obs.obs_properties_apply_settings(props, script_sets) save_prepared() - -- update_source_text() return true end @@ -554,6 +507,54 @@ function prepare_selection_made(props, prop, settings) return true end +-- delete selected prepared song from list (user request) +function remove_selection_made(props, p) + dbg_method("unprepare_selection_made") + local name = obs.obs_data_get_string(script_sets, "prop_prepared_list") + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + local count = obs.obs_property_list_item_count(prop_prep_list) + for i = 0, count do + local song = obs.obs_property_list_item_string(prop_prep_list, i) + if song == name then + table.remove(prepared_songs, i + 1) + save_prepared() + prepared_songs = {} + load_prepared() + obs.obs_property_list_clear(prop_prep_list) + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prop_prep_list, name, name) + end + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + prepared_index = 1 + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + prepared_index = 0 + end + end + end + obs.obs_properties_apply_settings(props, script_sets) + update_source_text() + return true +end + +-- removes prepared songs +function clear_prepared_clicked(props, p) + dbg_method("clear_prepared_clicked") + prepared_songs = {} + page_index = 0 + prepared_index = 0 + update_source_text() + -- clear the list + local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prep_prop) + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + obs.obs_properties_apply_settings(props, script_sets) + save_prepared() + transition_lyric_text(false) + return true +end + function prepare_selected(name) dbg_method("prepare_selected: " .. name) if name == nil then @@ -562,20 +563,15 @@ function prepare_selected(name) if name == "" then return false end - if name == prepared_songs[prepared_index] or (using_source and name == source_song_title) then - return false - end prepare_song_by_name(name) page_index = 1 if not using_source then prepared_index = get_index_in_list(prepared_songs, name) - transition_lyric_text(false) + dbg_custom("prepared_index: " .. prepared_index) else source_song_title = name - transition_lyric_text(true) end - -- visible = true - -- set_text_visiblity(TEXT_VISIBLE) + transition_lyric_text(false) return true end @@ -601,28 +597,6 @@ function preview_selection_made(props, prop, settings) return true end --- removes prepared songs -function clear_prepared_clicked(props, p) - dbg_method("clear_prepared_clicked") - -- scene_load_complete = false - prepared_songs = {} - -- lyrics = {} - -- alternate = {} - -- static = "" - set_text_visiblity(TEXT_HIDDEN) - -- clear the list - local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_clear(prep_prop) - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - obs.obs_properties_apply_settings(props, script_sets) - save_prepared() - --page_index = 0 - prepared_index = 0 - transition_lyric_text(false) - -- displayed_song = "" - return true -end - function open_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_directory_list") if testValid(name) then @@ -630,7 +604,6 @@ function open_song_clicked(props, p) else path = get_song_file_path(enc(name), ".enc") end - print(path) if windows_os then os.execute('explorer "' .. path .. '"') else @@ -654,148 +627,30 @@ end ---------------- -------- --- updates the displayed lyrics -function update_source_text() - dbg_method("update_source_text") - if prepared_index == nil or prepared_index == 0 then - return - end - local text = "" - local alttext = "" - local next_lyric = "" - local next_alternate = "" - local static = static_text - local title = "" - - if not using_source then - title = prepared_songs[prepared_index] - else - title = source_song_title - end - -- init_opacity = 0; - - local source = obs.obs_get_source_by_name(source_name) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - local stat_source = obs.obs_get_source_by_name(static_source_name) - local title_source = obs.obs_get_source_by_name(title_source_name) - - -- if visible then - -- text_fade_dir = 2 - -- init_opacity = 100 - -- get text - if #lyrics > 0 then -- and sourceShowing() then - if lyrics[page_index] ~= nil then - text = lyrics[page_index] - end - end - if #alternate > 0 then -- and alternateShowing() then - if alternate[page_index] ~= nil then - alttext = alternate[page_index] - end - end - - -- end - - if link_text then - if string.len(text) == 0 and string.len(alttext) == 0 then - static = "" - title = "" - end - end - - -- update source texts - if source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", text) - -- obs.obs_data_set_int(settings, "opacity", init_opacity) - -- obs.obs_data_set_int(settings, "outline_opacity", init_opacity) - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) - - next_lyric = lyrics[page_index + 1] - if (next_lyric == nil) then - next_lyric = "" - end - end - if alt_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", alttext) - -- obs.obs_data_set_int(alt_settings, "opacity", init_opacity) - -- obs.obs_data_set_int(alt_settings, "outline_opacity", init_opacity) - obs.obs_source_update(alt_source, settings) - obs.obs_data_release(settings) - - next_alternate = alternate[page_index + 1] - if (next_alternate == nil) then - next_alternate = "" - end - end - if stat_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", static) - obs.obs_source_update(stat_source, settings) - obs.obs_data_release(settings) - end - if title_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", title) - obs.obs_source_update(title_source, settings) - obs.obs_data_release(settings) - end - -- release source references - obs.obs_source_release(source) - obs.obs_source_release(alt_source) - obs.obs_source_release(stat_source) - obs.obs_source_release(title_source) - - local next_prepared = prepared_songs[prepared_index + 1] - if (next_prepared == nil) then - next_prepared = "" - end - update_monitor( - title, - text:gsub("\n", "
• "), - next_lyric:gsub("\n", "
• "), - alttext:gsub("\n", "
• "), - next_alternate:gsub("\n", "
• "), - next_prepared - ) - -- if obs.obs_data_get_bool(script_sets, "transition_enabled") then - -- if transition_completed then - -- obs.obs_frontend_preview_program_trigger_transition() - -- transition_completed = true - -- end - -- end -end - function apply_source_opacity() + dbg_method("apply_source_visiblity") local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local source = obs.obs_get_source_by_name(source_name) if source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero obs.obs_source_update(source, settings) end obs.obs_source_release(source) local alt_source = obs.obs_get_source_by_name(alternate_source_name) if alt_source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero obs.obs_source_update(alt_source, settings) end obs.obs_source_release(alt_source) - if text_status ~= TEXT_TRANSITION_IN and text_status ~= TEXT_TRANSITION_OUT then + dbg_bool("lyric_change", lyric_change) + if lyric_change then local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero obs.obs_source_update(title_source, settings) end obs.obs_source_release(title_source) local static_source = obs.obs_get_source_by_name(static_source_name) if static_source ~= nil then - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero obs.obs_source_update(static_source, settings) end obs.obs_source_release(static_source) @@ -812,9 +667,9 @@ function set_text_visiblity(end_status) -- if fade is disabled, change visibility immediately if not text_fade_enabled then if end_status == TEXT_HIDDEN then - opacity = 0 + text_opacity = 0 elseif end_status == TEXT_VISIBLE then - opacity = 100 + text_opacity = 100 end text_status = end_status apply_source_opacity() @@ -828,12 +683,14 @@ function set_text_visiblity(end_status) end start_fade_timer() end + update_source_text() end -- transition to the next lyrics, use fade if enabled -- if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) dbg_method("transition_lyric_text") + dbg_bool("using_source", using_source) -- update the lyrics display immediately on 2 conditions -- a) the text is hidden or hiding, and we will not force it to show -- b) text fade is not enabled @@ -843,13 +700,14 @@ function transition_lyric_text(force_show) update_source_text() dbg_inner("hidden") elseif not text_fade_enabled then - update_source_text() set_text_visiblity(TEXT_VISIBLE) + update_source_text() dbg_inner("no text fade") else text_status = TEXT_TRANSITION_OUT start_fade_timer() end + dbg_bool("using_source", using_source) end function start_fade_timer() @@ -861,12 +719,12 @@ function start_fade_timer() end function fade_callback() - dbg_method("fade_callback") -- if not in a transitory state, exit callback if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then timer_exists = false obs.remove_current_callback() dbg_inner("ended fade timer") + lyric_change = false end -- the amount we want to change opacity by local opacity_delta = 1 + text_fade_speed @@ -879,7 +737,7 @@ function fade_callback() -- completed fade out, determine next move text_opacity = 0 if text_status == TEXT_TRANSITION_OUT then - update_source_text() + update_source_text() --------------------------------------------UPDATE to NEW LYRIC BETWEEN FADES text_status = TEXT_TRANSITION_IN else text_status = TEXT_HIDDEN @@ -900,6 +758,7 @@ function fade_callback() end function prepare_song_by_index(index) + dbg_method("prepare_song_by_index") if index <= #prepared_songs then prepare_song_by_name(prepared_songs[index]) end @@ -907,7 +766,8 @@ end -- prepares lyrics of the song function prepare_song_by_name(name) - -- pause_timer = true + dbg_method("prepare_song_by_name") + lyric_change = true if name == nil then return false end @@ -1233,7 +1093,7 @@ function load_song_directory() song_directory = {} local filenames = {} local dir = obs.os_opendir(get_songs_folder_path()) - -- get_songs_folder_path()) + -- get_songs_folder_path()) local entry local songExt local songTitle @@ -1375,7 +1235,6 @@ end function load_prepared() dbg_method("load_prepared") - -- pause_timer = true local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") if file ~= nil then for line in file:lines() do @@ -1384,13 +1243,120 @@ function load_prepared() prepared_index = 1 file:close() end - -- pause_timer = false return true end +-- updates the selected lyrics +function update_source_text() + dbg_method("update_source_text") + + local text = "" + local alttext = "" + local next_lyric = "" + local next_alternate = "" + local static = static_text + local mstatic = static -- save static for use with monitor + local title = "" + if not using_source then + dbg_custom("Load title from prepared: " .. prepared_index) + if prepared_index ~= nil or prepared_index ~= 0 then + title = prepared_songs[prepared_index] + end + else + dbg_custom("Load title from source") + title = source_song_title + end + local mtitle = title -- save title for use with monitor + + local source = obs.obs_get_source_by_name(source_name) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) + + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then + if #lyrics > 0 then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] + end + end + if #alternate > 0 then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] + end + end + + if link_text then + if string.len(text) == 0 and string.len(alttext) == 0 then + static = "" + title = "" + end + end + end + -- update source texts + if source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + next_lyric = lyrics[page_index + 1] + if (next_lyric == nil) then + next_lyric = "" + end + end + if alt_source ~= nil then + local settings = obs.obs_data_create() -- setup TEXT settings with opacity values + obs.obs_data_set_string(settings, "text", alttext) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" + end + end + if stat_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) + end + if title_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) + end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) + obs.obs_source_release(title_source) + + local next_prepared = "" + if using_source then + next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song + elseif prepared_index < #prepared_songs then + next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song + else + if source_active then + next_prepared = source_song_title -- plan to go back to source loaded song + else + next_prepared = prepared_songs[1] -- plan to loop around to first prepared song + end + end + + update_monitor( + title, + text:gsub("\n", "
• "), + next_lyric:gsub("\n", "
• "), + alttext:gsub("\n", "
• "), + next_alternate:gsub("\n", "
• "), + next_prepared + ) +end + function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) dbg_method("update_monitor") - local tableback = "#000000" + local tableback = "black" local text = "" text = text .. "" text = text .. "" @@ -1404,11 +1370,13 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) text = text .. "
" - if not using_source then - text = - text .. - "
Prepared Song: " .. - prepared_index + text = + text .. + "
" + if using_source then + text = text .. "From Source: " .. load_scene .. "
" + else + text = text .. "Prepared Song: " .. prepared_index text = text .. " of " .. #prepared_songs .. "
" @@ -1419,49 +1387,56 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) page_index text = text .. "
of " .. #lyrics .. "
" text = text .. "
" - -- show if song is from source or prepared songs - text = text .. "From: " - if using_source then - text = text .. "Source" - else - text = text .. "Prepared" + if not anythingActive() then + tableback = "#440000" + end + local visbgTitle = tableback + local visbgText = tableback + if text_status == TEXT_HIDDEN or text_status == TEXT_HIDING then + visbgText = "maroon" + if link_text then + visbgTitle = "maroon" + end end text = text .. "
" - if song ~= "" then + if song ~= "" and song ~= nil then text = text .. "" - text = text .. "" + text = + text .. + "" end - if lyric ~= "" then + if lyric ~= "" and lyric ~= nil then text = text .. "" - text = text .. "" + text = text .. "" end - if nextlyric ~= "" then + if nextlyric ~= "" and nextlyric ~= nil then text = text .. "" text = text .. "" end - if alt ~= "" then + if alt ~= "" and alt ~= nil then text = text .. "" - text = text .. "" + text = + text .. "" end - if nextalt ~= "" then + if nextalt ~= "" and nextalt ~= nil then text = text .. "" text = text .. "" end - if nextsong ~= "" then + if nextsong ~= "" and nextsong ~= nil then text = text .. "" @@ -1469,6 +1444,7 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) end text = text .. "
Song
Title
" .. song .. "
" .. song .. "
Current
Page
• " .. lyric .. "
• " .. lyric .. "
Next
Page
• " .. nextlyric .. "
Alt
Lyric
• " .. alt .. "
• " .. alt .. "
Next
Alt
• " .. nextalt .. "
Next
Song:
" local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") + dbg_inner("write file") file:write(text) file:close() return true @@ -1645,23 +1621,27 @@ function script_properties() obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) + prepare_props = prep_prop for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prep_prop, name, name) end obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) + obs.obs_properties_add_button(script_props, "prop_undo_button", "UnPrepare Selected Song", remove_selection_made) obs.obs_properties_add_button(script_props, "prop_clear_button", "Clear Prepared Songs", clear_prepared_clicked) obs.obs_properties_add_button(script_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(script_props, "prop_next_button", "Next Lyric", next_button_clicked) obs.obs_properties_add_button(script_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) obs.obs_properties_add_button(script_props, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(script_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(script_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) obs.obs_properties_add_button( script_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked ) - if #prepared_songs > 0 and prepared_index > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[prepared_index]) + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) end obs.obs_properties_apply_settings(script_props, script_sets) @@ -1735,8 +1715,6 @@ function script_update(settings) if reload then if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then prepare_selected(prepared_songs[prepared_index]) - -- page_index = 1 - -- transition_lyric_text(false) end end end @@ -1745,13 +1723,6 @@ end function script_defaults(settings) dbg_method("script_defaults") obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - -- obs.obs_data_set_default_string(settings, "prop_source_list", prepared_songs[1] ) - -- if #prepared_songs ~= 0 then - -- prepared_songs[prepared_index] = prepared_songs[1] - -- prepared_index = 1 - -- else - -- prepared_songs[prepared_index] = "" - -- end if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -1767,37 +1738,30 @@ function script_save(settings) save_prepared() local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) - -- hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) obs.obs_data_array_release(hotkey_save_array) hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) obs.obs_data_set_array(settings, "lyric_prev_hotkey", hotkey_save_array) - -- hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) obs.obs_data_array_release(hotkey_save_array) hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) obs.obs_data_set_array(settings, "lyric_clear_hotkey", hotkey_save_array) - -- hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) obs.obs_data_array_release(hotkey_save_array) hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) obs.obs_data_set_array(settings, "next_prepared_hotkey", hotkey_save_array) - -- hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) obs.obs_data_array_release(hotkey_save_array) hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) obs.obs_data_set_array(settings, "previous_prepared_hotkey", hotkey_save_array) - -- hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) obs.obs_data_array_release(hotkey_save_array) hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) obs.obs_data_set_array(settings, "home_song_hotkey", hotkey_save_array) - -- hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) obs.obs_data_array_release(hotkey_save_array) hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) - -- hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) obs.obs_data_array_release(hotkey_save_array) end @@ -1847,8 +1811,13 @@ function script_load(settings) end -- must be set prior to calling any file functions load_song_directory() load_prepared() - -- obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for Source * Marker (WZ) - -- obs.timer_add(timer_callback, 50) -- Setup callback for text fade effect + local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") + local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") + local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") + obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) + obs.obs_property_set_enabled(fade_speed_prop, not transition_set) + transition_enabled = transition_set + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end -------- @@ -1963,9 +1932,9 @@ end source_def.create = function(settings, source) data = {} sh = obs.obs_source_get_signal_handler(source) - obs.signal_handler_connect(sh, "activate", source_active) -- Set Active Callback + obs.signal_handler_connect(sh, "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(sh, "show", source_showing) -- Set Preview Callback - obs.signal_handler_connect(sh, "hide", source_hidden) -- Set Preview Callback + obs.signal_handler_connect(sh, "deactivate", source_inactive) -- Set Preview Callback return data end @@ -1977,58 +1946,32 @@ end source_def.destroy = function(source) end --- function on_event(event) --- if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then --- set_current_scene_name() --- rename_source() --- -- update_source_text() --- transition_lyric_text(false) --- end +function updateSources() + obs.timer_remove(updateSources) + update_source_text() +end --- end +function on_event(event) + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then + dbg_method("on_event") + obs.timer_add(updateSources, 100) -- delay updating source text until all sources have been removed by OBS + end +end function load_song(source, preview) dbg_method("load_song") local settings = obs.obs_source_get_settings(source) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - -- if song ~= prepared_songs[prepared_index] then if song == nil or song == "" then return end dbg_inner("load_song: " .. song) - -- local prop_prep_list = obs.obs_properties_get(script_props, "prop_prepared_list") - -- if scene_load_complete then - -- obs.obs_property_list_item_remove(prop_prep_list, 0) - -- table.remove(prepared_songs , 1) -- clear older scene loaded song - -- end - -- obs.obs_property_list_insert_string(prop_prep_list, 0, song, song) - -- table.insert(prepared_songs, 1, song) - -- scene_load_complete = true - -- obs.obs_data_set_string(script_sets, "prop_prepared_list", song) - -- obs.obs_properties_apply_settings(script_props, script_sets) - - -- update scene info - -- set_current_scene_name() - -- load_scene = current_scene - using_source = true - - -- prepare song and update lyrics - -- if (song ~= prepared_songs[prepared_index]) then + load_source = source prepare_selected(song) - -- end - - -- save_prepared() - -- page_index = 1 - -- prepared_index = 1 - - -- update_source_text() - -- text_opacity = 99 - -- text_fade_dir = 2 - -- transition_lyric_text(true) - -- end - -- TODO: ensure home on activate working correctly + --transition_lyric_text() + --set_text_visiblity(TEXT_SHOWING) if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end @@ -2036,20 +1979,33 @@ function load_song(source, preview) obs.obs_data_release(settings) end -function source_active(cd) +function source_isactive(cd) + dbg_method("source_active") local source = obs.calldata_source(cd, "source") if source == nil then return end + dbg_inner("source active") + load_scene = get_current_scene_name() load_song(source, false) + source_active = true -- using source lyric +end + +function source_inactive(cd) + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + dbg_inner("source inactive") + source_active = false -- indicates source loading lyric is active (but using prepared lyrics is still possible) end function source_showing(cd) + dbg_method("source_showing") local source = obs.calldata_source(cd, "source") if source == nil then return end - -- if sourceActive() then return end load_song(source, true) end @@ -2077,4 +2033,14 @@ function dbg_custom(message) end end +function dbg_bool(message, value) + if DEBUG_BOOL then + if value then + dbg("BOOL: " .. message .. " = true") + else + dbg("BOOL: " .. message .. " = false") + end + end +end + obs.obs_register_source(source_def) From e84e9200e8d764e6b03aad3d4660a15ee20bfe84 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 23 Sep 2021 15:45:28 -0600 Subject: [PATCH 006/105] Update lyrics.lua Added a change that allows Titles to just be valid filenames. If used as a filename, the actual title can be marked up in lyrics with #T: title --- lyrics.lua | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 998a534..f2c5c3c 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -789,6 +789,7 @@ function prepare_song_by_name(name) lyrics = {} alternate = {} static_text = "" + alt_title = "" local adjusted_display_lines = display_lines local refrain_display_lines = display_lines local alternate_display_lines = display_lines @@ -861,6 +862,13 @@ function prepare_song_by_name(name) static_text = line new_lines = 0 end + local title_index = line:find("#T:") + if title_index ~= nil then + local title_indexEnd = line:find("%s+", title_index + 1) + line = line:sub(title_indexEnd + 1) + alt_title = line + new_lines = 0 + end local alt_index = line:find("#A:") if alt_index ~= nil then local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) @@ -1257,15 +1265,19 @@ function update_source_text() local static = static_text local mstatic = static -- save static for use with monitor local title = "" - if not using_source then - dbg_custom("Load title from prepared: " .. prepared_index) - if prepared_index ~= nil or prepared_index ~= 0 then - title = prepared_songs[prepared_index] - end - else - dbg_custom("Load title from source") - title = source_song_title - end + if alt_title ~= "" then + title = alt_title + else + if not using_source then + dbg_custom("Load title from prepared: " .. prepared_index) + if prepared_index ~= nil or prepared_index ~= 0 then + title = prepared_songs[prepared_index] + end + else + dbg_custom("Load title from source") + title = source_song_title + end + end local mtitle = title -- save title for use with monitor local source = obs.obs_get_source_by_name(source_name) @@ -1501,7 +1513,7 @@ end function script_properties() dbg_method("script_properties") script_props = obs.obs_properties_create() - obs.obs_properties_add_text(script_props, "prop_edit_song_title", "Song Title", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(script_props, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) local lyric_prop = obs.obs_properties_add_text(script_props, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_property_set_long_description(lyric_prop, "Lyric Text with Markup") @@ -1652,7 +1664,7 @@ end -- A function named script_description returns the description shown to -- the user function script_description() - return "Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2) Author: Amirchev & DC Strato; with significant contributions from taxilian.
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
" + return "Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2) Author: Amirchev & DC Strato; with significant contributions from taxilian.
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
" end function change_fade_property(props, prop, settings) From 8217f5293d4f7a6b99ee2991c1acaeab98fcc917 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Fri, 24 Sep 2021 02:00:21 -0600 Subject: [PATCH 007/105] Update lyrics.lua Added Formatted settings with collapsible groups --- lyrics.lua | 162 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 101 insertions(+), 61 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index f2c5c3c..9cfa798 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1513,62 +1513,78 @@ end function script_properties() dbg_method("script_properties") script_props = obs.obs_properties_create() - obs.obs_properties_add_text(script_props, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) +----------- + info_prop = obs.obs_properties_add_bool(script_props, "info_showing", "Hide Song Information") + obs.obs_property_set_modified_callback(info_prop, change_info_visible) + gp = obs.obs_properties_create() + obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) local lyric_prop = - obs.obs_properties_add_text(script_props, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_property_set_long_description(lyric_prop, "Lyric Text with Markup") - obs.obs_properties_add_button(script_props, "prop_save_button", "Save Song", save_song_clicked) - local prop_dir_list = - obs.obs_properties_add_list( - script_props, - "prop_directory_list", - "Song Directory", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) + obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) + obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) + obs.obs_properties_add_button(gp, "prop_opensong_button","Edit Song with System Editor", open_song_clicked) + obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) + obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) +------------ + prep_prop = obs.obs_properties_add_bool(script_props, "prepared_showing", "Hide Source Selections") + obs.obs_property_set_modified_callback(prep_prop, change_prepared_visible) + gp = obs.obs_properties_create() + local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(prop_dir_list, name, name) end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - - obs.obs_properties_add_button(script_props, "prop_prepare_button", "Prepare Song", prepare_song_clicked) - obs.obs_properties_add_button(script_props, "prop_delete_button", "Delete Song", delete_song_clicked) - obs.obs_properties_add_button( - script_props, - "prop_opensong_button", - "Edit Song with System Editor", - open_song_clicked - ) - obs.obs_properties_add_button(script_props, "prop_open_button", "Open Songs Folder", open_button_clicked) - local lines_prop = obs.obs_properties_add_int(script_props, "prop_lines_counter", "Lines to Display", 1, 100, 1) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) + gps = obs.obs_properties_create() + obs.obs_properties_add_group(gp, "line", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) + prepare_props = prep_prop + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prep_prop, name, name) + end + obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) + obs.obs_properties_add_button(gps, "prop_undo_button", "UnPrepare Selected Song", remove_selection_made) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) + obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) +------ + options_prop = obs.obs_properties_add_bool(script_props, "options_showing", "Hide Display Options") + obs.obs_property_set_modified_callback(options_prop, change_options_visible) + + gp = obs.obs_properties_create() + local lines_prop = obs.obs_properties_add_int(gp, "prop_lines_counter", "Lines to Display", 1, 100, 1) obs.obs_property_set_long_description( lines_prop, "Sets default lines per page of lyric, overwritten by Markup: #L:n" ) - local prop_lines = obs.obs_properties_add_bool(script_props, "prop_lines_bool", "Strictly ensure number of lines") + local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") local link_prop = - obs.obs_properties_add_bool(script_props, "link_text", "Only show title and static text with lyrics") + obs.obs_properties_add_bool(gp, "link_text", "Only show title and static text with lyrics") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") local transition_prop = - obs.obs_properties_add_bool(script_props, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") obs.obs_property_set_modified_callback(transition_prop, change_transition_property) obs.obs_property_set_long_description( transition_prop, "Use with Studio Mode, duplicate sources, and OBS source transitions" ) - local fade_prop = obs.obs_properties_add_bool(script_props, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) + local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - obs.obs_properties_add_int_slider(script_props, "text_fade_speed", "Fade Speed", 1, 10, 1) - + obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) +------------- + src_prop = obs.obs_properties_add_bool(script_props, "src_showing", "Hide Source Selections") + obs.obs_property_set_modified_callback(src_prop, change_src_visible) + gp = obs.obs_properties_create() local source_prop = obs.obs_properties_add_list( - script_props, + gp, "prop_source_list", "Text Source", obs.OBS_COMBO_TYPE_LIST, @@ -1577,7 +1593,7 @@ function script_properties() obs.obs_property_set_long_description(source_prop, "Shows main lyric text") local title_source_prop = obs.obs_properties_add_list( - script_props, + gp, "prop_title_list", "Title Source", obs.OBS_COMBO_TYPE_LIST, @@ -1586,7 +1602,7 @@ function script_properties() obs.obs_property_set_long_description(title_source_prop, "Shows text from song title") local alternate_source_prop = obs.obs_properties_add_list( - script_props, + gp, "prop_alternate_list", "Alternate Source", obs.OBS_COMBO_TYPE_LIST, @@ -1595,7 +1611,7 @@ function script_properties() obs.obs_property_set_long_description(alternate_source_prop, "Shows text annotated with #A[ and #A]") local static_source_prop = obs.obs_properties_add_list( - script_props, + gp, "prop_static_list", "Static Source", obs.OBS_COMBO_TYPE_LIST, @@ -1624,38 +1640,27 @@ function script_properties() end end obs.source_list_release(sources) - obs.obs_properties_add_button(script_props, "prop_refresh", "Refresh Sources", refresh_button_clicked) - local prep_prop = - obs.obs_properties_add_list( - script_props, - "prop_prepared_list", - "Prepared Songs", - obs.OBS_COMBO_TYPE_EDITABLE, - obs.OBS_COMBO_FORMAT_STRING - ) - prepare_props = prep_prop - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prep_prop, name, name) - end - obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) - obs.obs_properties_add_button(script_props, "prop_undo_button", "UnPrepare Selected Song", remove_selection_made) - obs.obs_properties_add_button(script_props, "prop_clear_button", "Clear Prepared Songs", clear_prepared_clicked) - obs.obs_properties_add_button(script_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) - obs.obs_properties_add_button(script_props, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(script_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) - obs.obs_properties_add_button(script_props, "prop_home_button", "Reset to Song Start", home_button_clicked) - obs.obs_properties_add_button(script_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) - obs.obs_properties_add_button(script_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button( - script_props, - "prop_reset_button", - "Reset to First Prepared Song", - reset_button_clicked - ) + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) + obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) +------------------ + ctrl_prop = obs.obs_properties_add_bool(script_props, "ctrl_showing", "Hide Lyric Controls") + obs.obs_property_set_modified_callback(ctrl_prop, change_ctrl_visible) + + gp = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(gp, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(gp, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(gp, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(gp, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(gp, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) + obs.obs_properties_add_button(gp,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) + obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,gp) +----------------- if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) end - + pp = obs.obs_properties_get(script_props,"ctrl_grp") + obs.obs_property_set_visible(pp, true) obs.obs_properties_apply_settings(script_props, script_sets) return script_props @@ -1667,6 +1672,41 @@ function script_description() return "Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2) Author: Amirchev & DC Strato; with significant contributions from taxilian.
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
" end +function change_info_visible(props, prop, settings) + local visible = obs.obs_data_get_bool(settings, "info_showing") + local ctrlpp = obs.obs_properties_get(script_props,"info_grp") + obs.obs_property_set_visible(ctrlpp, not visible) + return true +end + +function change_prepared_visible(props, prop, settings) + local visible = obs.obs_data_get_bool(settings, "prepared_showing") + local ctrlpp = obs.obs_properties_get(script_props,"prep_grp") + obs.obs_property_set_visible(ctrlpp, not visible) + return true +end + +function change_options_visible(props, prop, settings) + local visible = obs.obs_data_get_bool(settings, "options_showing") + local ctrlpp = obs.obs_properties_get(script_props,"disp_grp") + obs.obs_property_set_visible(ctrlpp, not visible) + return true +end + +function change_src_visible(props, prop, settings) + local visible = obs.obs_data_get_bool(settings, "src_showing") + local ctrlpp = obs.obs_properties_get(script_props,"src_grp") + obs.obs_property_set_visible(ctrlpp, not visible) + return true +end + +function change_ctrl_visible(props, prop, settings) + local visible = obs.obs_data_get_bool(settings, "ctrl_showing") + local ctrlpp = obs.obs_properties_get(script_props,"ctrl_grp") + obs.obs_property_set_visible(ctrlpp, not visible) + return true +end + function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") From afb838bcbdc4ba16a3277ce8c4591ec1a40fde36 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Fri, 24 Sep 2021 02:48:11 -0600 Subject: [PATCH 008/105] Update lyrics.lua Added expand/collapse all button --- lyrics.lua | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lyrics.lua b/lyrics.lua index 9cfa798..301c6e8 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1,4 +1,4 @@ ---- Copyright 2020 amirchev +----- Copyright 2020 amirchev -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -88,6 +88,7 @@ text_opacity = 100 text_fade_speed = 1 text_fade_enabled = false load_source = nil +expandcollapse = false transition_completed = false @@ -1513,6 +1514,7 @@ end function script_properties() dbg_method("script_properties") script_props = obs.obs_properties_create() + obs.obs_properties_add_button(script_props, "expand_all_button", "Expand/Collapse All Groups", expand_all_groups) ----------- info_prop = obs.obs_properties_add_bool(script_props, "info_showing", "Hide Song Information") obs.obs_property_set_modified_callback(info_prop, change_info_visible) @@ -1671,6 +1673,25 @@ end function script_description() return "Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2) Author: Amirchev & DC Strato; with significant contributions from taxilian.
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
" end +function expand_all_groups(props, prop, settings) + expandcollapse = not expandcollapse + obs.obs_data_set_bool(script_sets, "info_showing", not expandcollapse) + local ctrlpp = obs.obs_properties_get(script_props,"info_grp") + obs.obs_property_set_visible(ctrlpp, expandcollapse) + obs.obs_data_set_bool(script_sets, "prepared_showing", not expandcollapse) + local ctrlpp = obs.obs_properties_get(script_props,"prep_grp") + obs.obs_property_set_visible(ctrlpp, expandcollapse) + obs.obs_data_set_bool(script_sets, "options_showing", not expandcollapse) + local ctrlpp = obs.obs_properties_get(script_props,"disp_grp") + obs.obs_property_set_visible(ctrlpp, expandcollapse) + obs.obs_data_set_bool(script_sets, "src_showing", not expandcollapse) + local ctrlpp = obs.obs_properties_get(script_props,"src_grp") + obs.obs_property_set_visible(ctrlpp, expandcollapse) + obs.obs_data_set_bool(script_sets, "ctrl_showing", not expandcollapse) + local ctrlpp = obs.obs_properties_get(script_props,"ctrl_grp") + obs.obs_property_set_visible(ctrlpp, expandcollapse) + return true +end function change_info_visible(props, prop, settings) local visible = obs.obs_data_get_bool(settings, "info_showing") From 0e071a0cd1481e399c6445057e5324ac322304c6 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Fri, 24 Sep 2021 23:46:53 -0600 Subject: [PATCH 009/105] Update lyrics.lua This seems to work now. Still testing --- lyrics.lua | 65 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 301c6e8..0ba5e5b 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1,4 +1,4 @@ ------ Copyright 2020 amirchev +--- Copyright 2020 amirchev -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -1457,7 +1457,7 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) end text = text .. "" local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") - dbg_inner("write file") + dbg_inner("write monitor file") file:write(text) file:close() return true @@ -1503,6 +1503,24 @@ function get_song_text(name) return song_lines end +function get_song_tag(name) + local song_lines = {} + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + song_lines[#song_lines + 1] = line + end + file:close() + end + + return song_lines +end -- ------ ---------------- ------------------------ OBS DEFAULT FUNCTIONS @@ -1807,7 +1825,8 @@ function script_defaults(settings) end -- A function named script_save will be called when the script is saved -function script_save(settings) +function script_save(settings) + dbg_method("script_save") save_prepared() local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) @@ -2043,8 +2062,8 @@ function load_song(source, preview) using_source = true load_source = source prepare_selected(song) - --transition_lyric_text() - --set_text_visiblity(TEXT_SHOWING) + transition_lyric_text() + set_text_visiblity(TEXT_VISIBLE) if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end @@ -2061,6 +2080,7 @@ function source_isactive(cd) dbg_inner("source active") load_scene = get_current_scene_name() load_song(source, false) + source_active = true -- using source lyric end @@ -2082,6 +2102,40 @@ function source_showing(cd) load_song(source, true) end +function ParseCSVLine (line,sep) + local res = {} + local pos = 1 + sep = sep or ',' + while true do + local c = string.sub(line,pos,pos) + if (c == "") then break end + if (c == '"') then + local txt = "" + repeat + local startp,endp = string.find(line,'^%b""',pos) + txt = txt..string.sub(line,startp+1,endp-1) + pos = endp + 1 + c = string.sub(line,pos,pos) + if (c == '"') then txt = txt..'"' end + until (c ~= '"') + table.insert(res,txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp,endp = string.find(line,sep,pos) + if (startp) then + table.insert(res,string.sub(line,pos,startp-1)) + pos = endp + 1 + else + table.insert(res,string.sub(line,pos)) + break + end + end + end + return res +end + + function dbg(message) if DEBUG then print(message) @@ -2116,4 +2170,5 @@ function dbg_bool(message, value) end end + obs.obs_register_source(source_def) From ae9b9cb059b6694c25fd4ea02d476f9b3b365d31 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 27 Sep 2021 20:22:39 -0600 Subject: [PATCH 010/105] Update lyrics.lua I think I addressed everything in your test list. However, I can't guarantee I fully understand your exact use case so You Tell Me. WZ --- lyrics.lua | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 0ba5e5b..172a7cc 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -12,8 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- added delete single prepared song (WZ) - obs = obslua bit = require("bit") @@ -59,9 +57,10 @@ lyric_change = false -- Text and Static should only fade when lyrics are changin source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) -transition_enabled = false -load_scene = "" + +load_scene = "" -- name of scene loading a lyric with a source timer_exists = false +forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility -- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID @@ -90,6 +89,7 @@ text_fade_enabled = false load_source = nil expandcollapse = false +transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false -- simple debugging/print mechanism @@ -331,7 +331,8 @@ function home_prepared(pressed) obs.obs_data_set_string(script_sets, "prop_prepared_list", "") end obs.obs_properties_apply_settings(props, script_sets) - + prepared_index = 1 + prepare_selected(prepared_songs[prepared_index]) return true end @@ -452,13 +453,17 @@ end -- prepare song button clicked function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") + if #prepared_songs == 0 then + forceNoFade = true + set_text_visiblity(TEXT_HIDDEN) + end prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) - if #prepared_songs == 1 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - prepare_song_by_index(#prepared_songs) - end + + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) + -- prepare_song_by_index(#prepared_songs) + --end obs.obs_properties_apply_settings(props, script_sets) save_prepared() return true @@ -572,7 +577,7 @@ function prepare_selected(name) else source_song_title = name end - transition_lyric_text(false) + update_source_text() return true end @@ -666,7 +671,8 @@ function set_text_visiblity(end_status) return end -- if fade is disabled, change visibility immediately - if not text_fade_enabled then + + if not text_fade_enabled or forceNoFade then if end_status == TEXT_HIDDEN then text_opacity = 0 elseif end_status == TEXT_VISIBLE then @@ -685,6 +691,7 @@ function set_text_visiblity(end_status) start_fade_timer() end update_source_text() + forceNoFade = false end -- transition to the next lyrics, use fade if enabled @@ -1547,7 +1554,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------ - prep_prop = obs.obs_properties_add_bool(script_props, "prepared_showing", "Hide Source Selections") + prep_prop = obs.obs_properties_add_bool(script_props, "prepared_showing", "Hide Manage Prepared Songs") obs.obs_property_set_modified_callback(prep_prop, change_prepared_visible) gp = obs.obs_properties_create() local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) @@ -2055,15 +2062,12 @@ function load_song(source, preview) local settings = obs.obs_source_get_settings(source) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - if song == nil or song == "" then - return - end dbg_inner("load_song: " .. song) using_source = true load_source = source prepare_selected(song) - transition_lyric_text() - set_text_visiblity(TEXT_VISIBLE) + --transition_lyric_text() + --set_text_visiblity(TEXT_VISIBLE) if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end From 5e76bf9f72b3b9d9ec42aa1da1de8dd18a7ffa2c Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 27 Sep 2021 20:31:02 -0600 Subject: [PATCH 011/105] Update lyrics.lua --- lyrics.lua | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 172a7cc..20c8a30 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2106,40 +2106,6 @@ function source_showing(cd) load_song(source, true) end -function ParseCSVLine (line,sep) - local res = {} - local pos = 1 - sep = sep or ',' - while true do - local c = string.sub(line,pos,pos) - if (c == "") then break end - if (c == '"') then - local txt = "" - repeat - local startp,endp = string.find(line,'^%b""',pos) - txt = txt..string.sub(line,startp+1,endp-1) - pos = endp + 1 - c = string.sub(line,pos,pos) - if (c == '"') then txt = txt..'"' end - until (c ~= '"') - table.insert(res,txt) - assert(c == sep or c == "") - pos = pos + 1 - else - local startp,endp = string.find(line,sep,pos) - if (startp) then - table.insert(res,string.sub(line,pos,startp-1)) - pos = endp + 1 - else - table.insert(res,string.sub(line,pos)) - break - end - end - end - return res -end - - function dbg(message) if DEBUG then print(message) From c7a8d95572094de7f699791fdf0817c132a90b8b Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 27 Sep 2021 20:54:57 -0600 Subject: [PATCH 012/105] Update lyrics.lua --- lyrics.lua | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 20c8a30..e751c2b 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1510,24 +1510,6 @@ function get_song_text(name) return song_lines end -function get_song_tag(name) - local song_lines = {} - local path = {} - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") - end - local file = io.open(path, "r") - if file ~= nil then - for line in file:lines() do - song_lines[#song_lines + 1] = line - end - file:close() - end - - return song_lines -end -- ------ ---------------- ------------------------ OBS DEFAULT FUNCTIONS From 7be12c78cd12f52f57ca375f722f72322d5d2b01 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 27 Sep 2021 21:15:01 -0600 Subject: [PATCH 013/105] Update lyrics.lua --- lyrics.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index e751c2b..a840552 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -12,6 +12,8 @@ -- See the License for the specific language governing permissions and -- limitations under the License. +-- added delete single prepared song (WZ) + obs = obslua bit = require("bit") @@ -57,10 +59,10 @@ lyric_change = false -- Text and Static should only fade when lyrics are changin source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) - -load_scene = "" -- name of scene loading a lyric with a source +transition_enabled = false +load_scene = "" timer_exists = false -forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility +forceNoFade = false -- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID @@ -89,11 +91,10 @@ text_fade_enabled = false load_source = nil expandcollapse = false -transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false -- simple debugging/print mechanism -DEBUG = false -- on/off switch for entire debugging mechanism +DEBUG = true -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints DEBUG_CUSTOM = true -- print custom debugging messages @@ -453,6 +454,7 @@ end -- prepare song button clicked function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") + print("prepared=" .. #prepared_songs) if #prepared_songs == 0 then forceNoFade = true set_text_visiblity(TEXT_HIDDEN) @@ -573,7 +575,6 @@ function prepare_selected(name) page_index = 1 if not using_source then prepared_index = get_index_in_list(prepared_songs, name) - dbg_custom("prepared_index: " .. prepared_index) else source_song_title = name end @@ -2088,6 +2089,7 @@ function source_showing(cd) load_song(source, true) end + function dbg(message) if DEBUG then print(message) From deceaa849e1cf8ba6fd157bd9bc5761a319b9ec6 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Tue, 28 Sep 2021 00:00:44 -0600 Subject: [PATCH 014/105] Update lyrics.lua I added meta tags. You can decide if you want to include them in the next update. --- lyrics.lua | 149 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 124 insertions(+), 25 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index a840552..a20da5d 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -59,10 +59,10 @@ lyric_change = false -- Text and Static should only fade when lyrics are changin source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) -transition_enabled = false -load_scene = "" + +load_scene = "" -- name of scene loading a lyric with a source timer_exists = false -forceNoFade = false +forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility -- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID @@ -91,10 +91,11 @@ text_fade_enabled = false load_source = nil expandcollapse = false +transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false -- simple debugging/print mechanism -DEBUG = true -- on/off switch for entire debugging mechanism +DEBUG = false -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints DEBUG_CUSTOM = true -- print custom debugging messages @@ -454,7 +455,6 @@ end -- prepare song button clicked function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") - print("prepared=" .. #prepared_songs) if #prepared_songs == 0 then forceNoFade = true set_text_visiblity(TEXT_HIDDEN) @@ -476,6 +476,7 @@ function refresh_button_clicked(props, p) local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") local static_source_prop = obs.obs_properties_get(props, "prop_static_list") local title_source_prop = obs.obs_properties_get(props, "prop_title_list") + local prop_dir_list = obs.obs_properties_get(props,"prop_directory_list") obs.obs_property_list_clear(source_prop) -- clear current properties list obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list obs.obs_property_list_clear(static_source_prop) -- clear current properties list @@ -504,6 +505,13 @@ function refresh_button_clicked(props, p) end obs.source_list_release(sources) load_song_directory() + table.sort(song_directory) + obs.obs_property_list_clear(prop_dir_list) -- clear directories + for _, name in ipairs(song_directory) do + print(name) + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_properties_apply_settings(props, script_sets) return true end @@ -1103,46 +1111,134 @@ end ------------------------ FILE FUNCTIONS ---------------- -------- +-- delete previewed song +function delete_song(name) + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + os.remove(path) + table.remove(song_directory, get_index_in_list(song_directory, name)) + load_song_directory() +end -- loads the song directory function load_song_directory() - -- pause_timer = true + local keytext = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + local keys = ParseCSVLine(keytext) song_directory = {} local filenames = {} + local tags = {} local dir = obs.os_opendir(get_songs_folder_path()) -- get_songs_folder_path()) local entry local songExt local songTitle + local goodEntry = true + repeat entry = obs.os_readdir(dir) if entry and not entry.directory and - (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") - then - songExt = obs.os_get_path_extension(entry.d_name) - songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) - if songExt == ".enc" then - song_directory[#song_directory + 1] = dec(songTitle) - else - song_directory[#song_directory + 1] = songTitle - end - end + (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") then + songExt = obs.os_get_path_extension(entry.d_name) + songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) + tags = readTags(songTitle) + goodEntry = true + + if #keys>0 then + if keys[1] ~= "*" or #tags> 0 then + goodEntry = false + end + end + if (#tags > 0 and #keys > 0) then + goodEntry = false + for t = 1, #tags do + for k = 1, #keys do + if tags[t] == keys[k] then + goodEntry = true + break + end + end + if goodEntry then + break + end + end + end + if goodEntry then + if songExt == ".enc" then + song_directory[#song_directory + 1] = dec(songTitle) + else + song_directory[#song_directory + 1] = songTitle + end + end + end until not entry obs.os_closedir(dir) -- pause_timer = false end --- delete previewed song -function delete_song(name) +function readTags(name) + local meta = "" + local path = {} if testValid(name) then path = get_song_file_path(name, ".txt") else path = get_song_file_path(enc(name), ".enc") end - os.remove(path) - table.remove(song_directory, get_index_in_list(song_directory, name)) - load_song_directory() + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + meta = line + break; + end + file:close() + end + local meta_index = meta:find("//meta ") -- Look for meta block Set + if meta_index ~= nil then + meta = meta:sub(meta_index + 7) + return ParseCSVLine(meta) + end + return {} +end + +function ParseCSVLine (line) + local res = {} + local pos = 1 + sep = ',' + while true do + local c = string.sub(line,pos,pos) + if (c == "") then break end + if (c == '"') then + local txt = "" + repeat + local startp,endp = string.find(line,'^%b""',pos) + txt = txt..string.sub(line,startp+1,endp-1) + pos = endp + 1 + c = string.sub(line,pos,pos) + if (c == '"') then txt = txt..'"' end + until (c ~= '"') + txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + table.insert(res,txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp,endp = string.find(line,sep,pos) + if (startp) then + local t = string.sub(line,pos,startp-1) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + table.insert(res,t) + pos = endp + 1 + else + local t = string.sub(line,pos) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + table.insert(res,t) + break + end + end + end + return res end local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet @@ -1540,15 +1636,19 @@ function script_properties() prep_prop = obs.obs_properties_add_bool(script_props, "prepared_showing", "Hide Manage Prepared Songs") obs.obs_property_set_modified_callback(prep_prop, change_prepared_visible) gp = obs.obs_properties_create() - local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(prop_dir_list, name, name) end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) - gps = obs.obs_properties_create() - obs.obs_properties_add_group(gp, "line", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "prop_refresh", "Refresh Directory", refresh_button_clicked) + obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + obs.obs_properties_add_group(gp, "line", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) prepare_props = prep_prop for _, name in ipairs(prepared_songs) do @@ -2089,7 +2189,6 @@ function source_showing(cd) load_song(source, true) end - function dbg(message) if DEBUG then print(message) From 14bde1472fc095f00b68c41d5feef8cce45a7804 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Tue, 28 Sep 2021 14:52:54 -0600 Subject: [PATCH 015/105] Update lyrics.lua Small button issue --- lyrics.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyrics.lua b/lyrics.lua index a20da5d..35bc0f4 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1645,7 +1645,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "prop_refresh", "Refresh Directory", refresh_button_clicked) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_button_clicked) obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() obs.obs_properties_add_group(gp, "line", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) From 1d376f049c66902e2015a3fe0483e1364e662633 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Tue, 28 Sep 2021 15:31:06 -0600 Subject: [PATCH 016/105] Update lyrics.lua First lyric page transition was also fading title --- lyrics.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 35bc0f4..bda8986 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2149,8 +2149,7 @@ function load_song(source, preview) using_source = true load_source = source prepare_selected(song) - --transition_lyric_text() - --set_text_visiblity(TEXT_VISIBLE) + lyric_change = false -- transition is with scene for source loads if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end From 427189e9a088baa11824750de7b447f6985bda30 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sat, 2 Oct 2021 11:41:05 -0600 Subject: [PATCH 017/105] Update lyrics.lua Minor bug fixes --- lyrics.lua | 161 +++++++++++++++++++++++++++++------------------------ 1 file changed, 88 insertions(+), 73 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index bda8986..854e7c8 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1,4 +1,4 @@ ---- Copyright 2020 amirchev +---- Copyright 2020 amirchev -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -95,7 +95,7 @@ transition_enabled = false -- transitions are a work in progress to support transition_completed = false -- simple debugging/print mechanism -DEBUG = false -- on/off switch for entire debugging mechanism +DEBUG = true -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints DEBUG_CUSTOM = true -- print custom debugging messages @@ -238,50 +238,54 @@ function prev_prepared(pressed) if not pressed then return end - - if using_source then - using_source = false - prepare_selected(prepared_songs[prepared_index]) - return - end - if prepared_index > 1 then - using_source = false - prepare_selected(prepared_songs[prepared_index - 1]) - return - end - - if not source_active or using_source then - using_source = false - prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared - else - using_source = true - prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source - load_song(load_source, false) - end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) + return + end + if prepared_index > 1 then + using_source = false + prepare_selected(prepared_songs[prepared_index - 1]) + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared + else + using_source = true + prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source + load_song(load_source, false) + end end function next_prepared(pressed) if not pressed then return end - if using_source then - using_source = false - prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song - return - end - if prepared_index < #prepared_songs then - using_source = false - prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared - return - end - if not source_active or using_source then - using_source = false - prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available - else - using_source = true - prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source - load_song(load_source, false) - end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song + return + end + if prepared_index < #prepared_songs then + using_source = false + prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available + else + using_source = true + prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source + load_song(load_source, false) + end end function toggle_lyrics_visibility(pressed) @@ -572,7 +576,7 @@ function clear_prepared_clicked(props, p) end function prepare_selected(name) - dbg_method("prepare_selected: " .. name) + --dbg_method("prepare_selected: " .. name) if name == nil then return false end @@ -643,7 +647,7 @@ end -------- function apply_source_opacity() - dbg_method("apply_source_visiblity") +-- dbg_method("apply_source_visiblity") local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -657,7 +661,7 @@ function apply_source_opacity() obs.obs_source_update(alt_source, settings) end obs.obs_source_release(alt_source) - dbg_bool("lyric_change", lyric_change) + -- dbg_bool("lyric_change", lyric_change) if lyric_change then local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then @@ -740,7 +744,6 @@ function fade_callback() if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then timer_exists = false obs.remove_current_callback() - dbg_inner("ended fade timer") lyric_change = false end -- the amount we want to change opacity by @@ -874,8 +877,7 @@ function prepare_song_by_name(name) end local static_index = line:find("#S:") if static_index ~= nil then - local static_indexEnd = line:find("%s+", static_index + 1) - line = line:sub(static_indexEnd + 1) + line = line:sub(static_index+3) static_text = line new_lines = 0 end @@ -890,7 +892,7 @@ function prepare_song_by_name(name) if alt_index ~= nil then local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) - _, alt_indexEnd = line:find("%s+", alt_indexEnd + 1) + local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) line = line:sub(alt_indexEnd + 1) singleAlternate = true end @@ -943,8 +945,12 @@ function prepare_song_by_name(name) line = line:sub(1, refrain_index - 1) new_lines = 0 end + refrain_index = line:find("##R") - if refrain_index ~= nil then + if refrain_index == nil then + refrain_index = line:find("##r") + end + if refrain_index ~= nil then playRefrain = true line = line:sub(1, refrain_index - 1) new_lines = 0 @@ -958,6 +964,7 @@ function prepare_song_by_name(name) end newcount_index = line:find("#B:") if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) line = line:sub(1, newcount_index - 1) end local phantom_index = line:find("##P") @@ -967,7 +974,6 @@ function prepare_song_by_name(name) phantom_index = line:find("##B") if phantom_index ~= nil then line = line:gsub("%s*##B%s*", "") .. "\n" - -- line = line:sub(1, phantom_index - 1) end if line ~= nil then if use_static then @@ -1146,27 +1152,36 @@ function load_song_directory() songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) tags = readTags(songTitle) goodEntry = true - - if #keys>0 then - if keys[1] ~= "*" or #tags> 0 then - goodEntry = false - end - end - if (#tags > 0 and #keys > 0) then - goodEntry = false - for t = 1, #tags do + if #keys>0 then -- need to check files + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + goodEntry = false -- start assuming file will not be shown + if #tags == 0 then -- check no tagged option for k = 1, #keys do - if tags[t] == keys[k] then - goodEntry = true + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files break end end - if goodEntry then - break + else -- have keys and tags so compare them + for k = 1, #keys do + for t = 1, #tags do + if tags[t] == keys[k] then + goodEntry = true -- found match so show file + break + end + end + if goodEntry then -- stop outer key loop on match + break + end end end end - if goodEntry then + if goodEntry then -- add file if valid match if songExt == ".enc" then song_directory[#song_directory + 1] = dec(songTitle) else @@ -1176,7 +1191,6 @@ function load_song_directory() end until not entry obs.os_closedir(dir) - -- pause_timer = false end function readTags(name) @@ -1489,7 +1503,7 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) "
" text = text .. - "
" + "
" if using_source then text = text .. "From Source: " .. load_scene .. "
" else @@ -1500,7 +1514,7 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) end text = text .. - "
Lyric Page: " .. + "
Lyric Page: " .. page_index text = text .. " of " .. #lyrics .. "
" text = text .. "
" @@ -1643,12 +1657,12 @@ function script_properties() end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) - gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_button_clicked) - obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() - obs.obs_properties_add_group(gp, "line", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_button_clicked) + obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + obs.obs_properties_add_group(gp, "line", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) prepare_props = prep_prop for _, name in ipairs(prepared_songs) do @@ -2149,7 +2163,8 @@ function load_song(source, preview) using_source = true load_source = source prepare_selected(song) - lyric_change = false -- transition is with scene for source loads + --transition_lyric_text() + lyric_change = false if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end From 8af191e66e02f28114b294da6b606838de76c30e Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Tue, 5 Oct 2021 12:48:26 -0600 Subject: [PATCH 018/105] Update lyrics.lua removed the Unprepare button and added Edit List Button which allows for deleting and reordering. You could add a prepared song here but it would need to match the name exactly. I think I can add a test that every prepared song in the list is actually in the directory. Comments welcome on this one. --- lyrics.lua | 158 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 103 insertions(+), 55 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 854e7c8..718164c 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1,4 +1,4 @@ ----- Copyright 2020 amirchev +--- Copyright 2020 amirchev -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -94,8 +94,11 @@ expandcollapse = false transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false +editVisSet = false + + -- simple debugging/print mechanism -DEBUG = true -- on/off switch for entire debugging mechanism +DEBUG = false -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints DEBUG_CUSTOM = true -- print custom debugging messages @@ -527,37 +530,6 @@ function prepare_selection_made(props, prop, settings) return true end --- delete selected prepared song from list (user request) -function remove_selection_made(props, p) - dbg_method("unprepare_selection_made") - local name = obs.obs_data_get_string(script_sets, "prop_prepared_list") - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - local count = obs.obs_property_list_item_count(prop_prep_list) - for i = 0, count do - local song = obs.obs_property_list_item_string(prop_prep_list, i) - if song == name then - table.remove(prepared_songs, i + 1) - save_prepared() - prepared_songs = {} - load_prepared() - obs.obs_property_list_clear(prop_prep_list) - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prop_prep_list, name, name) - end - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) - prepared_index = 1 - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - prepared_index = 0 - end - end - end - obs.obs_properties_apply_settings(props, script_sets) - update_source_text() - return true -end - -- removes prepared songs function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") @@ -575,6 +547,9 @@ function clear_prepared_clicked(props, p) return true end + + + function prepare_selected(name) --dbg_method("prepare_selected: " .. name) if name == nil then @@ -1631,6 +1606,7 @@ end -- can change for the entire script module itself function script_properties() dbg_method("script_properties") + editVisSet = false script_props = obs.obs_properties_create() obs.obs_properties_add_button(script_props, "expand_all_button", "Expand/Collapse All Groups", expand_all_groups) ----------- @@ -1647,30 +1623,35 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------ - prep_prop = obs.obs_properties_add_bool(script_props, "prepared_showing", "Hide Manage Prepared Songs") + prep_prop = obs.obs_properties_add_bool(script_props, "prepared_showing", "Hide Prepared Songs") obs.obs_property_set_modified_callback(prep_prop, change_prepared_visible) gp = obs.obs_properties_create() - local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) - gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_button_clicked) - obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() - obs.obs_properties_add_group(gp, "line", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) - local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) - prepare_props = prep_prop - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prep_prop, name, name) - end - obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) - obs.obs_properties_add_button(gps, "prop_undo_button", "UnPrepare Selected Song", remove_selection_made) - obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) + local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) + gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_button_clicked) + obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) + prepare_props = prep_prop + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prep_prop, name, name) + end + obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared Songs List",edit_prepared_clicked) + eps = obs.obs_properties_create() + local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) + obs.obs_property_set_modified_callback(edit_prop, setEditVis) + obs.obs_properties_add_button(eps, "prop_save_button", "Done",save_edits_clicked) + obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) ------ options_prop = obs.obs_properties_add_bool(script_props, "options_showing", "Hide Display Options") @@ -1785,6 +1766,7 @@ function script_properties() end pp = obs.obs_properties_get(script_props,"ctrl_grp") obs.obs_property_set_visible(pp, true) + obs.obs_properties_apply_settings(script_props, script_sets) return script_props @@ -1814,7 +1796,71 @@ function expand_all_groups(props, prop, settings) obs.obs_property_set_visible(ctrlpp, expandcollapse) return true end +function setEditVis(props, prop, settings) + dbg_method("setEditVis") + if not editVisSet then + pp = obs.obs_properties_get(script_props,"edit_grp") + obs.obs_property_set_visible(pp, false) + editVisSet = true + end +end + +function edit_prepared_clicked(props, p) + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + local count = obs.obs_property_list_item_count(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + if count2 > 0 then + for i = 0, count2 do + obs.obs_data_array_erase(songNames,0) + end + end + for i = 0, count-1 do + local song = obs.obs_property_list_item_string(prop_prep_list, i) + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song) + obs.obs_data_array_push_back(songNames,array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(script_sets, "prep_list", songNames) + obs.obs_data_array_release(songNames) + pp = obs.obs_properties_get(script_props,"edit_grp") + obs.obs_property_set_visible(pp, true) + --obs.obs_properties_apply_settings(props, script_sets) + return true +end + +-- removes prepared songs +function save_edits_clicked(props, p) + prepared_songs = {} + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + print("count: " .. count2) + if count2 > 0 then + for i = 0, count2-1 do + local item = obs.obs_data_array_item(songNames, i); + local itemName = obs.obs_data_get_string(item, "value"); + prepared_songs[#prepared_songs+1] = itemName + obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) + print(itemName) + end + end + save_prepared() + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + prepared_index = 1 + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + prepared_index = 0 + end + pp = obs.obs_properties_get(script_props,"edit_grp") + obs.obs_property_set_visible(pp, false) + obs.obs_properties_apply_settings(props, script_sets) + return true +end function change_info_visible(props, prop, settings) local visible = obs.obs_data_get_bool(settings, "info_showing") local ctrlpp = obs.obs_properties_get(script_props,"info_grp") @@ -1912,6 +1958,7 @@ function script_update(settings) prepare_selected(prepared_songs[prepared_index]) end end + end -- A function named script_defaults will be called to set the default settings @@ -1926,6 +1973,7 @@ function script_defaults(settings) else os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') end + end -- A function named script_save will be called when the script is saved From 18ca2d89d68ec8fdb45f81ec44160b5dd17e742e Mon Sep 17 00:00:00 2001 From: amirchev Date: Wed, 6 Oct 2021 09:34:42 -0700 Subject: [PATCH 019/105] fixed prints to dbg_inner --- lyrics.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 718164c..624dcb6 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -98,7 +98,7 @@ editVisSet = false -- simple debugging/print mechanism -DEBUG = false -- on/off switch for entire debugging mechanism +DEBUG = true -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints DEBUG_CUSTOM = true -- print custom debugging messages @@ -515,7 +515,7 @@ function refresh_button_clicked(props, p) table.sort(song_directory) obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do - print(name) + dbg_inner(name) obs.obs_property_list_add_string(prop_dir_list, name, name) end obs.obs_properties_apply_settings(props, script_sets) @@ -1838,14 +1838,14 @@ function save_edits_clicked(props, p) obs.obs_property_list_clear(prop_prep_list) local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) - print("count: " .. count2) + dbg_inner("count: " .. count2) if count2 > 0 then for i = 0, count2-1 do local item = obs.obs_data_array_item(songNames, i); local itemName = obs.obs_data_get_string(item, "value"); prepared_songs[#prepared_songs+1] = itemName obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) - print(itemName) + dbg_inner(itemName) end end save_prepared() From c6f76f0083b4c7756a1dcb5c42b63c0b49d8cf5f Mon Sep 17 00:00:00 2001 From: amirchev Date: Wed, 6 Oct 2021 11:38:20 -0700 Subject: [PATCH 020/105] bug fixes --- lyrics.lua | 185 ++++++++++++++++++++++++++++------------------------- 1 file changed, 97 insertions(+), 88 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 624dcb6..136de3d 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -55,14 +55,14 @@ prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_s song_directory = {} prepared_songs = {} link_text = false -- true if Title and Static should fade with text only during hide/show -lyric_change = false -- Text and Static should only fade when lyrics are changing or during show/hide +all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) load_scene = "" -- name of scene loading a lyric with a source timer_exists = false -forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility +--forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility -- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID @@ -102,7 +102,7 @@ DEBUG = true -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints DEBUG_CUSTOM = true -- print custom debugging messages -DEBUG_BOOL = true -- print message with bool state true/false +DEBUG_BOOL = false -- print message with bool state true/false -------- ---------------- @@ -260,7 +260,7 @@ function prev_prepared(pressed) else using_source = true prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source - load_song(load_source, false) + load_source_song(load_source, false) end end @@ -287,7 +287,7 @@ function next_prepared(pressed) else using_source = true prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source - load_song(load_source, false) + load_source_song(load_source, false) end end @@ -296,13 +296,12 @@ function toggle_lyrics_visibility(pressed) if not pressed then return end - lyric_change = true -- This makes sure title and static change with text if selected if text_status ~= TEXT_HIDDEN then dbg_inner("hiding") - set_text_visiblity(TEXT_HIDDEN) + set_text_visibility(TEXT_HIDDEN) else dbg_inner("showing") - set_text_visiblity(TEXT_VISIBLE) + set_text_visibility(TEXT_VISIBLE) end end @@ -463,8 +462,8 @@ end function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") if #prepared_songs == 0 then - forceNoFade = true - set_text_visiblity(TEXT_HIDDEN) + --forceNoFade = true + set_text_visibility(TEXT_HIDDEN) end prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") @@ -511,7 +510,7 @@ function refresh_button_clicked(props, p) end end obs.source_list_release(sources) - load_song_directory() + load_source_song_directory() table.sort(song_directory) obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do @@ -533,39 +532,40 @@ end -- removes prepared songs function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") - prepared_songs = {} - page_index = 0 - prepared_index = 0 - update_source_text() + -- set_text_visibility(TEXT_HIDDEN) + -- prepared_songs = {} + -- page_index = 0 + -- prepared_index = 0 + -- update_source_text() -- clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) obs.obs_data_set_string(script_sets, "prop_prepared_list", "") obs.obs_properties_apply_settings(props, script_sets) save_prepared() - transition_lyric_text(false) return true end - - - function prepare_selected(name) - --dbg_method("prepare_selected: " .. name) - if name == nil then - return false - end - if name == "" then - return false - end - prepare_song_by_name(name) - page_index = 1 - if not using_source then - prepared_index = get_index_in_list(prepared_songs, name) - else - source_song_title = name - end - update_source_text() + dbg_method("prepare_selected") + -- try to prepare song + if prepare_song_by_name(name) then + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + else + source_song_title = name + end + all_sources_fade = true + -- if using source, then force show the new lyrics, even if lyrics were previously hidden + transition_lyric_text(using_source) + else + -- hide everything if unable to prepare song + -- TODO: clear lyrics entirely after text is hidden + set_text_visibility(TEXT_HIDDEN) + end + + --update_source_text() return true end @@ -636,8 +636,7 @@ function apply_source_opacity() obs.obs_source_update(alt_source, settings) end obs.obs_source_release(alt_source) - -- dbg_bool("lyric_change", lyric_change) - if lyric_change then + if all_sources_fade and link_text then local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then obs.obs_source_update(title_source, settings) @@ -652,15 +651,15 @@ function apply_source_opacity() obs.obs_data_release(settings) end -function set_text_visiblity(end_status) - dbg_method("set_text_visiblity") +function set_text_visibility(end_status) + dbg_method("set_text_visibility") -- if already at desired visibility, then exit if text_status == end_status then return end -- if fade is disabled, change visibility immediately - if not text_fade_enabled or forceNoFade then + if not text_fade_enabled then --or forceNoFade then if end_status == TEXT_HIDDEN then text_opacity = 0 elseif end_status == TEXT_VISIBLE then @@ -676,10 +675,11 @@ function set_text_visiblity(end_status) elseif end_status == TEXT_VISIBLE then text_status = TEXT_SHOWING end + all_sources_fade = true start_fade_timer() end update_source_text() - forceNoFade = false + --forceNoFade = false end -- transition to the next lyrics, use fade if enabled @@ -694,9 +694,15 @@ function transition_lyric_text(force_show) -- fade out transition is complete if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then update_source_text() + -- if text is done hiding, we can cancel the all_sources_fade + if text_status == TEXT_HIDDEN then + all_sources_fade = false + end dbg_inner("hidden") elseif not text_fade_enabled then - set_text_visiblity(TEXT_VISIBLE) + -- if text fade is not enabled, then we can cancel the all_sources_fade + all_sources_fade = false + set_text_visibility(TEXT_VISIBLE) update_source_text() dbg_inner("no text fade") else @@ -719,7 +725,7 @@ function fade_callback() if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then timer_exists = false obs.remove_current_callback() - lyric_change = false + all_sources_fade = false end -- the amount we want to change opacity by local opacity_delta = 1 + text_fade_speed @@ -732,7 +738,9 @@ function fade_callback() -- completed fade out, determine next move text_opacity = 0 if text_status == TEXT_TRANSITION_OUT then - update_source_text() --------------------------------------------UPDATE to NEW LYRIC BETWEEN FADES + -- update to new lyric between fades + update_source_text() + -- begin transition back in text_status = TEXT_TRANSITION_IN else text_status = TEXT_HIDDEN @@ -762,14 +770,17 @@ end -- prepares lyrics of the song function prepare_song_by_name(name) dbg_method("prepare_song_by_name") - lyric_change = true if name == nil then return false end -- if using transition on lyric change, first transition -- would be reset with new song prepared transition_completed = false + -- load song lines local song_lines = get_song_text(name) + if song_lines == nil then + return false + end local cur_line = 1 local cur_aline = 1 local recordRefrain = false @@ -1092,6 +1103,7 @@ end ------------------------ FILE FUNCTIONS ---------------- -------- + -- delete previewed song function delete_song(name) if testValid(name) then @@ -1101,11 +1113,11 @@ function delete_song(name) end os.remove(path) table.remove(song_directory, get_index_in_list(song_directory, name)) - load_song_directory() + load_source_song_directory() end -- loads the song directory -function load_song_directory() +function load_source_song_directory() local keytext = obs.obs_data_get_string(script_sets, "prop_edit_metatags") local keys = ParseCSVLine(keytext) song_directory = {} @@ -1335,19 +1347,6 @@ function save_prepared() return true end -function load_prepared() - dbg_method("load_prepared") - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") - if file ~= nil then - for line in file:lines() do - prepared_songs[#prepared_songs + 1] = line - end - prepared_index = 1 - file:close() - end - return true -end - -- updates the selected lyrics function update_source_text() dbg_method("update_source_text") @@ -1363,8 +1362,8 @@ function update_source_text() title = alt_title else if not using_source then - dbg_custom("Load title from prepared: " .. prepared_index) - if prepared_index ~= nil or prepared_index ~= 0 then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Load title from prepared: " .. prepared_index) title = prepared_songs[prepared_index] end else @@ -1591,7 +1590,9 @@ function get_song_text(name) song_lines[#song_lines + 1] = line end file:close() - end + else + return nil + end return song_lines end @@ -2053,15 +2054,23 @@ function script_load(settings) if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions - load_song_directory() - load_prepared() + load_source_song_directory() + -- load prepared songs from previous + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") + if file ~= nil then + for line in file:lines() do + prepared_songs[#prepared_songs + 1] = line + end + --prepared_index = 1 + file:close() + end local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) obs.obs_property_set_enabled(fade_speed_prop, not transition_set) transition_enabled = transition_set - obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture + -- obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end -------- @@ -2154,7 +2163,7 @@ source_def.update = function(data, settings) end source_def.get_properties = function(data) - load_song_directory() + load_source_song_directory() local props = obs.obs_properties_create() local source_dir_list = obs.obs_properties_add_list( @@ -2190,29 +2199,27 @@ end source_def.destroy = function(source) end -function updateSources() - obs.timer_remove(updateSources) - update_source_text() -end +-- function update_source_callback() + -- obs.remove_current_callback() + -- update_source_text() +-- end -function on_event(event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then - dbg_method("on_event") - obs.timer_add(updateSources, 100) -- delay updating source text until all sources have been removed by OBS - end -end +-- function on_event(event) + -- if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then + -- dbg_method("on_event") + -- obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS + -- end +-- end -function load_song(source, preview) - dbg_method("load_song") +function load_source_song(source, preview) + dbg_method("load_source_song") local settings = obs.obs_source_get_settings(source) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - dbg_inner("load_song: " .. song) using_source = true load_source = source prepare_selected(song) --transition_lyric_text() - lyric_change = false if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end @@ -2228,7 +2235,7 @@ function source_isactive(cd) end dbg_inner("source active") load_scene = get_current_scene_name() - load_song(source, false) + load_source_song(source, false) source_active = true -- using source lyric end @@ -2248,7 +2255,7 @@ function source_showing(cd) if source == nil then return end - load_song(source, true) + load_source_song(source, true) end function dbg(message) @@ -2259,29 +2266,31 @@ end function dbg_inner(message) if DEBUG_INNER then - dbg("INNER: " .. message) + dbg("INNR: " .. message) end end function dbg_method(message) if DEBUG_METHODS then - dbg("METHOD: " .. message) + dbg("-- MTHD: " .. message) end end function dbg_custom(message) if DEBUG_CUSTOM then - dbg("CUSTOM: " .. message) + dbg("CUST: " .. message) end end -function dbg_bool(message, value) +function dbg_bool(name, value) if DEBUG_BOOL then + local message = "BOOL: " .. name if value then - dbg("BOOL: " .. message .. " = true") + message = message .. " = true" else - dbg("BOOL: " .. message .. " = false") + message = message .. " = false" end + dbg(message) end end From 302b1830b864c3dac8c2a3d8d0bb3beff9a9308c Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 7 Oct 2021 02:06:23 -0600 Subject: [PATCH 021/105] Update lyrics.lua Mostly UI changes Manually entered songs in the Edit Prepared window are checked with the directory for validity. Check boxes went to buttons. Added Syntax Help to Button Text so it would also hide. Moved Update Monitor function to static variables. Seems to help. Monitor looks like it works now. I will keep testing. Too Late to keep thinking. :) --- lyrics.lua | 478 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 336 insertions(+), 142 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 136de3d..a8369aa 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -50,7 +50,7 @@ ensure_lines = true lyrics = {} -- refrain = {} alternate = {} -page_index = 1 +page_index = 0 prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected song_directory = {} prepared_songs = {} @@ -76,7 +76,18 @@ hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID -- script placeholders script_sets = nil script_props = nil -prepare_props = nil +source_sets = nil +source_props = nil + +--monitor variables +mon_song = "" +mon_lyric = "" +mon_nextlyric = "" +mon_alt = "" +mon_nextalt = "" +mon_nextsong = "" +meta_tags = "" + -- text status & fade TEXT_VISIBLE = 0 -- text is visible TEXT_HIDDEN = 1 -- text is hidden @@ -89,7 +100,8 @@ text_opacity = 100 text_fade_speed = 1 text_fade_enabled = false load_source = nil -expandcollapse = false +expandcollapse = true +showhelp = false transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false @@ -482,7 +494,6 @@ function refresh_button_clicked(props, p) local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") local static_source_prop = obs.obs_properties_get(props, "prop_static_list") local title_source_prop = obs.obs_properties_get(props, "prop_title_list") - local prop_dir_list = obs.obs_properties_get(props,"prop_directory_list") obs.obs_property_list_clear(source_prop) -- clear current properties list obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list obs.obs_property_list_clear(static_source_prop) -- clear current properties list @@ -509,17 +520,31 @@ function refresh_button_clicked(props, p) obs.obs_property_list_add_string(static_source_prop, name, name) end end + refresh_directory() + + return true +end + +function refresh_directory_button_clicked(props, p) +dbg_method("refresh directory") + refresh_directory() + return true +end + +function refresh_directory() + local prop_dir_list = obs.obs_properties_get(script_props,"prop_directory_list") + local source_prop = obs.obs_properties obs.source_list_release(sources) - load_source_song_directory() + source_filter = false + load_source_song_directory(true) table.sort(song_directory) obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do dbg_inner(name) obs.obs_property_list_add_string(prop_dir_list, name, name) end - obs.obs_properties_apply_settings(props, script_sets) - return true -end + obs.obs_properties_apply_settings(script_props, script_sets) +end function prepare_selection_made(props, prop, settings) dbg_method("prepare_selection_made") @@ -533,10 +558,10 @@ end function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") -- set_text_visibility(TEXT_HIDDEN) - -- prepared_songs = {} - -- page_index = 0 - -- prepared_index = 0 - -- update_source_text() + prepared_songs = {} -- required for monitor page + page_index = 0 -- required for monitor page + prepared_index = 0 -- required for monitor page + update_source_text() -- required for monitor page -- clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) @@ -1113,12 +1138,17 @@ function delete_song(name) end os.remove(path) table.remove(song_directory, get_index_in_list(song_directory, name)) - load_source_song_directory() + source_filter = false + load_source_song_directory(false) end -- loads the song directory -function load_source_song_directory() - local keytext = obs.obs_data_get_string(script_sets, "prop_edit_metatags") +function load_source_song_directory(use_filter) +dbg_method("load_source_song_directory") + local keytext = meta_tags + if source_filter then + keytext = obs.obs_data_get_string(source_sets, "prop_edit_metatags") + end local keys = ParseCSVLine(keytext) song_directory = {} local filenames = {} @@ -1139,7 +1169,7 @@ function load_source_song_directory() songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) tags = readTags(songTitle) goodEntry = true - if #keys>0 then -- need to check files + if use_filter and #keys>0 then -- need to check files for k = 1, #keys do if keys[k] == "*" then goodEntry = true -- okay to show untagged files @@ -1350,7 +1380,7 @@ end -- updates the selected lyrics function update_source_text() dbg_method("update_source_text") - + dbg_inner("Page Index: " .. page_index) local text = "" local alttext = "" local next_lyric = "" @@ -1449,17 +1479,18 @@ function update_source_text() end end - update_monitor( - title, - text:gsub("\n", "
• "), - next_lyric:gsub("\n", "
• "), - alttext:gsub("\n", "
• "), - next_alternate:gsub("\n", "
• "), - next_prepared - ) + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared + + update_monitor() end -function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) +function update_monitor() + dbg_method("update_monitor") local tableback = "black" local text = "" @@ -1508,44 +1539,44 @@ function update_monitor(song, lyric, nextlyric, alt, nextalt, nextsong) text .. "
" - if song ~= "" and song ~= nil then + if mon_song ~= "" and Mon_song ~= nil then text = text .. "" text = text .. - "" + "" end - if lyric ~= "" and lyric ~= nil then + if mon_lyric ~= "" and mon_lyric ~= nil then text = text .. "" - text = text .. "" + text = text .. "" end - if nextlyric ~= "" and nextlyric ~= nil then + if mon_nextlyric ~= "" and mon_nextlyric ~= nil then text = text .. "" - text = text .. "" + text = text .. "" end - if alt ~= "" and alt ~= nil then + if mon_alt ~= "" and mon_alt ~= nil then text = text .. "" text = - text .. "" + text .. "" end - if nextalt ~= "" and nextalt ~= nil then + if mon_nextalt ~= "" and mon_nextalt ~= nil then text = text .. "" - text = text .. "" + text = text .. "" end - if nextsong ~= "" and nextsong ~= nil then + if mon_nextsong ~= "" and mon_nextsong ~= nil then text = text .. "" - text = text .. "" + text = text .. "" end text = text .. "
Song
Title
" .. song .. "
" .. mon_song .. "
Current
Page
• " .. lyric .. "
• " .. mon_lyric .. "
Next
Page
• " .. nextlyric .. "
• " .. mon_nextlyric .. "
Alt
Lyric
• " .. alt .. "
• " .. mon_alt .. "
Next
Alt
• " .. nextalt .. "
• " .. mon_nextalt .. "
Next
Song:
" .. nextsong .. "
" .. mon_nextsong .. "
" local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") @@ -1605,14 +1636,29 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself + +local help = "-------------- MARKUP SYNTAX HELP --------------\n\n" .. + "Markup      Syntax        Markup       Syntax\n" .. + "=======    =======      =======     ======\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n\n" .. + "Titles must be valid filenames. Override Title with #T: title markup\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line\n\n" .. + "*** CLICK TO CLOSE ***" + function script_properties() dbg_method("script_properties") editVisSet = false script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "expand_all_button", "Expand/Collapse All Groups", expand_all_groups) + obs.obs_properties_add_button(script_props, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) + obs.obs_properties_add_button(script_props, "expand_all_button", "<--- HIDE ALL GROUPS", expand_all_groups) ----------- - info_prop = obs.obs_properties_add_bool(script_props, "info_showing", "Hide Song Information") - obs.obs_property_set_modified_callback(info_prop, change_info_visible) + obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) local lyric_prop = @@ -1624,8 +1670,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------ - prep_prop = obs.obs_properties_add_bool(script_props, "prepared_showing", "Hide Prepared Songs") - obs.obs_property_set_modified_callback(prep_prop, change_prepared_visible) + obs.obs_properties_add_button(script_props, "prepared_showing", "<--- HIDE PREPARED SONGS",change_prepared_visible) gp = obs.obs_properties_create() local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) @@ -1634,13 +1679,14 @@ function script_properties() end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Songs by Meta Tags", filter_songs_clicked) gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_button_clicked) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) - prepare_props = prep_prop +-- prepare_props = prep_prop for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prep_prop, name, name) end @@ -1650,14 +1696,12 @@ function script_properties() eps = obs.obs_properties_create() local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) obs.obs_property_set_modified_callback(edit_prop, setEditVis) - obs.obs_properties_add_button(eps, "prop_save_button", "Done",save_edits_clicked) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) - obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) ------ - options_prop = obs.obs_properties_add_bool(script_props, "options_showing", "Hide Display Options") - obs.obs_property_set_modified_callback(options_prop, change_options_visible) - + obs.obs_properties_add_button(script_props, "options_showing", "<--- HIDE DISPLAY OPTIONS",change_options_visible) gp = obs.obs_properties_create() local lines_prop = obs.obs_properties_add_int(gp, "prop_lines_counter", "Lines to Display", 1, 100, 1) obs.obs_property_set_long_description( @@ -1685,8 +1729,7 @@ function script_properties() obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) ------------- - src_prop = obs.obs_properties_add_bool(script_props, "src_showing", "Hide Source Selections") - obs.obs_property_set_modified_callback(src_prop, change_src_visible) + obs.obs_properties_add_button(script_props, "src_showing", "<--- HIDE SOURCE TEXT SELECTIONS",change_src_visible) gp = obs.obs_properties_create() local source_prop = obs.obs_properties_add_list( @@ -1749,9 +1792,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) ------------------ - ctrl_prop = obs.obs_properties_add_bool(script_props, "ctrl_showing", "Hide Lyric Controls") - obs.obs_property_set_modified_callback(ctrl_prop, change_ctrl_visible) - + obs.obs_properties_add_button(script_props, "ctrl_showing", "<--- HIDE LYRIC CONTROLS",change_ctrl_visible) gp = obs.obs_properties_create() obs.obs_properties_add_button(gp, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(gp, "prop_next_button", "Next Lyric", next_button_clicked) @@ -1775,40 +1816,210 @@ end -- A function named script_description returns the description shown to -- the user + +local description = [[ +"Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from taxilian.
+ +
+ + + + + + + +
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
Optional comma delimeted meta tags following //meta on 1st line
+]] + function script_description() - return "Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2) Author: Amirchev & DC Strato; with significant contributions from taxilian.
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
" -end + return "Manage song Lyrics and Other Paged Text (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from Taxilian.
" + end + function expand_all_groups(props, prop, settings) expandcollapse = not expandcollapse - obs.obs_data_set_bool(script_sets, "info_showing", not expandcollapse) - local ctrlpp = obs.obs_properties_get(script_props,"info_grp") - obs.obs_property_set_visible(ctrlpp, expandcollapse) - obs.obs_data_set_bool(script_sets, "prepared_showing", not expandcollapse) - local ctrlpp = obs.obs_properties_get(script_props,"prep_grp") - obs.obs_property_set_visible(ctrlpp, expandcollapse) - obs.obs_data_set_bool(script_sets, "options_showing", not expandcollapse) - local ctrlpp = obs.obs_properties_get(script_props,"disp_grp") - obs.obs_property_set_visible(ctrlpp, expandcollapse) - obs.obs_data_set_bool(script_sets, "src_showing", not expandcollapse) - local ctrlpp = obs.obs_properties_get(script_props,"src_grp") - obs.obs_property_set_visible(ctrlpp, expandcollapse) - obs.obs_data_set_bool(script_sets, "ctrl_showing", not expandcollapse) - local ctrlpp = obs.obs_properties_get(script_props,"ctrl_grp") - obs.obs_property_set_visible(ctrlpp, expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"info_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"prep_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"disp_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"src_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"ctrl_grp"), expandcollapse) + local mode1 = "SHOW " + local mode2 = " --->" + if expandcollapse then + mode1 = "<--- HIDE " + mode2 = "" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) return true end -function setEditVis(props, prop, settings) + + +function all_vis_equal() + return (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) or not + (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) +end + +function change_info_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"info_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) + local mode1 = "SHOW " + local mode2 = " --->" + if vis then + mode1 = "<--- HIDE " + mode2 = "" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_prepared_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"prep_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) + local mode1 = "SHOW " + local mode2 = " --->" + if vis then + mode1 = "<--- HIDE " + mode2 = "" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_options_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"disp_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) + local mode1 = "SHOW " + local mode2 = " --->" + if vis then + mode1 = "<--- HIDE " + mode2 = "" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_src_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"src_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) + local mode1 = "SHOW " + local mode2 = " --->" + if vis then + mode1 = "<--- HIDE " + mode2 = "" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_ctrl_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"ctrl_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) + local mode1 = "SHOW " + local mode2 = " --->" + if vis then + mode1 = "<--- HIDE " + mode2 = "" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_fade_property(props, prop, settings) + local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") + local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") + obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) + return true +end + +function show_help_button(props, prop, settings) +dbg_method("show help") + local hb = obs.obs_properties_get(props, "show_help_button") + showhelp = not showhelp + if showhelp then + obs.obs_property_set_description(hb, help) + else + obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") + end + return true +end + +function setEditVis(props, prop, settings) -- hides edit group on initial showing dbg_method("setEditVis") if not editVisSet then - pp = obs.obs_properties_get(script_props,"edit_grp") + local pp = obs.obs_properties_get(script_props,"edit_grp") obs.obs_property_set_visible(pp, false) - editVisSet = true + pp = obs.obs_properties_get(props,"meta") + obs.obs_property_set_visible(pp, false) + editVisSet = true -- do this only once end end +function filter_songs_clicked(props, p) + local pp = obs.obs_properties_get(props,"meta") + if not obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "filter_songs_button") + obs.obs_property_set_description(mpb, "Clear Song Filters") -- change button function + meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + refresh_directory() + else + obs.obs_property_set_visible(pp, false) + meta_tags = "" -- clear meta tags + refresh_directory() + local mpb = obs.obs_properties_get(props, "filter_songs_button") -- + obs.obs_property_set_description(mpb, "Filter Songs by Meta Tags") -- reset button function + end + return true +end + function edit_prepared_clicked(props, p) + local pp = obs.obs_properties_get(props,"edit_grp") + if obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + return true + end local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") local count = obs.obs_property_list_item_count(prop_prep_list) + dbg_inner("count: " .. count) local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) if count2 > 0 then @@ -1826,27 +2037,28 @@ function edit_prepared_clicked(props, p) end obs.obs_data_set_array(script_sets, "prep_list", songNames) obs.obs_data_array_release(songNames) - pp = obs.obs_properties_get(script_props,"edit_grp") obs.obs_property_set_visible(pp, true) - --obs.obs_properties_apply_settings(props, script_sets) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Cancel Prepared Song Edits") return true end -- removes prepared songs function save_edits_clicked(props, p) + load_source_song_directory(false) prepared_songs = {} local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prop_prep_list) local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) - dbg_inner("count: " .. count2) if count2 > 0 then for i = 0, count2-1 do local item = obs.obs_data_array_item(songNames, i); local itemName = obs.obs_data_get_string(item, "value"); - prepared_songs[#prepared_songs+1] = itemName - obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) - dbg_inner(itemName) + if get_index_in_list(song_directory, itemName) ~= nil then + prepared_songs[#prepared_songs+1] = itemName + obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) + end end end save_prepared() @@ -1859,50 +2071,11 @@ function save_edits_clicked(props, p) end pp = obs.obs_properties_get(script_props,"edit_grp") obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") obs.obs_properties_apply_settings(props, script_sets) return true end -function change_info_visible(props, prop, settings) - local visible = obs.obs_data_get_bool(settings, "info_showing") - local ctrlpp = obs.obs_properties_get(script_props,"info_grp") - obs.obs_property_set_visible(ctrlpp, not visible) - return true -end - -function change_prepared_visible(props, prop, settings) - local visible = obs.obs_data_get_bool(settings, "prepared_showing") - local ctrlpp = obs.obs_properties_get(script_props,"prep_grp") - obs.obs_property_set_visible(ctrlpp, not visible) - return true -end - -function change_options_visible(props, prop, settings) - local visible = obs.obs_data_get_bool(settings, "options_showing") - local ctrlpp = obs.obs_properties_get(script_props,"disp_grp") - obs.obs_property_set_visible(ctrlpp, not visible) - return true -end - -function change_src_visible(props, prop, settings) - local visible = obs.obs_data_get_bool(settings, "src_showing") - local ctrlpp = obs.obs_properties_get(script_props,"src_grp") - obs.obs_property_set_visible(ctrlpp, not visible) - return true -end - -function change_ctrl_visible(props, prop, settings) - local visible = obs.obs_data_get_bool(settings, "ctrl_showing") - local ctrlpp = obs.obs_properties_get(script_props,"ctrl_grp") - obs.obs_property_set_visible(ctrlpp, not visible) - return true -end - -function change_fade_property(props, prop, settings) - local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") - local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") - obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) - return true -end function change_transition_property(props, prop, settings) local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") @@ -2054,7 +2227,7 @@ function script_load(settings) if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions - load_source_song_directory() + load_source_song_directory(false) -- load prepared songs from previous local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") if file ~= nil then @@ -2070,7 +2243,7 @@ function script_load(settings) obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) obs.obs_property_set_enabled(fade_speed_prop, not transition_set) transition_enabled = transition_set - -- obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end -------- @@ -2158,16 +2331,31 @@ source_def.get_name = function() return "Prepare Lyric" end -source_def.update = function(data, settings) +source_def.save = function(data, settings) rename_source() -- Rename and Mark sources instantly on update (WZ) end + +function source_refresh_button_clicked(props, p) + dbg_method("source_refresh_button") + source_filter = true + load_source_song_directory(true) + table.sort(song_directory) + local prop_dir_list = obs.obs_properties_get(props,"source_directory_list") + obs.obs_property_list_clear(prop_dir_list) -- clear directories + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + return true +end + source_def.get_properties = function(data) - load_source_song_directory() - local props = obs.obs_properties_create() + source_filter = true + load_source_song_directory(true) + local source_props = obs.obs_properties_create() local source_dir_list = obs.obs_properties_add_list( - props, + source_props, "songs", "Song Directory", obs.OBS_COMBO_TYPE_LIST, @@ -2177,9 +2365,15 @@ source_def.get_properties = function(data) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(source_dir_list, name, name) end - obs.obs_properties_add_bool(props, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode - obs.obs_properties_add_bool(props, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode - return props + gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "source_prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) + obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode + obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode + obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) + return source_props end source_def.create = function(settings, source) @@ -2199,17 +2393,17 @@ end source_def.destroy = function(source) end --- function update_source_callback() - -- obs.remove_current_callback() - -- update_source_text() --- end +function update_source_callback() + obs.remove_current_callback() + update_monitor() +end --- function on_event(event) - -- if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then - -- dbg_method("on_event") - -- obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS - -- end --- end +function on_event(event) + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then + dbg_method("on_event") + obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS + end +end function load_source_song(source, preview) dbg_method("load_source_song") @@ -2219,7 +2413,7 @@ function load_source_song(source, preview) using_source = true load_source = source prepare_selected(song) - --transition_lyric_text() + set_text_visibility(TEXT_VISIBLE) if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end From 1f9b3e82cc7356099cdcb34bcf4637ca750c0378 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 7 Oct 2021 10:31:05 -0600 Subject: [PATCH 022/105] Update lyrics.lua Found the bug with transitioning in sources. I updated set_text_visibility to always honor Hidden and Visible instantly. Test for text_fade_enabled seems irrelevant now as logic is driven by end_status request. This allows text to be set as Hidden prior to preparation in Load_source_song() and text just fades in, not out. --- lyrics.lua | 56 +++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index a8369aa..6ca13a3 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -113,8 +113,8 @@ editVisSet = false DEBUG = true -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints -DEBUG_CUSTOM = true -- print custom debugging messages -DEBUG_BOOL = false -- print message with bool state true/false +DEBUG_CUSTOM = false -- print custom debugging messages +DEBUG_BOOL = true -- print message with bool state true/false -------- ---------------- @@ -474,7 +474,6 @@ end function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") if #prepared_songs == 0 then - --forceNoFade = true set_text_visibility(TEXT_HIDDEN) end prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") @@ -557,7 +556,6 @@ end -- removes prepared songs function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") - -- set_text_visibility(TEXT_HIDDEN) prepared_songs = {} -- required for monitor page page_index = 0 -- required for monitor page prepared_index = 0 -- required for monitor page @@ -682,18 +680,20 @@ function set_text_visibility(end_status) if text_status == end_status then return end - -- if fade is disabled, change visibility immediately - - if not text_fade_enabled then --or forceNoFade then - if end_status == TEXT_HIDDEN then - text_opacity = 0 - elseif end_status == TEXT_VISIBLE then - text_opacity = 100 - end - text_status = end_status - apply_source_opacity() - dbg_inner("immediate visibility change") - else + -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + end + if text_status == end_status then + apply_source_opacity() + update_source_text() + return + end + --if text_fade_enabled then -- if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then text_status = TEXT_HIDING @@ -702,9 +702,8 @@ function set_text_visibility(end_status) end all_sources_fade = true start_fade_timer() - end + --end update_source_text() - --forceNoFade = false end -- transition to the next lyrics, use fade if enabled @@ -1661,9 +1660,7 @@ function script_properties() obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) - local lyric_prop = - obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) - obs.obs_property_set_long_description(lyric_prop, "Lyric Text with Markup") + obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) obs.obs_properties_add_button(gp, "prop_opensong_button","Edit Song with System Editor", open_song_clicked) @@ -2378,6 +2375,7 @@ end source_def.create = function(settings, source) data = {} + source_sets = settings sh = obs.obs_source_get_signal_handler(source) obs.signal_handler_connect(sh, "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(sh, "show", source_showing) -- Set Preview Callback @@ -2399,10 +2397,11 @@ function update_source_callback() end function on_event(event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then - dbg_method("on_event") - obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS - end + dbg_method("on_event") + if event == obs.OBS_FRONTEND_EVENT_TRANSITION_STOPPED then + dbg_bool("Active:",source_active) + obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS + end end function load_source_song(source, preview) @@ -2412,8 +2411,9 @@ function load_source_song(source, preview) local song = obs.obs_data_get_string(settings, "songs") using_source = true load_source = source + set_text_visibility(TEXT_HIDDEN) prepare_selected(song) - set_text_visibility(TEXT_VISIBLE) + transition_lyric_text() if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end @@ -2432,14 +2432,14 @@ function source_isactive(cd) load_source_song(source, false) source_active = true -- using source lyric -end + end function source_inactive(cd) + dbg_inner("source inactive") local source = obs.calldata_source(cd, "source") if source == nil then return end - dbg_inner("source inactive") source_active = false -- indicates source loading lyric is active (but using prepared lyrics is still possible) end From 1bf71036469cac09979a5a86ec98ddd51652955d Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 7 Oct 2021 15:49:57 -0600 Subject: [PATCH 023/105] Update lyrics.lua Just UI tinkering --- lyrics.lua | 64 +++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 6ca13a3..c5daebd 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1636,9 +1636,9 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself -local help = "-------------- MARKUP SYNTAX HELP --------------\n\n" .. +local help = "░░░░░░░░░░░░░░░ MARKUP SYNTAX HELP ░░░░░░░░░░░░░░░\n\n" .. "Markup      Syntax        Markup       Syntax\n" .. - "=======    =======      =======     ======\n" .. + "==========  ==========    ==========  ==========\n" .. " Display n Lines    #L:n      End Page after Line   Line ###\n" .. " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. @@ -1648,14 +1648,14 @@ local help = "-------------- MARKUP SYNTAX HELP --------------\n\n" .. "Comment Line    // Line       Block Comments    //[ and //] \n\n" .. "Titles must be valid filenames. Override Title with #T: title markup\n\n" .. "Optional comma delimited meta tags follow '//meta ' on 1st line\n\n" .. - "*** CLICK TO CLOSE ***" + "▲░░░░░░ CLICK TO CLOSE ░░░░░░▲" function script_properties() dbg_method("script_properties") editVisSet = false script_props = obs.obs_properties_create() obs.obs_properties_add_button(script_props, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) - obs.obs_properties_add_button(script_props, "expand_all_button", "<--- HIDE ALL GROUPS", expand_all_groups) + obs.obs_properties_add_button(script_props, "expand_all_button", "▲░ HIDE ALL GROUPS ░▲", expand_all_groups) ----------- obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) gp = obs.obs_properties_create() @@ -1667,7 +1667,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------ - obs.obs_properties_add_button(script_props, "prepared_showing", "<--- HIDE PREPARED SONGS",change_prepared_visible) + obs.obs_properties_add_button(script_props, "prepared_showing", "▲░ HIDE PREPARED SONGS ░▲",change_prepared_visible) gp = obs.obs_properties_create() local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) @@ -1698,7 +1698,7 @@ function script_properties() obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) ------ - obs.obs_properties_add_button(script_props, "options_showing", "<--- HIDE DISPLAY OPTIONS",change_options_visible) + obs.obs_properties_add_button(script_props, "options_showing", "▲░ HIDE DISPLAY OPTIONS ░▲",change_options_visible) gp = obs.obs_properties_create() local lines_prop = obs.obs_properties_add_int(gp, "prop_lines_counter", "Lines to Display", 1, 100, 1) obs.obs_property_set_long_description( @@ -1726,7 +1726,7 @@ function script_properties() obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) ------------- - obs.obs_properties_add_button(script_props, "src_showing", "<--- HIDE SOURCE TEXT SELECTIONS",change_src_visible) + obs.obs_properties_add_button(script_props, "src_showing", "▲░ HIDE SOURCE TEXT SELECTIONS ░▲",change_src_visible) gp = obs.obs_properties_create() local source_prop = obs.obs_properties_add_list( @@ -1789,7 +1789,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) ------------------ - obs.obs_properties_add_button(script_props, "ctrl_showing", "<--- HIDE LYRIC CONTROLS",change_ctrl_visible) + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) gp = obs.obs_properties_create() obs.obs_properties_add_button(gp, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(gp, "prop_next_button", "Next Lyric", next_button_clicked) @@ -1839,11 +1839,11 @@ function expand_all_groups(props, prop, settings) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"disp_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"src_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"ctrl_grp"), expandcollapse) - local mode1 = "SHOW " - local mode2 = " --->" + local mode1 = "▼░ SHOW " + local mode2 = "░▼" if expandcollapse then - mode1 = "<--- HIDE " - mode2 = "" + mode1 = "▲░ HIDE " + mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) @@ -1872,11 +1872,11 @@ function change_info_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"info_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) - local mode1 = "SHOW " - local mode2 = " --->" + local mode1 = "▼░ SHOW " + local mode2 = "░▼" if vis then - mode1 = "<--- HIDE " - mode2 = "" + mode1 = "▲░ HIDE " + mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) if all_vis_equal() then @@ -1890,11 +1890,11 @@ function change_prepared_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"prep_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) - local mode1 = "SHOW " - local mode2 = " --->" +local mode1 = "▼░ SHOW " + local mode2 = "░▼" if vis then - mode1 = "<--- HIDE " - mode2 = "" + mode1 = "▲░ HIDE " + mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) if all_vis_equal() then @@ -1908,11 +1908,11 @@ function change_options_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"disp_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) - local mode1 = "SHOW " - local mode2 = " --->" +local mode1 = "▼░ SHOW " + local mode2 = "░▼" if vis then - mode1 = "<--- HIDE " - mode2 = "" + mode1 = "▲░ HIDE " + mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) if all_vis_equal() then @@ -1926,11 +1926,11 @@ function change_src_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"src_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) - local mode1 = "SHOW " - local mode2 = " --->" +local mode1 = "▼░ SHOW " + local mode2 = "░▼" if vis then - mode1 = "<--- HIDE " - mode2 = "" + mode1 = "▲░ HIDE " + mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) if all_vis_equal() then @@ -1944,11 +1944,11 @@ function change_ctrl_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"ctrl_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) - local mode1 = "SHOW " - local mode2 = " --->" +local mode1 = "▼░ SHOW " + local mode2 = "░▼" if vis then - mode1 = "<--- HIDE " - mode2 = "" + mode1 = "▲░ HIDE " + mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) if all_vis_equal() then From f7ddb66c05553aa436cc3104cfdf08c05115779a Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 7 Oct 2021 20:27:55 -0600 Subject: [PATCH 024/105] Update lyrics.lua Added Verse Markup ##V at the beginning of a logical Verse Verse x of n shows in Monitor if used. Eventually we can attach verse limits to individual songs. Properties would need to build a text property array to hold CSV verse selections for each prepared lyric. (Easy with source loads) --- lyrics.lua | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index c5daebd..af02f5f 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -48,6 +48,8 @@ ensure_lines = true -- TODO: removed displayed_song and use prepared_songs[prepared_index] -- displayed_song = "" lyrics = {} +verses = {} + -- refrain = {} alternate = {} page_index = 0 @@ -82,6 +84,7 @@ source_props = nil --monitor variables mon_song = "" mon_lyric = "" +mon_verse = "" mon_nextlyric = "" mon_alt = "" mon_nextalt = "" @@ -817,6 +820,7 @@ function prepare_song_by_name(name) local refrain = {} local arefrain = {} lyrics = {} + verses = {} alternate = {} static_text = "" alt_title = "" @@ -985,6 +989,13 @@ function prepare_song_by_name(name) if phantom_index ~= nil then line = line:gsub("%s*##B%s*", "") .. "\n" end + local verse_index = line:find("##V") + if verse_index ~= nil then + line = line:sub(1, verse_index - 1) + new_lines = 0 + verses[#verses+1] = #lyrics + dbg_inner("Verse: " .. #lyrics) + end if line ~= nil then if use_static then if static_text == "" then @@ -1376,6 +1387,7 @@ function save_prepared() return true end + -- updates the selected lyrics function update_source_text() dbg_method("update_source_text") @@ -1387,6 +1399,8 @@ function update_source_text() local static = static_text local mstatic = static -- save static for use with monitor local title = "" + + if alt_title ~= "" then title = alt_title else @@ -1406,7 +1420,7 @@ function update_source_text() local alt_source = obs.obs_get_source_by_name(alternate_source_name) local stat_source = obs.obs_get_source_by_name(static_source_name) local title_source = obs.obs_get_source_by_name(title_source_name) - + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then if #lyrics > 0 then if lyrics[page_index] ~= nil then @@ -1477,7 +1491,14 @@ function update_source_text() next_prepared = prepared_songs[1] -- plan to loop around to first prepared song end end - + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i]+1 then + mon_verse = i + end + end -- v = current verse number for this page + end mon_song = title mon_lyric = text:gsub("\n", "
• ") mon_nextlyric = next_lyric:gsub("\n", "
• ") @@ -1516,11 +1537,12 @@ function update_monitor() text .. "
of " .. #prepared_songs .. "
" end - text = - text .. - "
Lyric Page: " .. - page_index + text = text .. "
Lyric Page: " .. page_index text = text .. " of " .. #lyrics .. "
" + if #verses ~= nil and mon_verse>0 then + text = text .. "
Verse: " .. mon_verse + text = text .. " of " .. #verses .. "
" + end text = text .. "
" if not anythingActive() then tableback = "#440000" @@ -1645,8 +1667,8 @@ local help = "░░░░░░░░░░░░░░░ MARKUP SYNTAX HELP " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. - "Comment Line    // Line       Block Comments    //[ and //] \n\n" .. - "Titles must be valid filenames. Override Title with #T: title markup\n\n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. "Optional comma delimited meta tags follow '//meta ' on 1st line\n\n" .. "▲░░░░░░ CLICK TO CLOSE ░░░░░░▲" From a9726e5020b232d7176e8f329cb231ab1f1519f1 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Fri, 8 Oct 2021 23:43:21 -0600 Subject: [PATCH 025/105] Update lyrics.lua A few updates while preparing for Sunday's service. --- lyrics.lua | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index af02f5f..e1cdc62 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1538,10 +1538,10 @@ function update_monitor() " of " .. #prepared_songs .. "
" end text = text .. "
Lyric Page: " .. page_index - text = text .. " of " .. #lyrics .. "
" + text = text .. " of " .. #lyrics .. "
" if #verses ~= nil and mon_verse>0 then text = text .. "
Verse: " .. mon_verse - text = text .. " of " .. #verses .. "
" + text = text .. " of " .. #verses .. "
" end text = text .. "
" if not anythingActive() then @@ -2350,10 +2350,24 @@ source_def.get_name = function() return "Prepare Lyric" end +saved = false + source_def.save = function(data, settings) + if saved then return end -- obs calls save for every load source in all scenes and we only need it once (So we could probably do rename here eventually) + saved = true + using_source = true + prepare_selected(obs.obs_data_get_string(source_sets, "songs")) -- show song to user rename_source() -- Rename and Mark sources instantly on update (WZ) end +source_def.update = function(data, settings) + saved = false -- mark properties changed + source_sets = settings -- saved so the actual SAVE callback can update the right song +end + +source_def.load = function(data) +dbg_method("load") +end function source_refresh_button_clicked(props, p) dbg_method("source_refresh_button") @@ -2396,12 +2410,13 @@ source_def.get_properties = function(data) end source_def.create = function(settings, source) +dbg_method("create") data = {} source_sets = settings - sh = obs.obs_source_get_signal_handler(source) - obs.signal_handler_connect(sh, "activate", source_isactive) -- Set Active Callback - obs.signal_handler_connect(sh, "show", source_showing) -- Set Preview Callback - obs.signal_handler_connect(sh, "deactivate", source_inactive) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback return data end @@ -2410,17 +2425,14 @@ source_def.get_defaults = function(settings) obs.obs_data_set_default_string(settings, "index", "0") end -source_def.destroy = function(source) -end - function update_source_callback() obs.remove_current_callback() update_monitor() end function on_event(event) - dbg_method("on_event") - if event == obs.OBS_FRONTEND_EVENT_TRANSITION_STOPPED then + dbg_method("on_event: " .. event) + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then dbg_bool("Active:",source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end @@ -2443,6 +2455,7 @@ function load_source_song(source, preview) obs.obs_data_release(settings) end + function source_isactive(cd) dbg_method("source_active") local source = obs.calldata_source(cd, "source") @@ -2452,9 +2465,8 @@ function source_isactive(cd) dbg_inner("source active") load_scene = get_current_scene_name() load_source_song(source, false) - source_active = true -- using source lyric - end +end function source_inactive(cd) dbg_inner("source inactive") From 300d9fc41036e23d0b23c75b1710af0e2a5aad2e Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sat, 9 Oct 2021 03:17:07 -0600 Subject: [PATCH 026/105] Update lyrics.lua Added rename callback to catch duplicate sources and change their color. OBS duplicates sources by default so it is easy to get the same load in different scenes and change one not realizing you are changing the twin in another scene. --- lyrics.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lyrics.lua b/lyrics.lua index e1cdc62..e92182f 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2430,12 +2430,21 @@ function update_source_callback() update_monitor() end +function rename_callback() + obs.remove_current_callback() + rename_source() +end + function on_event(event) dbg_method("on_event: " .. event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then dbg_bool("Active:",source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end + if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then + dbg_inner("Scene Change") + obs.timer_add(rename_callback, 1000) + end end function load_source_song(source, preview) From 2225aa23c7efc20163f5d0f76c910c6fe4abc73f Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sat, 9 Oct 2021 12:04:09 -0600 Subject: [PATCH 027/105] Update lyrics.lua WooHoo I found a way to use the save iteration for renaming! Much more efficient than the old way. I also removes the get_ID call which some documents have said to NOT USE because of instability it causes in OBS. Create adds a new isLLC true bool data item to settings in each source that can be checked to see when a source is ours. --- lyrics.lua | 146 +++++++++++++++++++++++++++-------------------------- 1 file changed, 74 insertions(+), 72 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index e92182f..308edf6 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2272,78 +2272,65 @@ end -------- -- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) -function rename_source() - -- pause_timer = true +function index_sources() local sources = obs.obs_enum_sources() if (sources ~= nil) then -- count and index sources local t = 1 for _, source in ipairs(sources) do - local source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "Prepare_Lyrics" then - local settings = obs.obs_source_get_settings(source) + local settings = obs.obs_source_get_settings(source) + if obs.obs_data_get_bool(settings,"isLLC") then obs.obs_data_set_string(settings, "index", t) -- add index to source data t = t + 1 - obs.obs_data_release(settings) -- release memory - end - end - -- Find and mark Duplicates in loadLyric_items table - local loadLyric_items = {} -- Start Table for all load Sources - local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items - if scenes ~= nil then - for _, scenesource in ipairs(scenes) do -- Loop through all scenes - local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer - local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id - if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) - if loadLyric_items[index] == nil then - loadLyric_items[index] = "x" -- First time to find this source so mark with x - else - loadLyric_items[index] = "*" -- Found this source again so mark with * - end - obs.obs_data_release(settings) -- release memory - end - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - end - obs.source_list_release(scenes) -- Free source list + end + obs.obs_data_release(settings) -- release memory end + end + local loadLyric_items = {} -- Start Table for all load Sources + local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items + if scenes ~= nil then + for _, scenesource in ipairs(scenes) do -- Loop through all scenes + local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer + local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + if obs.obs_data_get_bool(settings,"isLLC") then + local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) + obs.obs_data_set_bool(settings,"duplicate", loadLyric_items[index]) -- false (nil) the first time + loadLyric_items[index] = true -- duplicate (true) if used again + end + obs.obs_data_release(settings) -- release memory + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + end + obs.source_list_release(scenes) -- Free source list + end + obs.source_list_release(sources) +end - -- Name Source with Song Title - local i = 1 +function rename_sources() + local sources = obs.obs_enum_sources() + if (sources ~= nil) then + -- count and index sources + local t = 1 for _, source in ipairs(sources) do - local source_id = obs.obs_source_get_unversioned_id(source) -- Get source - if source_id == "Prepare_Lyrics" then -- Skip if not a Load Lyric source - local c_name = obs.obs_source_get_name(source) -- Get current Source Name - local settings = obs.obs_source_get_settings(source) -- Get settings for this source - local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load - local index = obs.obs_data_get_string(settings, "index") -- get index - if (song ~= nil) then - local name = t - i .. ". Load lyrics for: " .. song .. "" -- use index for compare - -- Mark Duplicates - if index ~= nil then - if loadLyric_items[index] == "*" then - name = '' .. name .. " * " - end - if (c_name ~= name) then - obs.obs_source_set_name(source, name) - end - end - i = i + 1 - end - obs.obs_data_release(settings) - end + local settings = obs.obs_source_get_settings(source) + if obs.obs_data_get_bool(settings,"isLLC") then + local name = obs.obs_data_get_string(settings, "index") .. ". Load lyrics for: " .. + obs.obs_data_get_string(settings, "songs") .. "" -- use index for compare + if obs.obs_data_get_bool(settings, "duplicate") then + name = '' .. name .. " * " + end + obs.obs_source_set_name(source, name) + end + obs.obs_data_release(settings) -- release memory end + obs.source_list_release(sources) end - obs.source_list_release(sources) - -- pause_timer = false end source_def.get_name = function() @@ -2353,11 +2340,7 @@ end saved = false source_def.save = function(data, settings) - if saved then return end -- obs calls save for every load source in all scenes and we only need it once (So we could probably do rename here eventually) - saved = true - using_source = true - prepare_selected(obs.obs_data_get_string(source_sets, "songs")) -- show song to user - rename_source() -- Rename and Mark sources instantly on update (WZ) + end source_def.update = function(data, settings) @@ -2365,10 +2348,6 @@ source_def.update = function(data, settings) source_sets = settings -- saved so the actual SAVE callback can update the right song end -source_def.load = function(data) -dbg_method("load") -end - function source_refresh_button_clicked(props, p) dbg_method("source_refresh_button") source_filter = true @@ -2386,6 +2365,7 @@ source_def.get_properties = function(data) source_filter = true load_source_song_directory(true) local source_props = obs.obs_properties_create() + index_sources() local source_dir_list = obs.obs_properties_add_list( source_props, @@ -2413,10 +2393,13 @@ source_def.create = function(settings, source) dbg_method("create") data = {} source_sets = settings + obs.obs_data_set_string(settings,"index",nil) + obs.obs_data_set_bool(settings,"duplicate",false) + obs.obs_data_set_bool(settings,"isLLC",true) obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "save", source_save) -- Set Preview Callback return data end @@ -2432,7 +2415,7 @@ end function rename_callback() obs.remove_current_callback() - rename_source() + rename_sources() end function on_event(event) @@ -2442,7 +2425,7 @@ function on_event(event) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then - dbg_inner("Scene Change") + dbg_inner("Scene Change") obs.timer_add(rename_callback, 1000) end end @@ -2477,6 +2460,25 @@ function source_isactive(cd) source_active = true -- using source lyric end +function source_save(cd) + dbg_inner("source save") + local source = obs.calldata_source(cd, "source") + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + dbg_inner("Index: " .. obs.obs_data_get_string(settings,"index")) + dbg_bool("Duplicate: ", obs.obs_data_get_bool(settings,"duplicate")) + --using_source = true + --prepare_selected(obs.obs_data_get_string(source_sets, "songs")) -- show song to user + local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load + local index = obs.obs_data_get_string(settings, "index") -- get index + local duplicate = obs.obs_data_get_bool(settings,"duplicate") + obs.obs_data_release(settings) -- release memory + local name = index .. ". Load lyrics for: " .. song .. "" -- use index for compare + if duplicate then + name = '' .. name .. " * " + end + obs.obs_source_set_name(source, name) +end + function source_inactive(cd) dbg_inner("source inactive") local source = obs.calldata_source(cd, "source") From 35622f12f9ede511b8a62d786148ef322c8b35ba Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 10 Oct 2021 03:28:38 -0600 Subject: [PATCH 028/105] Backing up to older rename methods as new one is all crashes. Best laid plans often fall apart. OBS has a way of not letting you get where you want to go. Backing up is tough but not much choice for now. --- lyrics.lua | 172 ++-- lyricstrial.lua | 2537 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2629 insertions(+), 80 deletions(-) create mode 100644 lyricstrial.lua diff --git a/lyrics.lua b/lyrics.lua index 308edf6..3681b6f 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -84,7 +84,7 @@ source_props = nil --monitor variables mon_song = "" mon_lyric = "" -mon_verse = "" +mon_verse = 0 mon_nextlyric = "" mon_alt = "" mon_nextalt = "" @@ -109,14 +109,15 @@ showhelp = false transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false -editVisSet = false +source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now +editVisSet = false -- simple debugging/print mechanism DEBUG = true -- on/off switch for entire debugging mechanism DEBUG_METHODS = true -- print method names DEBUG_INNER = true -- print inner method breakpoints -DEBUG_CUSTOM = false -- print custom debugging messages +DEBUG_CUSTOM = true -- print custom debugging messages DEBUG_BOOL = true -- print message with bool state true/false -------- @@ -2272,80 +2273,101 @@ end -------- -- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) -function index_sources() +function rename_source() + -- pause_timer = true local sources = obs.obs_enum_sources() if (sources ~= nil) then -- count and index sources local t = 1 for _, source in ipairs(sources) do - local settings = obs.obs_source_get_settings(source) - if obs.obs_data_get_bool(settings,"isLLC") then + local source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "Prepare_Lyrics" then + local settings = obs.obs_source_get_settings(source) obs.obs_data_set_string(settings, "index", t) -- add index to source data t = t + 1 - end - obs.obs_data_release(settings) -- release memory + obs.obs_data_release(settings) -- release memory + end + end + -- Find and mark Duplicates in loadLyric_items table + local loadLyric_items = {} -- Start Table for all load Sources + local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items + if scenes ~= nil then + for _, scenesource in ipairs(scenes) do -- Loop through all scenes + local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer + local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id + if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) + if loadLyric_items[index] == nil then + loadLyric_items[index] = "x" -- First time to find this source so mark with x + else + loadLyric_items[index] = "*" -- Found this source again so mark with * + end + obs.obs_data_release(settings) -- release memory + end + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + end + obs.source_list_release(scenes) -- Free source list end - end - local loadLyric_items = {} -- Start Table for all load Sources - local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items - if scenes ~= nil then - for _, scenesource in ipairs(scenes) do -- Loop through all scenes - local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer - local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - if obs.obs_data_get_bool(settings,"isLLC") then - local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) - obs.obs_data_set_bool(settings,"duplicate", loadLyric_items[index]) -- false (nil) the first time - loadLyric_items[index] = true -- duplicate (true) if used again - end - obs.obs_data_release(settings) -- release memory - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - end - obs.source_list_release(scenes) -- Free source list - end - obs.source_list_release(sources) -end -function rename_sources() - local sources = obs.obs_enum_sources() - if (sources ~= nil) then - -- count and index sources - local t = 1 + -- Name Source with Song Title + local i = 1 for _, source in ipairs(sources) do - local settings = obs.obs_source_get_settings(source) - if obs.obs_data_get_bool(settings,"isLLC") then - local name = obs.obs_data_get_string(settings, "index") .. ". Load lyrics for: " .. - obs.obs_data_get_string(settings, "songs") .. "" -- use index for compare - if obs.obs_data_get_bool(settings, "duplicate") then - name = '' .. name .. " * " - end - obs.obs_source_set_name(source, name) - end - obs.obs_data_release(settings) -- release memory + local source_id = obs.obs_source_get_unversioned_id(source) -- Get source + if source_id == "Prepare_Lyrics" then -- Skip if not a Load Lyric source + local c_name = obs.obs_source_get_name(source) -- Get current Source Name + local settings = obs.obs_source_get_settings(source) -- Get settings for this source + local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load + local index = obs.obs_data_get_string(settings, "index") -- get index + if (song ~= nil) then + local name = t - i .. ". Load lyrics for: " .. song .. "" -- use index for compare + -- Mark Duplicates + if index ~= nil then + if loadLyric_items[index] == "*" then + name = '' .. name .. " * " + end + if (c_name ~= name) then + obs.obs_source_set_name(source, name) + end + end + i = i + 1 + end + obs.obs_data_release(settings) + end end - obs.source_list_release(sources) end + obs.source_list_release(sources) + -- pause_timer = false end source_def.get_name = function() return "Prepare Lyric" end -saved = false - source_def.save = function(data, settings) + if saved then return end -- obs calls save for every load source in all scenes and we only need it once (So we could probably do rename here eventually) + dbg_method("Source_save") + saved = true + using_source = true + + rename_source() -- Rename and Mark sources instantly on update (WZ) end source_def.update = function(data, settings) - saved = false -- mark properties changed - source_sets = settings -- saved so the actual SAVE callback can update the right song +dbg_method("update") + +end + +source_def.load = function(data) +dbg_method("load") end function source_refresh_button_clicked(props, p) @@ -2361,11 +2383,19 @@ function source_refresh_button_clicked(props, p) return true end +function source_selection_made(props, prop, settings) +dbg_method("source_selection") + local name = obs.obs_data_get_string(settings,"songs") + saved = false -- mark properties changed + using_source = true + prepare_selected(name) + return true +end + source_def.get_properties = function(data) source_filter = true load_source_song_directory(true) local source_props = obs.obs_properties_create() - index_sources() local source_dir_list = obs.obs_properties_add_list( source_props, @@ -2374,6 +2404,7 @@ source_def.get_properties = function(data) obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) + obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(source_dir_list, name, name) @@ -2386,20 +2417,20 @@ source_def.get_properties = function(data) obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) + dbg_inner("props") return source_props + end source_def.create = function(settings, source) dbg_method("create") data = {} source_sets = settings - obs.obs_data_set_string(settings,"index",nil) - obs.obs_data_set_bool(settings,"duplicate",false) - obs.obs_data_set_bool(settings,"isLLC",true) + obs.obs_data_set_bool(settings,"isLLS", true) obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "save", source_save) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback return data end @@ -2415,7 +2446,7 @@ end function rename_callback() obs.remove_current_callback() - rename_sources() + rename_source() end function on_event(event) @@ -2425,7 +2456,7 @@ function on_event(event) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then - dbg_inner("Scene Change") + dbg_inner("Scene Change") obs.timer_add(rename_callback, 1000) end end @@ -2460,25 +2491,6 @@ function source_isactive(cd) source_active = true -- using source lyric end -function source_save(cd) - dbg_inner("source save") - local source = obs.calldata_source(cd, "source") - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - dbg_inner("Index: " .. obs.obs_data_get_string(settings,"index")) - dbg_bool("Duplicate: ", obs.obs_data_get_bool(settings,"duplicate")) - --using_source = true - --prepare_selected(obs.obs_data_get_string(source_sets, "songs")) -- show song to user - local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load - local index = obs.obs_data_get_string(settings, "index") -- get index - local duplicate = obs.obs_data_get_bool(settings,"duplicate") - obs.obs_data_release(settings) -- release memory - local name = index .. ". Load lyrics for: " .. song .. "" -- use index for compare - if duplicate then - name = '' .. name .. " * " - end - obs.obs_source_set_name(source, name) -end - function source_inactive(cd) dbg_inner("source inactive") local source = obs.calldata_source(cd, "source") diff --git a/lyricstrial.lua b/lyricstrial.lua new file mode 100644 index 0000000..addac23 --- /dev/null +++ b/lyricstrial.lua @@ -0,0 +1,2537 @@ +--- Copyright 2020 amirchev + +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at + +-- http://www.apache.org/licenses/LICENSE-2.0 + +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- added delete single prepared song (WZ) + +obs = obslua +bit = require("bit") + +-- source definitions +source_data = {} +source_def = {} +source_def.id = "Prepare_Lyrics" +source_def.type = OBS_SOURCE_TYPE_INPUT +source_def.output_flags = bit.bor(obs.OBS_SOURCE_CUSTOM_DRAW) + +-- text sources +source_name = "" +alternate_source_name = "" +static_source_name = "" +static_text = "" +-- current_scene = "" +-- preview_scene = "" +title_source_name = "" + +-- settings +windows_os = false +first_open = true +-- in_timer = false +-- in_Load = false +-- in_directory = false +-- pause_timer = false +display_lines = 0 +ensure_lines = true +-- visible = false + +-- lyrics status +-- TODO: removed displayed_song and use prepared_songs[prepared_index] +-- displayed_song = "" +lyrics = {} +verses = {} + +-- refrain = {} +alternate = {} +page_index = 0 +prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected +song_directory = {} +prepared_songs = {} +link_text = false -- true if Title and Static should fade with text only during hide/show +all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide +source_song_title = "" -- The song title from a source loaded song +using_source = false -- true when a lyric load song is being used instead of a pre-prepared song +source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) + +load_scene = "" -- name of scene loading a lyric with a source +timer_exists = false +--forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility + +-- hotkeys +hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_p_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_c_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_n_p_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID + +-- script placeholders +script_sets = nil +script_props = nil +source_sets = nil +source_props = nil + +--monitor variables +mon_song = "" +mon_lyric = "" +mon_verse = 0 +mon_nextlyric = "" +mon_alt = "" +mon_nextalt = "" +mon_nextsong = "" +meta_tags = "" + +-- text status & fade +TEXT_VISIBLE = 0 -- text is visible +TEXT_HIDDEN = 1 -- text is hidden +TEXT_SHOWING = 3 -- going from hidden -> visible +TEXT_HIDING = 4 -- going from visible -> hidden +TEXT_TRANSITION_OUT = 5 -- fade out transition to next lyric +TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change +text_status = TEXT_VISIBLE +text_opacity = 100 +text_fade_speed = 1 +text_fade_enabled = false +load_source = nil +expandcollapse = true +showhelp = false + +transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) +transition_completed = false + +editVisSet = false + + +-- simple debugging/print mechanism +DEBUG = true -- on/off switch for entire debugging mechanism +DEBUG_METHODS = true -- print method names +DEBUG_INNER = true -- print inner method breakpoints +DEBUG_CUSTOM = false -- print custom debugging messages +DEBUG_BOOL = true -- print message with bool state true/false + +-------- +---------------- +------------------------ CALLBACKS +---------------- +-------- +function anythingShowing() + return sourceShowing() or alternateShowing() or titleShowing() or staticShowing() +end + +function sourceShowing() + local source = obs.obs_get_source_by_name(source_name) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function alternateShowing() + local source = obs.obs_get_source_by_name(alternate_source_name) + local showing = false + if source ~= nil then + -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function titleShowing() + local source = obs.obs_get_source_by_name(title_source_name) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function staticShowing() + local source = obs.obs_get_source_by_name(static_source_name) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function anythingActive() + return sourceActive() or alternateActive() or titleActive() or staticActive() +end + +function sourceActive() + local source = obs.obs_get_source_by_name(source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function alternateActive() + local source = obs.obs_get_source_by_name(alternate_source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function titleActive() + local source = obs.obs_get_source_by_name(title_source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function staticActive() + local source = obs.obs_get_source_by_name(static_source_name) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function next_lyric(pressed) + if not pressed then + return + end + dbg_method("next_lyric") + -- check if transition enabled + if transition_enabled and not transition_completed then + obs.obs_frontend_preview_program_trigger_transition() + transition_completed = true + return + end + dbg_inner("next page") + if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing + if page_index < #lyrics then + page_index = page_index + 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + next_prepared(true) + end + end +end + +function prev_lyric(pressed) + if not pressed then + return + end + dbg_method("prev_lyric") + if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing + if page_index > 1 then + page_index = page_index - 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + prev_prepared(true) + end + end +end + +function prev_prepared(pressed) + if not pressed then + return + end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) + return + end + if prepared_index > 1 then + using_source = false + prepare_selected(prepared_songs[prepared_index - 1]) + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared + else + using_source = true + prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source + load_source_song(load_source, false) + end +end + +function next_prepared(pressed) + if not pressed then + return + end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song + return + end + if prepared_index < #prepared_songs then + using_source = false + prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available + else + using_source = true + prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source + load_source_song(load_source, false) + end +end + +function toggle_lyrics_visibility(pressed) + dbg_method("toggle_lyrics_visibility") + if not pressed then + return + end + if text_status ~= TEXT_HIDDEN then + dbg_inner("hiding") + set_text_visibility(TEXT_HIDDEN) + else + dbg_inner("showing") + set_text_visibility(TEXT_VISIBLE) + end +end + +function get_load_lyric_song() + local scene = obs.obs_frontend_get_current_scene() + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + local song = nil + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id + if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + song = obs.obs_data_get_string(settings, "song") -- Get index for this source (set earlier) + obs.obs_data_release(settings) -- release memory + end + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + return song +end + +function home_prepared(pressed) + if not pressed then + return false + end + dbg_method("home_prepared") + using_source = false + page_index = 0 + + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + end + obs.obs_properties_apply_settings(props, script_sets) + prepared_index = 1 + prepare_selected(prepared_songs[prepared_index]) + return true +end + +function home_song(pressed) + if not pressed then + return false + end + dbg_method("home_song") + page_index = 1 + transition_lyric_text(false) + return true +end + +function get_current_scene_name() + dbg_method("get_current_scene_name") + local scene = obs.obs_frontend_get_current_scene() + local current_scene = obs.obs_source_get_name(scene) + obs.obs_source_release(scene) + if current_scene ~= nil then + return current_scene + else + return "-" + end +end + +function next_button_clicked(props, p) + next_lyric(true) + return true +end + +function prev_button_clicked(props, p) + prev_lyric(true) + return true +end + +function toggle_button_clicked(props, p) + toggle_lyrics_visibility(true) + return true +end + +function home_button_clicked(props, p) + home_song(true) + return true +end + +function reset_button_clicked(props, p) + home_prepared(true) + return true +end +function prev_prepared_clicked(props, p) + prev_prepared(true) + return true +end + +function next_prepared_clicked(props, p) + next_prepared(true) + return true +end + +function save_song_clicked(props, p) + local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") + local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") + -- if this is a new song, add it to the directory + if save_song(name, text) then + local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") + obs.obs_property_list_add_string(prop_dir_list, name, name) + obs.obs_data_set_string(script_sets, "prop_directory_list", name) + obs.obs_properties_apply_settings(props, script_sets) + elseif prepared_songs[prepared_index] == name then + -- if this song is being displayed, then prepare it anew + prepare_song_by_name(name) + transition_lyric_text(false) + end + return true +end + +function delete_song_clicked(props, p) + dbg_method("delete_song_clicked") + -- call delete song function + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + delete_song(name) + -- update + local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") + for i = 0, obs.obs_property_list_item_count(prop_dir_list) do + if obs.obs_property_list_item_string(prop_dir_list, i) == name then + obs.obs_property_list_item_remove(prop_dir_list, i) + if i > 1 then + i = i - 1 + end + if #song_directory > 0 then + obs.obs_data_set_string(script_sets, "prop_directory_list", song_directory[i]) + else + obs.obs_data_set_string(script_sets, "prop_directory_list", "") + obs.obs_data_set_string(script_sets, "prop_edit_song_title", "") + obs.obs_data_set_string(script_sets, "prop_edit_song_text", "") + end + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + if get_index_in_list(prepared_songs, name) ~= nil then + if obs.obs_property_list_item_string(prop_prep_list, i) == name then + obs.obs_property_list_item_remove(prop_prep_list, i) + if i > 1 then + i = i - 1 + end + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[i]) + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + end + end + end + obs.obs_properties_apply_settings(props, script_sets) + return true + end + end + return true +end + +-- prepare song button clicked +function prepare_song_clicked(props, p) + dbg_method("prepare_song_clicked") + if #prepared_songs == 0 then + set_text_visibility(TEXT_HIDDEN) + end + prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) + + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) + -- prepare_song_by_index(#prepared_songs) + --end + obs.obs_properties_apply_settings(props, script_sets) + save_prepared() + return true +end + +function refresh_button_clicked(props, p) + local source_prop = obs.obs_properties_get(props, "prop_source_list") + local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") + local static_source_prop = obs.obs_properties_get(props, "prop_static_list") + local title_source_prop = obs.obs_properties_get(props, "prop_title_list") + obs.obs_property_list_clear(source_prop) -- clear current properties list + obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list + obs.obs_property_list_clear(static_source_prop) -- clear current properties list + obs.obs_property_list_clear(title_source_prop) -- clear current properties list + + local sources = obs.obs_enum_sources() + if sources ~= nil then + local n = {} + for _, source in ipairs(sources) do + source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + n[#n + 1] = obs.obs_source_get_name(source) + end + end + table.sort(n) + obs.obs_property_list_add_string(source_prop, "", "") + obs.obs_property_list_add_string(title_source_prop, "", "") + obs.obs_property_list_add_string(alternate_source_prop, "", "") + obs.obs_property_list_add_string(static_source_prop, "", "") + for _, name in ipairs(n) do + obs.obs_property_list_add_string(source_prop, name, name) + obs.obs_property_list_add_string(title_source_prop, name, name) + obs.obs_property_list_add_string(alternate_source_prop, name, name) + obs.obs_property_list_add_string(static_source_prop, name, name) + end + end + refresh_directory() + + return true +end + +function refresh_directory_button_clicked(props, p) +dbg_method("refresh directory") + refresh_directory() + return true +end + +function refresh_directory() + local prop_dir_list = obs.obs_properties_get(script_props,"prop_directory_list") + local source_prop = obs.obs_properties + obs.source_list_release(sources) + source_filter = false + load_source_song_directory(true) + table.sort(song_directory) + obs.obs_property_list_clear(prop_dir_list) -- clear directories + for _, name in ipairs(song_directory) do + dbg_inner(name) + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_properties_apply_settings(script_props, script_sets) +end + +function prepare_selection_made(props, prop, settings) + dbg_method("prepare_selection_made") + local name = obs.obs_data_get_string(settings, "prop_prepared_list") + using_source = false + prepare_selected(name) + return true +end + +-- removes prepared songs +function clear_prepared_clicked(props, p) + dbg_method("clear_prepared_clicked") + prepared_songs = {} -- required for monitor page + page_index = 0 -- required for monitor page + prepared_index = 0 -- required for monitor page + update_source_text() -- required for monitor page + -- clear the list + local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prep_prop) + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + obs.obs_properties_apply_settings(props, script_sets) + save_prepared() + return true +end + +function prepare_selected(name) + dbg_method("prepare_selected") + -- try to prepare song + if prepare_song_by_name(name) then + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + else + source_song_title = name + end + all_sources_fade = true + -- if using source, then force show the new lyrics, even if lyrics were previously hidden + transition_lyric_text(using_source) + else + -- hide everything if unable to prepare song + -- TODO: clear lyrics entirely after text is hidden + set_text_visibility(TEXT_HIDDEN) + end + + --update_source_text() + return true +end + +-- called when selection is made from directory list +function preview_selection_made(props, prop, settings) + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + + if get_index_in_list(song_directory, name) == nil then + return false + end -- do nothing if invalid name + + obs.obs_data_set_string(settings, "prop_edit_song_title", name) + local song_lines = get_song_text(name) + local combined_text = "" + for i, line in ipairs(song_lines) do + if (i < #song_lines) then + combined_text = combined_text .. line .. "\n" + else + combined_text = combined_text .. line + end + end + obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) + return true +end + +function open_song_clicked(props, p) + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + if windows_os then + os.execute('explorer "' .. path .. '"') + else + os.execute('xdg-open "' .. path .. '"') + end + return true +end + +function open_button_clicked(props, p) + local path = get_songs_folder_path() + if windows_os then + os.execute('explorer "' .. path .. '"') + else + os.execute('xdg-open "' .. path .. '"') + end +end + +-------- +---------------- +------------------------ PROGRAM FUNCTIONS +---------------- +-------- + +function apply_source_opacity() +-- dbg_method("apply_source_visiblity") + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + local source = obs.obs_get_source_by_name(source_name) + if source ~= nil then + obs.obs_source_update(source, settings) + end + obs.obs_source_release(source) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + if alt_source ~= nil then + obs.obs_source_update(alt_source, settings) + end + obs.obs_source_release(alt_source) + if all_sources_fade and link_text then + local title_source = obs.obs_get_source_by_name(title_source_name) + if title_source ~= nil then + obs.obs_source_update(title_source, settings) + end + obs.obs_source_release(title_source) + local static_source = obs.obs_get_source_by_name(static_source_name) + if static_source ~= nil then + obs.obs_source_update(static_source, settings) + end + obs.obs_source_release(static_source) + end + obs.obs_data_release(settings) +end + +function set_text_visibility(end_status) + dbg_method("set_text_visibility") + -- if already at desired visibility, then exit + if text_status == end_status then + return + end + -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + end + if text_status == end_status then + apply_source_opacity() + update_source_text() + return + end + --if text_fade_enabled then + -- if fade enabled, begin fade in or out + if end_status == TEXT_HIDDEN then + text_status = TEXT_HIDING + elseif end_status == TEXT_VISIBLE then + text_status = TEXT_SHOWING + end + all_sources_fade = true + start_fade_timer() + --end + update_source_text() +end + +-- transition to the next lyrics, use fade if enabled +-- if lyrics are hidden, force_show set to true will make them visible +function transition_lyric_text(force_show) + dbg_method("transition_lyric_text") + dbg_bool("using_source", using_source) + -- update the lyrics display immediately on 2 conditions + -- a) the text is hidden or hiding, and we will not force it to show + -- b) text fade is not enabled + -- otherwise, start text transition out and update the lyrics once + -- fade out transition is complete + if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then + update_source_text() + -- if text is done hiding, we can cancel the all_sources_fade + if text_status == TEXT_HIDDEN then + all_sources_fade = false + end + dbg_inner("hidden") + elseif not text_fade_enabled then + -- if text fade is not enabled, then we can cancel the all_sources_fade + all_sources_fade = false + set_text_visibility(TEXT_VISIBLE) + update_source_text() + dbg_inner("no text fade") + else + text_status = TEXT_TRANSITION_OUT + start_fade_timer() + end + dbg_bool("using_source", using_source) +end + +function start_fade_timer() + if not timer_exists then + timer_exists = true + obs.timer_add(fade_callback, 50) + dbg_inner("started fade timer") + end +end + +function fade_callback() + -- if not in a transitory state, exit callback + if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then + timer_exists = false + obs.remove_current_callback() + all_sources_fade = false + end + -- the amount we want to change opacity by + local opacity_delta = 1 + text_fade_speed + -- change opacity in the direction of transitory state + if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then + local new_opacity = text_opacity - opacity_delta + if new_opacity > 0 then + text_opacity = new_opacity + else + -- completed fade out, determine next move + text_opacity = 0 + if text_status == TEXT_TRANSITION_OUT then + -- update to new lyric between fades + update_source_text() + -- begin transition back in + text_status = TEXT_TRANSITION_IN + else + text_status = TEXT_HIDDEN + end + end + elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then + local new_opacity = text_opacity + opacity_delta + if new_opacity < 100 then + text_opacity = new_opacity + else + -- completed fade in + text_opacity = 100 + text_status = TEXT_VISIBLE + end + end + -- apply the new opacity + apply_source_opacity() +end + +function prepare_song_by_index(index) + dbg_method("prepare_song_by_index") + if index <= #prepared_songs then + prepare_song_by_name(prepared_songs[index]) + end +end + +-- prepares lyrics of the song +function prepare_song_by_name(name) + dbg_method("prepare_song_by_name") + if name == nil then + return false + end + -- if using transition on lyric change, first transition + -- would be reset with new song prepared + transition_completed = false + -- load song lines + local song_lines = get_song_text(name) + if song_lines == nil then + return false + end + local cur_line = 1 + local cur_aline = 1 + local recordRefrain = false + local playRefrain = false + local use_alternate = false + local use_static = false + local showText = true + local commentBlock = false + local singleAlternate = false + local refrain = {} + local arefrain = {} + lyrics = {} + verses = {} + alternate = {} + static_text = "" + alt_title = "" + local adjusted_display_lines = display_lines + local refrain_display_lines = display_lines + local alternate_display_lines = display_lines + local displaySize = display_lines + for _, line in ipairs(song_lines) do + local new_lines = 1 + local single_line = false + local comment_index = line:find("//%[") -- Look for comment block Set + if comment_index ~= nil then + commentBlock = true + line = line:sub(comment_index + 3) + end + comment_index = line:find("//]") -- Look for comment block Clear + if comment_index ~= nil then + commentBlock = false + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + if not commentBlock then + local comment_index = line:find("%s*//") + if comment_index ~= nil then + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + local alternate_index = line:find("#A%[") + if alternate_index ~= nil then + use_alternate = true + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + alternate_index = line:find("#A]") + if alternate_index ~= nil then + use_alternate = false + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + local static_index = line:find("#S%[") + if static_index ~= nil then + use_static = true + line = line:sub(1, static_index - 1) + new_lines = 0 + end + static_index = line:find("#S]") + if static_index ~= nil then + use_static = false + line = line:sub(1, static_index - 1) + new_lines = 0 + end + + local newcount_index = line:find("#L:") + if newcount_index ~= nil then + local iS, iE = line:find("%d+", newcount_index + 3) + local newLines = tonumber(line:sub(iS, iE)) + if use_alternate then + alternate_display_lines = newLines + elseif recordRefrain then + refrain_display_lines = newLines + else + adjusted_display_lines = newLines + refrain_display_lines = newLines + alternate_display_lines = newLines + end + line = line:sub(1, newcount_index - 1) + new_lines = 0 -- ignore line + end + local static_index = line:find("#S:") + if static_index ~= nil then + line = line:sub(static_index+3) + static_text = line + new_lines = 0 + end + local title_index = line:find("#T:") + if title_index ~= nil then + local title_indexEnd = line:find("%s+", title_index + 1) + line = line:sub(title_indexEnd + 1) + alt_title = line + new_lines = 0 + end + local alt_index = line:find("#A:") + if alt_index ~= nil then + local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) + new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) + local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) + line = line:sub(alt_indexEnd + 1) + singleAlternate = true + end + if line:find("###") ~= nil then -- Look for single line + line = line:gsub("%s*###%s*", "") + single_line = true + end + local newcount_index = line:find("#D:") + if newcount_index ~= nil then + local newcount_indexStart, newcount_indexEnd = line:find("%d+", newcount_index + 3) + new_lines = tonumber(line:sub(newcount_indexStart, newcount_indexEnd)) + _, newcount_indexEnd = line:find("%s+", newcount_indexEnd + 1) + line = line:sub(newcount_indexEnd + 1) + end + local refrain_index = line:find("#R%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = false + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#R]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + + refrain_index = line:find("##R") + if refrain_index == nil then + refrain_index = line:find("##r") + end + if refrain_index ~= nil then + playRefrain = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + else + playRefrain = false + end + newcount_index = line:find("#P:") + if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) + line = line:sub(1, newcount_index - 1) + end + newcount_index = line:find("#B:") + if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) + line = line:sub(1, newcount_index - 1) + end + local phantom_index = line:find("##P") + if phantom_index ~= nil then + line = line:sub(1, phantom_index - 1) + end + phantom_index = line:find("##B") + if phantom_index ~= nil then + line = line:gsub("%s*##B%s*", "") .. "\n" + end + local verse_index = line:find("##V") + if verse_index ~= nil then + line = line:sub(1, verse_index - 1) + new_lines = 0 + verses[#verses+1] = #lyrics + dbg_inner("Verse: " .. #lyrics) + end + if line ~= nil then + if use_static then + if static_text == "" then + static_text = line + else + static_text = static_text .. "\n" .. line + end + else + if use_alternate or singleAlternate then + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = alternate_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + arefrain[#refrain + 1] = line + else + arefrain[#refrain] = arefrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_aline == 1) then + alternate[#alternate + 1] = line + else + alternate[#alternate] = alternate[#alternate] .. "\n" .. line + end + end + cur_aline = cur_aline + 1 + if single_line or singleAlternate or cur_aline > displaySize then + if ensure_lines then + for i = cur_aline, displaySize, 1 do + cur_aline = i + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + if recordRefrain then + arefrain[#refrain] = arefrain[#refrain] .. "\n" + end + end + end + cur_aline = 1 + end + new_lines = new_lines - 1 + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(arefrain) do + alternate[#alternate + 1] = refrain_line + end + end + singleAlternate = false + else + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = adjusted_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + refrain[#refrain + 1] = line + else + refrain[#refrain] = refrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_line == 1) then + lyrics[#lyrics + 1] = line + else + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line + end + end + cur_line = cur_line + 1 + if single_line or cur_line > displaySize then + if ensure_lines then + for i = cur_line, displaySize, 1 do + cur_line = i + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end + end + cur_line = 1 + end + new_lines = new_lines - 1 + end + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(refrain) do + lyrics[#lyrics + 1] = refrain_line + end + end + end + end + end + end + if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then + for i = cur_line, displaySize, 1 do + cur_line = i + if use_alternate then + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + else + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end + end + lyrics[#lyrics + 1] = "" + -- pause_timer = false + return true +end + +-- finds the index of a song in the directory +-- if item is not in list, then return nil +function get_index_in_list(list, q_item) + for index, item in ipairs(list) do + if item == q_item then + return index + end + end + return nil +end + +-------- +---------------- +------------------------ FILE FUNCTIONS +---------------- +-------- + +-- delete previewed song +function delete_song(name) + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + os.remove(path) + table.remove(song_directory, get_index_in_list(song_directory, name)) + source_filter = false + load_source_song_directory(false) +end + +-- loads the song directory +function load_source_song_directory(use_filter) +dbg_method("load_source_song_directory") + local keytext = meta_tags + if source_filter then + keytext = obs.obs_data_get_string(source_sets, "prop_edit_metatags") + end + local keys = ParseCSVLine(keytext) + song_directory = {} + local filenames = {} + local tags = {} + local dir = obs.os_opendir(get_songs_folder_path()) + -- get_songs_folder_path()) + local entry + local songExt + local songTitle + local goodEntry = true + + repeat + entry = obs.os_readdir(dir) + if + entry and not entry.directory and + (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") then + songExt = obs.os_get_path_extension(entry.d_name) + songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) + tags = readTags(songTitle) + goodEntry = true + if use_filter and #keys>0 then -- need to check files + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + goodEntry = false -- start assuming file will not be shown + if #tags == 0 then -- check no tagged option + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + else -- have keys and tags so compare them + for k = 1, #keys do + for t = 1, #tags do + if tags[t] == keys[k] then + goodEntry = true -- found match so show file + break + end + end + if goodEntry then -- stop outer key loop on match + break + end + end + end + end + if goodEntry then -- add file if valid match + if songExt == ".enc" then + song_directory[#song_directory + 1] = dec(songTitle) + else + song_directory[#song_directory + 1] = songTitle + end + end + end + until not entry + obs.os_closedir(dir) +end + +function readTags(name) + local meta = "" + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + meta = line + break; + end + file:close() + end + local meta_index = meta:find("//meta ") -- Look for meta block Set + if meta_index ~= nil then + meta = meta:sub(meta_index + 7) + return ParseCSVLine(meta) + end + return {} +end + +function ParseCSVLine (line) + local res = {} + local pos = 1 + sep = ',' + while true do + local c = string.sub(line,pos,pos) + if (c == "") then break end + if (c == '"') then + local txt = "" + repeat + local startp,endp = string.find(line,'^%b""',pos) + txt = txt..string.sub(line,startp+1,endp-1) + pos = endp + 1 + c = string.sub(line,pos,pos) + if (c == '"') then txt = txt..'"' end + until (c ~= '"') + txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + table.insert(res,txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp,endp = string.find(line,sep,pos) + if (startp) then + local t = string.sub(line,pos,startp-1) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + table.insert(res,t) + pos = endp + 1 + else + local t = string.sub(line,pos) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + table.insert(res,t) + break + end + end + end + return res +end + +local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet + +-- encoding +function enc(data) + return ((data:gsub( + ".", + function(x) + local r, b = "", x:byte() + for i = 8, 1, -1 do + r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ) .. "0000"):gsub( + "%d%d%d?%d?%d?%d?", + function(x) + if (#x < 6) then + return "" + end + local c = 0 + for i = 1, 6 do + c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0) + end + return b:sub(c + 1, c + 1) + end + ) .. ({"", "==", "="})[#data % 3 + 1]) +end + +function dec(data) + data = string.gsub(data, "[^" .. b .. "=]", "") + return (data:gsub( + ".", + function(x) + if (x == "=") then + return "" + end + local r, f = "", (b:find(x) - 1) + for i = 6, 1, -1 do + r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ):gsub( + "%d%d%d?%d?%d?%d?%d?%d?", + function(x) + if (#x ~= 8) then + return "" + end + local c = 0 + for i = 1, 8 do + c = c + (x:sub(i, i) == "1" and 2 ^ (8 - i) or 0) + end + return string.char(c) + end + )) +end + +function testValid(filename) + if string.find(filename, "[\128-\255]") ~= nil then + return false + end + if string.find(filename, '[\\\\/:*?"<>|]') ~= nil then + return false + end + return true +end + +-- saves previewed song, return true if new song +function save_song(name, text) + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "w") + if file ~= nil then + for line in text:gmatch("([^\n]+)") do + local trimmed = line:match("%s*(%S-.*%S+)%s*") + if trimmed ~= nil then + file:write(trimmed, "\n") + end + end + file:close() + if get_index_in_list(song_directory, name) == nil then + song_directory[#song_directory + 1] = name + return true + end + end + return false +end + +-- saves preprepared songs +function save_prepared() + dbg_method("save_prepared") + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") + for i, name in ipairs(prepared_songs) do + -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs + file:write(name, "\n") + -- end + end + file:close() + return true +end + + +-- updates the selected lyrics +function update_source_text() + dbg_method("update_source_text") + dbg_inner("Page Index: " .. page_index) + local text = "" + local alttext = "" + local next_lyric = "" + local next_alternate = "" + local static = static_text + local mstatic = static -- save static for use with monitor + local title = "" + + + if alt_title ~= "" then + title = alt_title + else + if not using_source then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Load title from prepared: " .. prepared_index) + title = prepared_songs[prepared_index] + end + else + dbg_custom("Load title from source") + title = source_song_title + end + end + local mtitle = title -- save title for use with monitor + + local source = obs.obs_get_source_by_name(source_name) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) + + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then + if #lyrics > 0 then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] + end + end + if #alternate > 0 then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] + end + end + + if link_text then + if string.len(text) == 0 and string.len(alttext) == 0 then + static = "" + title = "" + end + end + end + -- update source texts + if source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + next_lyric = lyrics[page_index + 1] + if (next_lyric == nil) then + next_lyric = "" + end + end + if alt_source ~= nil then + local settings = obs.obs_data_create() -- setup TEXT settings with opacity values + obs.obs_data_set_string(settings, "text", alttext) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" + end + end + if stat_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) + end + if title_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) + end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) + obs.obs_source_release(title_source) + + local next_prepared = "" + if using_source then + next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song + elseif prepared_index < #prepared_songs then + next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song + else + if source_active then + next_prepared = source_song_title -- plan to go back to source loaded song + else + next_prepared = prepared_songs[1] -- plan to loop around to first prepared song + end + end + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i]+1 then + mon_verse = i + end + end -- v = current verse number for this page + end + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared + + update_monitor() +end + +function update_monitor() + + dbg_method("update_monitor") + local tableback = "black" + local text = "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = + text .. + "
" + text = + text .. + "
" + if using_source then + text = text .. "From Source: " .. load_scene .. "
" + else + text = text .. "Prepared Song: " .. prepared_index + text = + text .. + " of " .. #prepared_songs .. "
" + end + text = text .. "
Lyric Page: " .. page_index + text = text .. " of " .. #lyrics .. "
" + if #verses ~= nil and mon_verse>0 then + text = text .. "
Verse: " .. mon_verse + text = text .. " of " .. #verses .. "
" + end + text = text .. "
" + if not anythingActive() then + tableback = "#440000" + end + local visbgTitle = tableback + local visbgText = tableback + if text_status == TEXT_HIDDEN or text_status == TEXT_HIDING then + visbgText = "maroon" + if link_text then + visbgTitle = "maroon" + end + end + + text = + text .. + "
" + if mon_song ~= "" and Mon_song ~= nil then + text = + text .. + "" + text = + text .. + "" + end + if mon_lyric ~= "" and mon_lyric ~= nil then + text = + text .. + "" + text = text .. "" + end + if mon_nextlyric ~= "" and mon_nextlyric ~= nil then + text = + text .. + "" + text = text .. "" + end + if mon_alt ~= "" and mon_alt ~= nil then + text = + text .. + "" + text = + text .. "" + end + if mon_nextalt ~= "" and mon_nextalt ~= nil then + text = + text .. + "" + text = text .. "" + end + if mon_nextsong ~= "" and mon_nextsong ~= nil then + text = + text .. + "" + text = text .. "" + end + text = text .. "
Song
Title
" .. mon_song .. "
Current
Page
• " .. mon_lyric .. "
Next
Page
• " .. mon_nextlyric .. "
Alt
Lyric
• " .. mon_alt .. "
Next
Alt
• " .. mon_nextalt .. "
Next
Song:
" .. mon_nextsong .. "
" + local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") + dbg_inner("write monitor file") + file:write(text) + file:close() + return true +end + +-- returns path of the given song name +function get_song_file_path(name, suffix) + if name == nil then + return nil + end + return get_songs_folder_path() .. "\\" .. name .. suffix +end + +-- returns path of the lyrics songs folder +function get_songs_folder_path() + local sep = package.config:sub(1, 1) + local path = "" + if windows_os then + path = os.getenv("USERPROFILE") + else + path = os.getenv("HOME") + end + return path .. sep .. ".config" .. sep .. ".obs_lyrics" +end + +-- gets the text of a song +function get_song_text(name) + local song_lines = {} + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + song_lines[#song_lines + 1] = line + end + file:close() + else + return nil + end + + return song_lines +end + +-- ------ +---------------- +------------------------ OBS DEFAULT FUNCTIONS +-- -------------- +-------- + +-- A function named script_properties defines the properties that the user +-- can change for the entire script module itself + +local help = "░░░░░░░░░░░░░░░ MARKUP SYNTAX HELP ░░░░░░░░░░░░░░░\n\n" .. + "Markup      Syntax        Markup       Syntax\n" .. + "==========  ==========    ==========  ==========\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line\n\n" .. + "▲░░░░░░ CLICK TO CLOSE ░░░░░░▲" + +function script_properties() + dbg_method("script_properties") + editVisSet = false + script_props = obs.obs_properties_create() + obs.obs_properties_add_button(script_props, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) + obs.obs_properties_add_button(script_props, "expand_all_button", "▲░ HIDE ALL GROUPS ░▲", expand_all_groups) +----------- + obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) + gp = obs.obs_properties_create() + obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) + obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) + obs.obs_properties_add_button(gp, "prop_opensong_button","Edit Song with System Editor", open_song_clicked) + obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) + obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) +------------ + obs.obs_properties_add_button(script_props, "prepared_showing", "▲░ HIDE PREPARED SONGS ░▲",change_prepared_visible) + gp = obs.obs_properties_create() + local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Songs by Meta Tags", filter_songs_clicked) + gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) + obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) +-- prepare_props = prep_prop + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prep_prop, name, name) + end + obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared Songs List",edit_prepared_clicked) + eps = obs.obs_properties_create() + local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) + obs.obs_property_set_modified_callback(edit_prop, setEditVis) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) + obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) +------ + obs.obs_properties_add_button(script_props, "options_showing", "▲░ HIDE DISPLAY OPTIONS ░▲",change_options_visible) + gp = obs.obs_properties_create() + local lines_prop = obs.obs_properties_add_int(gp, "prop_lines_counter", "Lines to Display", 1, 100, 1) + obs.obs_property_set_long_description( + lines_prop, + "Sets default lines per page of lyric, overwritten by Markup: #L:n" + ) + + local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") + obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") + + local link_prop = + obs.obs_properties_add_bool(gp, "link_text", "Only show title and static text with lyrics") + obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") + + local transition_prop = + obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_property_set_modified_callback(transition_prop, change_transition_property) + obs.obs_property_set_long_description( + transition_prop, + "Use with Studio Mode, duplicate sources, and OBS source transitions" + ) + + local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) + obs.obs_property_set_modified_callback(fade_prop, change_fade_property) + obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) +------------- + obs.obs_properties_add_button(script_props, "src_showing", "▲░ HIDE SOURCE TEXT SELECTIONS ░▲",change_src_visible) + gp = obs.obs_properties_create() + local source_prop = + obs.obs_properties_add_list( + gp, + "prop_source_list", + "Text Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(source_prop, "Shows main lyric text") + local title_source_prop = + obs.obs_properties_add_list( + gp, + "prop_title_list", + "Title Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(title_source_prop, "Shows text from song title") + local alternate_source_prop = + obs.obs_properties_add_list( + gp, + "prop_alternate_list", + "Alternate Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(alternate_source_prop, "Shows text annotated with #A[ and #A]") + local static_source_prop = + obs.obs_properties_add_list( + gp, + "prop_static_list", + "Static Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_long_description(static_source_prop, "Shows text annotated with #S[ and #S]") + local sources = obs.obs_enum_sources() + if sources ~= nil then + local n = {} + for _, source in ipairs(sources) do + source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + n[#n + 1] = obs.obs_source_get_name(source) + end + end + table.sort(n) + obs.obs_property_list_add_string(source_prop, "", "") + obs.obs_property_list_add_string(title_source_prop, "", "") + obs.obs_property_list_add_string(alternate_source_prop, "", "") + obs.obs_property_list_add_string(static_source_prop, "", "") + for _, name in ipairs(n) do + obs.obs_property_list_add_string(source_prop, name, name) + obs.obs_property_list_add_string(title_source_prop, name, name) + obs.obs_property_list_add_string(alternate_source_prop, name, name) + obs.obs_property_list_add_string(static_source_prop, name, name) + end + end + obs.source_list_release(sources) + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) + obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) +------------------ + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) + gp = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(gp, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(gp, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(gp, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(gp, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(gp, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) + obs.obs_properties_add_button(gp,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) + obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,gp) +----------------- + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + end + pp = obs.obs_properties_get(script_props,"ctrl_grp") + obs.obs_property_set_visible(pp, true) + + obs.obs_properties_apply_settings(script_props, script_sets) + + return script_props +end + +-- A function named script_description returns the description shown to +-- the user + +local description = [[ +"Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from taxilian.
+ +
+ + + + + + + +
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
Optional comma delimeted meta tags following //meta on 1st line
+]] + +function script_description() + return "Manage song Lyrics and Other Paged Text (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from Taxilian.
" + end + +function expand_all_groups(props, prop, settings) + expandcollapse = not expandcollapse + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"info_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"prep_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"disp_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"src_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"ctrl_grp"), expandcollapse) + local mode1 = "▼░ SHOW " + local mode2 = "░▼" + if expandcollapse then + mode1 = "▲░ HIDE " + mode2 = "░▲" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + return true +end + + +function all_vis_equal() + return (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) or not + (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) +end + +function change_info_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"info_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) + local mode1 = "▼░ SHOW " + local mode2 = "░▼" + if vis then + mode1 = "▲░ HIDE " + mode2 = "░▲" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_prepared_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"prep_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) +local mode1 = "▼░ SHOW " + local mode2 = "░▼" + if vis then + mode1 = "▲░ HIDE " + mode2 = "░▲" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_options_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"disp_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) +local mode1 = "▼░ SHOW " + local mode2 = "░▼" + if vis then + mode1 = "▲░ HIDE " + mode2 = "░▲" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_src_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"src_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) +local mode1 = "▼░ SHOW " + local mode2 = "░▼" + if vis then + mode1 = "▲░ HIDE " + mode2 = "░▲" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_ctrl_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props,"ctrl_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp,vis) +local mode1 = "▼░ SHOW " + local mode2 = "░▼" + if vis then + mode1 = "▲░ HIDE " + mode2 = "░▲" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + if all_vis_equal() then + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + expandcollapse = not expandcollapse + end + return true +end + +function change_fade_property(props, prop, settings) + local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") + local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") + obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) + return true +end + +function show_help_button(props, prop, settings) +dbg_method("show help") + local hb = obs.obs_properties_get(props, "show_help_button") + showhelp = not showhelp + if showhelp then + obs.obs_property_set_description(hb, help) + else + obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") + end + return true +end + +function setEditVis(props, prop, settings) -- hides edit group on initial showing + dbg_method("setEditVis") + if not editVisSet then + local pp = obs.obs_properties_get(script_props,"edit_grp") + obs.obs_property_set_visible(pp, false) + pp = obs.obs_properties_get(props,"meta") + obs.obs_property_set_visible(pp, false) + editVisSet = true -- do this only once + end +end + +function filter_songs_clicked(props, p) + local pp = obs.obs_properties_get(props,"meta") + if not obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "filter_songs_button") + obs.obs_property_set_description(mpb, "Clear Song Filters") -- change button function + meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + refresh_directory() + else + obs.obs_property_set_visible(pp, false) + meta_tags = "" -- clear meta tags + refresh_directory() + local mpb = obs.obs_properties_get(props, "filter_songs_button") -- + obs.obs_property_set_description(mpb, "Filter Songs by Meta Tags") -- reset button function + end + return true +end + +function edit_prepared_clicked(props, p) + local pp = obs.obs_properties_get(props,"edit_grp") + if obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + return true + end + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + local count = obs.obs_property_list_item_count(prop_prep_list) + dbg_inner("count: " .. count) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + if count2 > 0 then + for i = 0, count2 do + obs.obs_data_array_erase(songNames,0) + end + end + + for i = 0, count-1 do + local song = obs.obs_property_list_item_string(prop_prep_list, i) + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song) + obs.obs_data_array_push_back(songNames,array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(script_sets, "prep_list", songNames) + obs.obs_data_array_release(songNames) + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Cancel Prepared Song Edits") + return true +end + +-- removes prepared songs +function save_edits_clicked(props, p) + load_source_song_directory(false) + prepared_songs = {} + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + if count2 > 0 then + for i = 0, count2-1 do + local item = obs.obs_data_array_item(songNames, i); + local itemName = obs.obs_data_get_string(item, "value"); + if get_index_in_list(song_directory, itemName) ~= nil then + prepared_songs[#prepared_songs+1] = itemName + obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) + end + end + end + save_prepared() + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + prepared_index = 1 + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + prepared_index = 0 + end + pp = obs.obs_properties_get(script_props,"edit_grp") + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + obs.obs_properties_apply_settings(props, script_sets) + return true +end + +function change_transition_property(props, prop, settings) + local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") + local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") + local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") + obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) + obs.obs_property_set_enabled(fade_speed_prop, not transition_set) + transition_enabled = transition_set + return true +end +-- A function named script_update will be called when settings are changed +function script_update(settings) + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") -- Fade Enable (WZ) + text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") -- Fade Speed (WZ) + reload = false + local cur_display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + if display_lines ~= cur_display_lines then + display_lines = cur_display_lines + reload = true + end + local cur_source_name = obs.obs_data_get_string(settings, "prop_source_list") + if source_name ~= cur_source_name then + source_name = cur_source_name + reload = true + end + local alt_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") + if alternate_source_name ~= alt_source_name then + alternate_source_name = alt_source_name + reload = true + end + local stat_source_name = obs.obs_data_get_string(settings, "prop_static_list") + if static_source_name ~= stat_source_name then + static_source_name = stat_source_name + reload = true + end + local cur_title_source = obs.obs_data_get_string(settings, "prop_title_list") + if title_source_name ~= cur_title_source then + title_source_name = cur_title_source + reload = true + end + local cur_ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") + if cur_ensure_lines ~= ensure_lines then + ensure_lines = cur_ensure_lines + reload = true + end + local cur_link_text = obs.obs_data_get_bool(settings, "link_text") + if cur_link_text ~= link_text then + link_text = cur_link_text + reload = true + end + + if reload then + if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then + prepare_selected(prepared_songs[prepared_index]) + end + end + +end + +-- A function named script_defaults will be called to set the default settings +function script_defaults(settings) + dbg_method("script_defaults") + obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + if windows_os then + os.execute('mkdir "' .. get_songs_folder_path() .. '"') + else + os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') + end + +end + +-- A function named script_save will be called when the script is saved +function script_save(settings) + dbg_method("script_save") + save_prepared() + local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) + obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) + obs.obs_data_set_array(settings, "lyric_prev_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) + obs.obs_data_set_array(settings, "lyric_clear_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) + obs.obs_data_set_array(settings, "next_prepared_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) + obs.obs_data_set_array(settings, "previous_prepared_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) + obs.obs_data_set_array(settings, "home_song_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) + obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) +end + +-- a function named script_load will be called on startup +function script_load(settings) + dbg_method("script_load") + hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) + local hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") + obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") + obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") + obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") + obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) + hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") + obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_reset_id = + obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") + obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + script_sets = settings + source_name = obs.obs_data_get_string(settings, "prop_source_list") + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + load_source_song_directory(false) + -- load prepared songs from previous + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") + if file ~= nil then + for line in file:lines() do + prepared_songs[#prepared_songs + 1] = line + end + --prepared_index = 1 + file:close() + end + local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") + local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") + local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") + obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) + obs.obs_property_set_enabled(fade_speed_prop, not transition_set) + transition_enabled = transition_set + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture +end + +-------- +---------------- +------------------------ SOURCE FUNCTIONS +---------------- +-------- + +-- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) +function index_sources() + local sources = obs.obs_enum_sources() + if (sources ~= nil) then + -- count and index sources + local t = 1 + for _, source in ipairs(sources) do + local settings = obs.obs_source_get_settings(source) + if obs.obs_data_get_bool(settings,"isLLC") then + obs.obs_data_set_string(settings, "index", t) -- add index to source data + t = t + 1 + end + obs.obs_data_release(settings) -- release memory + end + end + local loadLyric_items = {} -- Start Table for all load Sources + local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items + if scenes ~= nil then + for _, scenesource in ipairs(scenes) do -- Loop through all scenes + local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer + local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + if obs.obs_data_get_bool(settings,"isLLC") then + local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) + obs.obs_data_set_bool(settings,"duplicate", loadLyric_items[index]) -- false (nil) the first time + loadLyric_items[index] = true -- duplicate (true) if used again + end + obs.obs_data_release(settings) -- release memory + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + end + obs.source_list_release(scenes) -- Free source list + end + obs.source_list_release(sources) +end + +function rename_sources() + local sources = obs.obs_enum_sources() + if (sources ~= nil) then + -- count and index sources + local t = 1 + for _, source in ipairs(sources) do + local settings = obs.obs_source_get_settings(source) + if obs.obs_data_get_bool(settings,"isLLC") then + local name = obs.obs_data_get_string(settings, "index") .. ". Load lyrics for: " .. + obs.obs_data_get_string(settings, "songs") .. "" -- use index for compare + if obs.obs_data_get_bool(settings, "duplicate") then + name = '' .. name .. " * " + end + obs.obs_source_set_name(source, name) + end + obs.obs_data_release(settings) -- release memory + end + obs.source_list_release(sources) + end +end + +source_def.get_name = function() + return "Prepare Lyric" +end + +saved = false + +source_def.save = function(data, settings) + +end + +source_def.update = function(data, settings) + saved = false -- mark properties changed + source_sets = settings -- saved so the actual SAVE callback can update the right song +end + +function source_refresh_button_clicked(props, p) + dbg_method("source_refresh_button") + source_filter = true + load_source_song_directory(true) + table.sort(song_directory) + local prop_dir_list = obs.obs_properties_get(props,"source_directory_list") + obs.obs_property_list_clear(prop_dir_list) -- clear directories + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + return true +end + +source_def.get_properties = function(data) + source_filter = true + load_source_song_directory(true) + local source_props = obs.obs_properties_create() + index_sources() + local source_dir_list = + obs.obs_properties_add_list( + source_props, + "songs", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(source_dir_list, name, name) + end + gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "source_prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) + obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode + obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode + obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) + return source_props +end + +source_def.create = function(settings, source) +dbg_method("create") + data = {} + source_sets = settings + obs.obs_data_set_string(settings,"index",nil) + obs.obs_data_set_bool(settings,"duplicate",false) + obs.obs_data_set_bool(settings,"isLLC",true) + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "save", source_save) -- Set Preview Callback + return data +end + +source_def.get_defaults = function(settings) + obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) + obs.obs_data_set_default_string(settings, "index", "0") +end + +function update_source_callback() + obs.remove_current_callback() + update_monitor() +end + +function rename_callback() + obs.remove_current_callback() + rename_sources() +end + +function on_event(event) + dbg_method("on_event: " .. event) + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then + dbg_bool("Active:",source_active) + obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS + end + if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then + dbg_inner("Scene Change") + obs.timer_add(rename_callback, 1000) + end +end + +function load_source_song(source, preview) + dbg_method("load_source_song") + local settings = obs.obs_source_get_settings(source) + if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then + local song = obs.obs_data_get_string(settings, "songs") + using_source = true + load_source = source + set_text_visibility(TEXT_HIDDEN) + prepare_selected(song) + transition_lyric_text() + if obs.obs_data_get_bool(settings, "source_home_on_active") then + home_prepared(true) + end + end + obs.obs_data_release(settings) +end + + +function source_isactive(cd) + dbg_method("source_active") + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + dbg_inner("source active") + load_scene = get_current_scene_name() + load_source_song(source, false) + source_active = true -- using source lyric +end + +function source_save(cd) + dbg_inner("source save") + local source = obs.calldata_source(cd, "source") + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + dbg_inner("Index: " .. obs.obs_data_get_string(settings,"index")) + dbg_bool("Duplicate: ", obs.obs_data_get_bool(settings,"duplicate")) + --using_source = true + --prepare_selected(obs.obs_data_get_string(source_sets, "songs")) -- show song to user + local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load + local index = obs.obs_data_get_string(settings, "index") -- get index + local duplicate = obs.obs_data_get_bool(settings,"duplicate") + obs.obs_data_release(settings) -- release memory + local name = index .. ". Load lyrics for: " .. song .. "" -- use index for compare + if duplicate then + name = '' .. name .. " * " + end + obs.obs_source_set_name(source, name) +end + +function source_inactive(cd) + dbg_inner("source inactive") + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + source_active = false -- indicates source loading lyric is active (but using prepared lyrics is still possible) +end + +function source_showing(cd) + dbg_method("source_showing") + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + load_source_song(source, true) +end + +function dbg(message) + if DEBUG then + print(message) + end +end + +function dbg_inner(message) + if DEBUG_INNER then + dbg("INNR: " .. message) + end +end + +function dbg_method(message) + if DEBUG_METHODS then + dbg("-- MTHD: " .. message) + end +end + +function dbg_custom(message) + if DEBUG_CUSTOM then + dbg("CUST: " .. message) + end +end + +function dbg_bool(name, value) + if DEBUG_BOOL then + local message = "BOOL: " .. name + if value then + message = message .. " = true" + else + message = message .. " = false" + end + dbg(message) + end +end + + +obs.obs_register_source(source_def) From 690ce8fa9f9c6774980b04f8599c5e4986a7e0c0 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 10 Oct 2021 13:01:47 -0600 Subject: [PATCH 029/105] Update lyrics.lua Finally I think this is getting there. Used it in service this morning with no issues. Hid the numbers in the source names using qt markup. --- lyrics.lua | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 3681b6f..0a7a07e 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -90,6 +90,7 @@ mon_alt = "" mon_nextalt = "" mon_nextsong = "" meta_tags = "" +source_meta_tags = "" -- text status & fade TEXT_VISIBLE = 0 -- text is visible @@ -523,6 +524,7 @@ function refresh_button_clicked(props, p) obs.obs_property_list_add_string(static_source_prop, name, name) end end + obs.source_list_release(sources) refresh_directory() return true @@ -536,8 +538,7 @@ end function refresh_directory() local prop_dir_list = obs.obs_properties_get(script_props,"prop_directory_list") - local source_prop = obs.obs_properties - obs.source_list_release(sources) + local source_prop = obs.obs_properties_get(props, "prop_source_list") source_filter = false load_source_song_directory(true) table.sort(song_directory) @@ -1158,9 +1159,11 @@ function load_source_song_directory(use_filter) dbg_method("load_source_song_directory") local keytext = meta_tags if source_filter then - keytext = obs.obs_data_get_string(source_sets, "prop_edit_metatags") + keytext = source_meta_tags end + dbg_inner(keytext) local keys = ParseCSVLine(keytext) + song_directory = {} local filenames = {} local tags = {} @@ -1262,6 +1265,7 @@ function ParseCSVLine (line) if (c == '"') then txt = txt..'"' end until (c ~= '"') txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. txt) table.insert(res,txt) assert(c == sep or c == "") pos = pos + 1 @@ -1270,11 +1274,13 @@ function ParseCSVLine (line) if (startp) then local t = string.sub(line,pos,startp-1) t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) table.insert(res,t) pos = endp + 1 else local t = string.sub(line,pos) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) table.insert(res,t) break end @@ -1852,7 +1858,7 @@ local description = [[ ]] function script_description() - return "Manage song Lyrics and Other Paged Text (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from Taxilian.
" + return "Manage song Lyrics and Other Paged Text (Version: Nov 2021)
Authors: Amirchev & DC Strato; with significant contributions from Taxilian.
" end function expand_all_groups(props, prop, settings) @@ -2327,7 +2333,7 @@ function rename_source() local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load local index = obs.obs_data_get_string(settings, "index") -- get index if (song ~= nil) then - local name = t - i .. ". Load lyrics for: " .. song .. "" -- use index for compare + local name = "Load lyrics for: " .. song .. "" -- use index for compare -- Mark Duplicates if index ~= nil then if loadLyric_items[index] == "*" then @@ -2373,16 +2379,24 @@ end function source_refresh_button_clicked(props, p) dbg_method("source_refresh_button") source_filter = true + dbg_inner("tags: " .. source_meta_tags) load_source_song_directory(true) table.sort(song_directory) - local prop_dir_list = obs.obs_properties_get(props,"source_directory_list") + local prop_dir_list = obs.obs_properties_get(props,"songs") obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do + dbg_inner("SLD: " .. name) obs.obs_property_list_add_string(prop_dir_list, name, name) end return true end +function update_source_metatags(props, p, settings) + + source_meta_tags = obs.obs_data_get_string(settings,"metatags") + return true +end + function source_selection_made(props, prop, settings) dbg_method("source_selection") local name = obs.obs_data_get_string(settings,"songs") @@ -2410,7 +2424,8 @@ source_def.get_properties = function(data) obs.obs_property_list_add_string(source_dir_list, name, name) end gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "source_prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() @@ -2435,6 +2450,7 @@ dbg_method("create") end source_def.get_defaults = function(settings) +source_sets = settings obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) obs.obs_data_set_default_string(settings, "index", "0") end From 068a5f995d8a31b189e2f7f77188df19d255a0b3 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:39:59 -0600 Subject: [PATCH 030/105] Update lyrics.lua Added using_source test to prepare selected so that lyrics show when loaded by source, but not when prepared manually until show/hide. Moved markup help button. --- lyrics.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 0a7a07e..b3688d3 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -586,7 +586,9 @@ function prepare_selected(name) end all_sources_fade = true -- if using source, then force show the new lyrics, even if lyrics were previously hidden - transition_lyric_text(using_source) + if using_source then -- this keeps preapare selected from showing lyrics until hide/show + transition_lyric_text(using_source) + end else -- hide everything if unable to prepare song -- TODO: clear lyrics entirely after text is hidden @@ -1683,12 +1685,12 @@ function script_properties() dbg_method("script_properties") editVisSet = false script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) obs.obs_properties_add_button(script_props, "expand_all_button", "▲░ HIDE ALL GROUPS ░▲", expand_all_groups) ----------- obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) @@ -2048,6 +2050,7 @@ function edit_prepared_clicked(props, p) dbg_inner("count: " .. count) local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) + dbg_inner("count2: " .. count2) if count2 > 0 then for i = 0, count2 do obs.obs_data_array_erase(songNames,0) @@ -2056,6 +2059,7 @@ function edit_prepared_clicked(props, p) for i = 0, count-1 do local song = obs.obs_property_list_item_string(prop_prep_list, i) + print("song to move: " .. song) local array_obj = obs.obs_data_create() obs.obs_data_set_string(array_obj, "value", song) obs.obs_data_array_push_back(songNames,array_obj) @@ -2310,9 +2314,9 @@ function rename_source() local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) if loadLyric_items[index] == nil then - loadLyric_items[index] = "x" -- First time to find this source so mark with x + loadLyric_items[index] = 1 -- First time to find this source so mark with 1 else - loadLyric_items[index] = "*" -- Found this source again so mark with * + loadLyric_items[index] = loadLyric_items[index]+1 -- Found this source again so increment end obs.obs_data_release(settings) -- release memory end @@ -2336,8 +2340,8 @@ function rename_source() local name = "Load lyrics for: " .. song .. "" -- use index for compare -- Mark Duplicates if index ~= nil then - if loadLyric_items[index] == "*" then - name = '' .. name .. " * " + if loadLyric_items[index] > 1 then + name = '' .. name .. " " .. loadLyric_items[index] .. "" end if (c_name ~= name) then obs.obs_source_set_name(source, name) From c507866317778409733984b047a66d6a2be95c0b Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 10 Oct 2021 18:50:36 -0600 Subject: [PATCH 031/105] Update lyrics.lua The last of the data releases fixed the remaining memory leaks. --- lyrics.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lyrics.lua b/lyrics.lua index b3688d3..cb9d774 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2089,8 +2089,10 @@ function save_edits_clicked(props, p) prepared_songs[#prepared_songs+1] = itemName obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) end + obs.obs_data_release(item) end end + obs.obs_data_array_release(songNames) save_prepared() if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) From e938a95044099d67e55d4155fe21385a98d79806 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 11 Oct 2021 03:13:19 -0600 Subject: [PATCH 032/105] Update lyrics.lua Just a few more "qwerks" addressed in when text shows and not. Got a bit creative with retrieving the hot-key definitions if they have defiined them. --- lyrics.lua | 173 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 126 insertions(+), 47 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index cb9d774..587cd20 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -75,11 +75,20 @@ hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_n_key = "" +hotkey_p_key = "" +hotkey_c_key = "" +hotkey_n_p_key = "" +hotkey_p_p_key = "" +hotkey_home_key = "" +hotkey_reset_key = "" + -- script placeholders script_sets = nil script_props = nil source_sets = nil source_props = nil +hotkey_props = nil --monitor variables mon_song = "" @@ -290,19 +299,23 @@ function next_prepared(pressed) end if using_source then using_source = false + dbg_custom("do current prepared") prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song return end if prepared_index < #prepared_songs then using_source = false + dbg_custom("do next prepared") prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared return end if not source_active or using_source then using_source = false + dbg_custom("do first prepared") prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available else using_source = true + dbg_custom("do source prepared") prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source load_source_song(load_source, false) end @@ -485,9 +498,8 @@ function prepare_song_clicked(props, p) local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - -- prepare_song_by_index(#prepared_songs) - --end + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) + obs.obs_properties_apply_settings(props, script_sets) save_prepared() return true @@ -550,7 +562,10 @@ function refresh_directory() obs.obs_properties_apply_settings(script_props, script_sets) end + +-- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) + obs.obs_property_set_description(prop, "Prepared Songs (" .. #prepared_songs .. ")") dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false @@ -583,12 +598,16 @@ function prepare_selected(name) prepared_index = get_index_in_list(prepared_songs, name) else source_song_title = name + all_sources_fade = true end - all_sources_fade = true + -- if using source, then force show the new lyrics, even if lyrics were previously hidden if using_source then -- this keeps preapare selected from showing lyrics until hide/show transition_lyric_text(using_source) + else + set_text_visibility(TEXT_HIDDEN) end + else -- hide everything if unable to prepare song -- TODO: clear lyrics entirely after text is hidden @@ -653,6 +672,7 @@ end function apply_source_opacity() -- dbg_method("apply_source_visiblity") + local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -661,24 +681,41 @@ function apply_source_opacity() obs.obs_source_update(source, settings) end obs.obs_source_release(source) + obs.obs_data_release(settings) + + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local alt_source = obs.obs_get_source_by_name(alternate_source_name) if alt_source ~= nil then obs.obs_source_update(alt_source, settings) end obs.obs_source_release(alt_source) - if all_sources_fade and link_text then + obs.obs_data_release(settings) + dbg_bool("All Sources Fade:", all_sources_fade) + dbg_bool("Link Text:", link_text) + if all_sources_fade or link_text then + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then obs.obs_source_update(title_source, settings) end - obs.obs_source_release(title_source) + obs.obs_source_release(title_source) + obs.obs_data_release(settings) + + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local static_source = obs.obs_get_source_by_name(static_source_name) if static_source ~= nil then obs.obs_source_update(static_source, settings) end obs.obs_source_release(static_source) + obs.obs_data_release(settings) end - obs.obs_data_release(settings) + end function set_text_visibility(end_status) @@ -694,13 +731,15 @@ function set_text_visibility(end_status) elseif end_status == TEXT_VISIBLE then text_opacity = 100 text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden end if text_status == end_status then apply_source_opacity() update_source_text() + all_sources_fade = false return end - --if text_fade_enabled then + if text_fade_enabled then -- if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then text_status = TEXT_HIDING @@ -709,15 +748,16 @@ function set_text_visibility(end_status) end all_sources_fade = true start_fade_timer() - --end - update_source_text() + else + update_source_text() + end end -- transition to the next lyrics, use fade if enabled -- if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) dbg_method("transition_lyric_text") - dbg_bool("using_source", using_source) + dbg_bool("force show", force_show) -- update the lyrics display immediately on 2 conditions -- a) the text is hidden or hiding, and we will not force it to show -- b) text fade is not enabled @@ -736,8 +776,9 @@ function transition_lyric_text(force_show) set_text_visibility(TEXT_VISIBLE) update_source_text() dbg_inner("no text fade") - else + else -- initiate fade out/in text_status = TEXT_TRANSITION_OUT + --set_text_visibility(TEXT_VISIBLE) start_fade_timer() end dbg_bool("using_source", using_source) @@ -1419,11 +1460,10 @@ function update_source_text() title = prepared_songs[prepared_index] end else - dbg_custom("Load title from source") + dbg_custom("Load title from source: " .. source_song_title) title = source_song_title end end - local mtitle = title -- save title for use with monitor local source = obs.obs_get_source_by_name(source_name) local alt_source = obs.obs_get_source_by_name(alternate_source_name) @@ -1444,13 +1484,14 @@ function update_source_text() if link_text then if string.len(text) == 0 and string.len(alttext) == 0 then - static = "" - title = "" + --static = "" + --title = "" end end end -- update source texts if source ~= nil then + dbg_inner("Title Load") local settings = obs.obs_data_create() obs.obs_data_set_string(settings, "text", text) obs.obs_source_update(source, settings) @@ -1569,7 +1610,7 @@ function update_monitor() text .. "
" - if mon_song ~= "" and Mon_song ~= nil then + if mon_song ~= "" and mon_song ~= nil then text = text .. "" @@ -1688,7 +1729,7 @@ function script_properties() obs.obs_properties_add_button(script_props, "expand_all_button", "▲░ HIDE ALL GROUPS ░▲", expand_all_groups) ----------- obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) - gp = obs.obs_properties_create() + local gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) @@ -1708,23 +1749,22 @@ function script_properties() obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Songs by Meta Tags", filter_songs_clicked) - gps = obs.obs_properties_create() + local gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() - local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) --- prepare_props = prep_prop + local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prep_prop, name, name) + obs.obs_property_list_add_string(prepare_prop, name, name) end - obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) + obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared Songs List",edit_prepared_clicked) - eps = obs.obs_properties_create() - local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) - obs.obs_property_set_modified_callback(edit_prop, setEditVis) - obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) + local eps = obs.obs_properties_create() + local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) + obs.obs_property_set_modified_callback(edit_prop, setEditVis) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) @@ -1741,7 +1781,7 @@ function script_properties() obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") local link_prop = - obs.obs_properties_add_bool(gp, "link_text", "Only show title and static text with lyrics") + obs.obs_properties_add_bool(gp, "do_link_text", "Only show title and static text with lyrics") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") local transition_prop = @@ -1821,15 +1861,16 @@ function script_properties() obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) - gp = obs.obs_properties_create() - obs.obs_properties_add_button(gp, "prop_prev_button", "Previous Lyric", prev_button_clicked) - obs.obs_properties_add_button(gp, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(gp, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) - obs.obs_properties_add_button(gp, "prop_home_button", "Reset to Song Start", home_button_clicked) - obs.obs_properties_add_button(gp, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) - obs.obs_properties_add_button(gp, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button(gp,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) - obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,gp) + hotkey_props = obs.obs_properties_create() + obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) + obs.obs_properties_add_button(hotkey_props,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) + ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,hotkey_props) + obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) ----------------- if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) @@ -2015,7 +2056,8 @@ function setEditVis(props, prop, settings) -- hides edit group on initial showin obs.obs_property_set_visible(pp, false) pp = obs.obs_properties_get(props,"meta") obs.obs_property_set_visible(pp, false) - editVisSet = true -- do this only once + editVisSet = true + -- do this only once end end @@ -2153,20 +2195,21 @@ function script_update(settings) ensure_lines = cur_ensure_lines reload = true end - local cur_link_text = obs.obs_data_get_bool(settings, "link_text") - if cur_link_text ~= link_text then - link_text = cur_link_text - reload = true - end + link_text = obs.obs_data_get_bool(settings, "do_link_text") + dbg_bool("link Text and Title", link_text) + if reload then if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then prepare_selected(prepared_songs[prepared_index]) end end - + + name_hotkeys() end + + -- A function named script_defaults will be called to set the default settings function script_defaults(settings) dbg_method("script_defaults") @@ -2215,42 +2258,78 @@ function script_save(settings) obs.obs_data_array_release(hotkey_save_array) end + +function get_hotkeys(hotkey_array, prefix) + item = obs.obs_data_array_item(hotkey_array, 0) + local key = obs.obs_data_get_string(item,"key") + local ctrl = obs.obs_data_get_bool(item,"control") + local alt = obs.obs_data_get_bool(item,"alt") + local shft = obs.obs_data_get_bool(item,"shift") + obs.obs_data_release(item) + local val = prefix + if key ~= nil and key ~= "" then + val = val .. " [" + if ctrl then val = val.."Ctrl+" end + if alt then val = val.."Alt+" end + if shft then val = val.."Shft+" end + val = val .. string.sub(key, 9) .. "]" + end + return val +end + +function name_hotkeys() + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) +end + + -- a function named script_load will be called on startup function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) - local hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + hotkey_n_key = get_hotkeys(hotkey_save_array,"Previous Lyric") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") + hotkey_p_key = get_hotkeys(hotkey_save_array,"Next Lyric") obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") + hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics") obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") + hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Lyric") obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") + hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared") obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") + hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start") obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) - hotkey_reset_id = - obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") + hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared") obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) From 15110fd077a6f20257e0d09ae97781ab582f1325 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 11 Oct 2021 09:07:41 -0600 Subject: [PATCH 033/105] Update lyrics.lua Minor stuff again. Still trying to get prepared lyrics to act reasonable. --- lyrics.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 587cd20..baf2ebc 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -601,12 +601,8 @@ function prepare_selected(name) all_sources_fade = true end - -- if using source, then force show the new lyrics, even if lyrics were previously hidden - if using_source then -- this keeps preapare selected from showing lyrics until hide/show - transition_lyric_text(using_source) - else - set_text_visibility(TEXT_HIDDEN) - end + transition_lyric_text(using_source) + else -- hide everything if unable to prepare song @@ -2293,13 +2289,13 @@ function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - hotkey_n_key = get_hotkeys(hotkey_save_array,"Previous Lyric") + hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") - hotkey_p_key = get_hotkeys(hotkey_save_array,"Next Lyric") + hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric") obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) From f1548da4e5a416aba713aa16cc02699f6b0344c0 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 11 Oct 2021 09:38:30 -0600 Subject: [PATCH 034/105] Update lyrics.lua General cleanup --- lyrics.lua | 66 +++++++++++++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index baf2ebc..fda9924 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1764,6 +1764,18 @@ function script_properties() obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) +------------------ + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) + hotkey_props = obs.obs_properties_create() + obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) + obs.obs_properties_add_button(hotkey_props,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) + ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,hotkey_props) + obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) ------ obs.obs_properties_add_button(script_props, "options_showing", "▲░ HIDE DISPLAY OPTIONS ░▲",change_options_visible) gp = obs.obs_properties_create() @@ -1855,18 +1867,7 @@ function script_properties() obs.source_list_release(sources) obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) ------------------- - obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) - hotkey_props = obs.obs_properties_create() - obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button(hotkey_props,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) - ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,hotkey_props) - obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) + ----------------- if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) @@ -1923,8 +1924,8 @@ function expand_all_groups(props, prop, settings) end -function all_vis_equal() - return (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and +function all_vis_equal(props) + if (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) and @@ -1933,7 +1934,17 @@ function all_vis_equal() obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) or obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) or obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) + obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) then + dbg_inner("change expand") + expandcollapse = not expandcollapse + local mode1 = "▼░ SHOW " + local mode2 = "░▼" + if expandcollapse then + mode1 = "▲░ HIDE " + mode2 = "░▲" + end + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + end end function change_info_visible(props, prop, settings) @@ -1947,10 +1958,7 @@ function change_info_visible(props, prop, settings) mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end @@ -1965,10 +1973,7 @@ local mode1 = "▼░ SHOW " mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end @@ -1983,10 +1988,7 @@ local mode1 = "▼░ SHOW " mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end @@ -2001,10 +2003,7 @@ local mode1 = "▼░ SHOW " mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end @@ -2019,10 +2018,7 @@ local mode1 = "▼░ SHOW " mode2 = "░▲" end obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end From 53e7bfc3d6e21ae766e34efdced0d11933cc92d5 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 02:35:49 -0600 Subject: [PATCH 035/105] Update lyrics.lua Updates to Named Hotkeys --- lyrics.lua | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index fda9924..2384c75 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -13,7 +13,6 @@ -- limitations under the License. -- added delete single prepared song (WZ) - obs = obslua bit = require("bit") @@ -2252,19 +2251,30 @@ end function get_hotkeys(hotkey_array, prefix) + local Translate = {["NUMLOCK"] = "Num Lock", ["NUMSLASH"] = "Num /", ["NUMASTERISK"] = "Num *", + ["NUMMINUS"] = "Num -", ["NUMPLUS"] = "Num +", ["NUM1"] = "Num 1", ["NUM2"] = "Num 2", + ["NUM3"] = "Num 3", ["NUM4"] = "Num 4", ["NUM5"] = "Num 5", ["NUM6"] = "Num 6", + ["NUM7"] = "Num 7", ["NUM8"] = "Num 8", ["NUM9"] = "Num 9", ["NUM0"] = "Num 0", + ["NUMPERIOD"] = "Num Del", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page Down", + ["PAGEUP"] = "Page Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", + ["UP"] = "Up", ["DOWN"] = "Down", ["RIGHT"] = "Right", ["LEFT"] = "Left", + ["SCROLLLOCK"] = "Scroll Lock", ["BACKSPACE"] = "Backspace"} item = obs.obs_data_array_item(hotkey_array, 0) - local key = obs.obs_data_get_string(item,"key") + local key = string.sub(obs.obs_data_get_string(item,"key"),9) + if Translate[key] ~= nil then + key = Translate[key] + end local ctrl = obs.obs_data_get_bool(item,"control") local alt = obs.obs_data_get_bool(item,"alt") local shft = obs.obs_data_get_bool(item,"shift") obs.obs_data_release(item) local val = prefix if key ~= nil and key ~= "" then - val = val .. " [" - if ctrl then val = val.."Ctrl+" end - if alt then val = val.."Alt+" end - if shft then val = val.."Shft+" end - val = val .. string.sub(key, 9) .. "]" + val = val .. " ──► " + if ctrl then val = val.."Ctrl + " end + if alt then val = val.."Alt + " end + if shft then val = val.."Shft + " end + val = val .. key end return val end From 1344ffeeeb609b114e225ab7448cf13beb54b1a5 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 03:19:59 -0600 Subject: [PATCH 036/105] Update lyrics.lua Just taking out the trash. --- lyrics.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lyrics.lua b/lyrics.lua index 2384c75..0f33c31 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2092,7 +2092,6 @@ function edit_prepared_clicked(props, p) for i = 0, count-1 do local song = obs.obs_property_list_item_string(prop_prep_list, i) - print("song to move: " .. song) local array_obj = obs.obs_data_create() obs.obs_data_set_string(array_obj, "value", song) obs.obs_data_array_push_back(songNames,array_obj) From b313df2af055030424094d5f4804847c4b761301 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 10:45:53 -0600 Subject: [PATCH 037/105] Update lyrics.lua Based on obs-hotkey.c source code (untested), added support for decoding the "command" modifier hotkey on mac keyboards. --- lyrics.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lyrics.lua b/lyrics.lua index 0f33c31..905607d 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2266,6 +2266,7 @@ function get_hotkeys(hotkey_array, prefix) local ctrl = obs.obs_data_get_bool(item,"control") local alt = obs.obs_data_get_bool(item,"alt") local shft = obs.obs_data_get_bool(item,"shift") + local cmd = obs.obs_data_get_bool(item,"command") obs.obs_data_release(item) local val = prefix if key ~= nil and key ~= "" then @@ -2273,6 +2274,7 @@ function get_hotkeys(hotkey_array, prefix) if ctrl then val = val.."Ctrl + " end if alt then val = val.."Alt + " end if shft then val = val.."Shft + " end + if cmd then val = val.."Cmd + " end val = val .. key end return val From d77765716134e144cebc60fbba98d18c3cc0275b Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 11:05:14 -0600 Subject: [PATCH 038/105] Update lyrics.lua Based on OBS-Hotkey.C source More Hot-key translations for Mac Computers (Plus TAB, CAPSLOCK, PRTSCR, PAUSE and ScrollLock. --- lyrics.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 905607d..f723d2b 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2254,10 +2254,12 @@ function get_hotkeys(hotkey_array, prefix) ["NUMMINUS"] = "Num -", ["NUMPLUS"] = "Num +", ["NUM1"] = "Num 1", ["NUM2"] = "Num 2", ["NUM3"] = "Num 3", ["NUM4"] = "Num 4", ["NUM5"] = "Num 5", ["NUM6"] = "Num 6", ["NUM7"] = "Num 7", ["NUM8"] = "Num 8", ["NUM9"] = "Num 9", ["NUM0"] = "Num 0", - ["NUMPERIOD"] = "Num Del", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page Down", - ["PAGEUP"] = "Page Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", + ["NUMPERIOD"] = "Num Del", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page-Down", + ["PAGEUP"] = "Page-Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", ["UP"] = "Up", ["DOWN"] = "Down", ["RIGHT"] = "Right", ["LEFT"] = "Left", - ["SCROLLLOCK"] = "Scroll Lock", ["BACKSPACE"] = "Backspace"} + ["SCROLLLOCK"] = "Scroll-Lock", ["BACKSPACE"] = "Backspace", ["ESCAPE"] = "Esc", + ["MENU"] = "Menu", ["META"] = "Meta", ["PRINT"] = "Prt", ["TAB"] = "Tab", + ["DELETE"] = "Del", ["CAPSLOCK"] = Caps-Lock, ["NUMEQUAL"] = "Num =", ["PAUSE"] = "Pause"} item = obs.obs_data_array_item(hotkey_array, 0) local key = string.sub(obs.obs_data_get_string(item,"key"),9) if Translate[key] ~= nil then From 40e433df12c08b5b82a1095218d50bfeed763cff Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 13:37:20 -0600 Subject: [PATCH 039/105] Update lyrics.lua Based on Expanded Keyboard. Added some keys OBS does not currently translate. Made some additional UI changes --- lyrics.lua | 93 ++++++++++++++++++++++++------------------------------ 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index f723d2b..131ebc5 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -564,7 +564,7 @@ end -- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) - obs.obs_property_set_description(prop, "Prepared Songs (" .. #prepared_songs .. ")") + obs.obs_property_set_description(obs.obs_properties_get(props, "prep_grp"), " Prepared Songs (" .. #prepared_songs .. ")") dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false @@ -1725,9 +1725,9 @@ function script_properties() ----------- obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) local gp = obs.obs_properties_create() - obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(gp, "prop_edit_song_title", "\tSong Title (Filename)", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) - obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "\tSong Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) obs.obs_properties_add_button(gp, "prop_opensong_button","Edit Song with System Editor", open_song_clicked) @@ -1736,7 +1736,7 @@ function script_properties() ------------ obs.obs_properties_add_button(script_props, "prepared_showing", "▲░ HIDE PREPARED SONGS ░▲",change_prepared_visible) gp = obs.obs_properties_create() - local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","\tSong Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(prop_dir_list, name, name) @@ -1749,7 +1749,7 @@ function script_properties() obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() - local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) + local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","\tPrepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end @@ -1757,15 +1757,16 @@ function script_properties() obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared Songs List",edit_prepared_clicked) local eps = obs.obs_properties_create() - local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) + local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "\tPrepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) obs.obs_property_set_modified_callback(edit_prop, setEditVis) obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) - obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) - obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) - obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) + obs.obs_properties_add_group(gps,"edit_grp","\tEdit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) + obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) hotkey_props = obs.obs_properties_create() + obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) @@ -1773,12 +1774,12 @@ function script_properties() obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) obs.obs_properties_add_button(hotkey_props,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) - ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,hotkey_props) + ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)", obs.OBS_GROUP_NORMAL,hotkey_props) obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) ------ obs.obs_properties_add_button(script_props, "options_showing", "▲░ HIDE DISPLAY OPTIONS ░▲",change_options_visible) gp = obs.obs_properties_create() - local lines_prop = obs.obs_properties_add_int(gp, "prop_lines_counter", "Lines to Display", 1, 100, 1) + local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "\tLines to Display", 1, 50, 1) obs.obs_property_set_long_description( lines_prop, "Sets default lines per page of lyric, overwritten by Markup: #L:n" @@ -1801,7 +1802,7 @@ function script_properties() local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + obs.obs_properties_add_int_slider(gp, "text_fade_speed", "\tFade Speed", 1, 10, 1) obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) ------------- obs.obs_properties_add_button(script_props, "src_showing", "▲░ HIDE SOURCE TEXT SELECTIONS ░▲",change_src_visible) @@ -1810,38 +1811,35 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_source_list", - "Text Source", + "\tText Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(source_prop, "Shows main lyric text") local title_source_prop = obs.obs_properties_add_list( gp, "prop_title_list", - "Title Source", + "\tTitle Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(title_source_prop, "Shows text from song title") local alternate_source_prop = obs.obs_properties_add_list( gp, "prop_alternate_list", - "Alternate Source", + "\tAlternate Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(alternate_source_prop, "Shows text annotated with #A[ and #A]") local static_source_prop = obs.obs_properties_add_list( gp, "prop_static_list", - "Static Source", + "\tStatic Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(static_source_prop, "Shows text annotated with #S[ and #S]") + local sources = obs.obs_enum_sources() if sources ~= nil then local n = {} @@ -1865,14 +1863,12 @@ function script_properties() end obs.source_list_release(sources) obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) - obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) + obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes\t", obs.OBS_GROUP_NORMAL,gp) ----------------- if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) end - pp = obs.obs_properties_get(script_props,"ctrl_grp") - obs.obs_property_set_visible(pp, true) obs.obs_properties_apply_settings(script_props, script_sets) @@ -1882,20 +1878,6 @@ end -- A function named script_description returns the description shown to -- the user -local description = [[ -"Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from taxilian.
-
Song
Title
-
- - - - - - - -
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
Optional comma delimeted meta tags following //meta on 1st line
-]] - function script_description() return "Manage song Lyrics and Other Paged Text (Version: Nov 2021)
Authors: Amirchev & DC Strato; with significant contributions from Taxilian.
" end @@ -2023,6 +2005,8 @@ end function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") + dbg_bool("Fade: ",text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) return true @@ -2046,7 +2030,7 @@ function setEditVis(props, prop, settings) -- hides edit group on initial showin local pp = obs.obs_properties_get(script_props,"edit_grp") obs.obs_property_set_visible(pp, false) pp = obs.obs_properties_get(props,"meta") - obs.obs_property_set_visible(pp, false) + obs.obs_property_set_visible(pp, false) editVisSet = true -- do this only once end @@ -2249,22 +2233,29 @@ function script_save(settings) end -function get_hotkeys(hotkey_array, prefix) +function get_hotkeys(hotkey_array, prefix, leader) local Translate = {["NUMLOCK"] = "Num Lock", ["NUMSLASH"] = "Num /", ["NUMASTERISK"] = "Num *", - ["NUMMINUS"] = "Num -", ["NUMPLUS"] = "Num +", ["NUM1"] = "Num 1", ["NUM2"] = "Num 2", - ["NUM3"] = "Num 3", ["NUM4"] = "Num 4", ["NUM5"] = "Num 5", ["NUM6"] = "Num 6", - ["NUM7"] = "Num 7", ["NUM8"] = "Num 8", ["NUM9"] = "Num 9", ["NUM0"] = "Num 0", + ["NUMMINUS"] = "Num -", ["NUMPLUS"] = "Num +", ["NUMPERIOD"] = "Num Del", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page-Down", ["PAGEUP"] = "Page-Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", ["UP"] = "Up", ["DOWN"] = "Down", ["RIGHT"] = "Right", ["LEFT"] = "Left", ["SCROLLLOCK"] = "Scroll-Lock", ["BACKSPACE"] = "Backspace", ["ESCAPE"] = "Esc", ["MENU"] = "Menu", ["META"] = "Meta", ["PRINT"] = "Prt", ["TAB"] = "Tab", - ["DELETE"] = "Del", ["CAPSLOCK"] = Caps-Lock, ["NUMEQUAL"] = "Num =", ["PAUSE"] = "Pause"} + ["DELETE"] = "Del", ["CAPSLOCK"] = "Caps-Lock", ["NUMEQUAL"] = "Num =", ["PAUSE"] = "Pause", + ["VK_VOLUME_MUTE"] = "Vol Mute", ["VK_VOLUME_DOWN"] = "Vol Dwn", ["VK_VOLUME_UP"] = "Vol Up", + ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", ["VK_MEDIA_STOP"] = "Media Stop", + ["VK_MEDIA_PREV_TRACK"] = "Media Prev", ["VK_MEDIA_NEXT_TRACK"] = "Media Next"} + item = obs.obs_data_array_item(hotkey_array, 0) local key = string.sub(obs.obs_data_get_string(item,"key"),9) if Translate[key] ~= nil then key = Translate[key] + elseif string.sub(key,1,3) == "NUM" then + key = "Num " .. string.sub(key,4) + elseif string.sub(key,1,5) == "MOUSE" then + key = "Mouse " .. string.sub(key,6) end + local ctrl = obs.obs_data_get_bool(item,"control") local alt = obs.obs_data_get_bool(item,"alt") local shft = obs.obs_data_get_bool(item,"shift") @@ -2272,7 +2263,7 @@ function get_hotkeys(hotkey_array, prefix) obs.obs_data_release(item) local val = prefix if key ~= nil and key ~= "" then - val = val .. " ──► " + val = val .. " " .. leader .. " " if ctrl then val = val.."Ctrl + " end if alt then val = val.."Alt + " end if shft then val = val.."Shft + " end @@ -2298,43 +2289,43 @@ function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric") + hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric", " ......................") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") - hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric") + hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric", " ..................") obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") - hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics") + hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics", " ..............") obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") - hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Lyric") + hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Prepared", " ................") obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") - hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared") + hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared", "............") obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") - hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start") + hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start", " ..........") obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") - hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared") + hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared", " .......") obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) From d209af50c74ccd45b3d05ec61df4dca14be88b83 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 14:09:21 -0600 Subject: [PATCH 040/105] Update lyrics.lua Just some UI refactoring to optimize Hide/Show buttons --- lyrics.lua | 78 +++++++++++++++++------------------------------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 131ebc5..d17c067 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1703,7 +1703,7 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself -local help = "░░░░░░░░░░░░░░░ MARKUP SYNTAX HELP ░░░░░░░░░░░░░░░\n\n" .. +local help = "============ MARKUP SYNTAX HELP ============\n\n" .. "Markup      Syntax        Markup       Syntax\n" .. "==========  ==========    ==========  ==========\n" .. " Display n Lines    #L:n      End Page after Line   Line ###\n" .. @@ -1715,15 +1715,15 @@ local help = "░░░░░░░░░░░░░░░ MARKUP SYNTAX HELP "Comment Line    // Line       Block Comments    //[ and //] \n" .. "Mark Verses     ##V        Override Title     #T: text\n\n" .. "Optional comma delimited meta tags follow '//meta ' on 1st line\n\n" .. - "▲░░░░░░ CLICK TO CLOSE ░░░░░░▲" + "▲-------- CLICK TO CLOSE --------▲" function script_properties() dbg_method("script_properties") editVisSet = false script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "expand_all_button", "▲░ HIDE ALL GROUPS ░▲", expand_all_groups) + obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) ----------- - obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) + obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲",change_info_visible) local gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "\tSong Title (Filename)", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) @@ -1734,7 +1734,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------ - obs.obs_properties_add_button(script_props, "prepared_showing", "▲░ HIDE PREPARED SONGS ░▲",change_prepared_visible) + obs.obs_properties_add_button(script_props, "prepared_showing", "▲- HIDE PREPARED SONGS -▲",change_prepared_visible) gp = obs.obs_properties_create() local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","\tSong Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) @@ -1764,7 +1764,7 @@ function script_properties() obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) ------------------ - obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲",change_ctrl_visible) hotkey_props = obs.obs_properties_create() obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) @@ -1777,7 +1777,7 @@ function script_properties() ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)", obs.OBS_GROUP_NORMAL,hotkey_props) obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) ------ - obs.obs_properties_add_button(script_props, "options_showing", "▲░ HIDE DISPLAY OPTIONS ░▲",change_options_visible) + obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲",change_options_visible) gp = obs.obs_properties_create() local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "\tLines to Display", 1, 50, 1) obs.obs_property_set_long_description( @@ -1881,20 +1881,19 @@ end function script_description() return "Manage song Lyrics and Other Paged Text (Version: Nov 2021)
Authors: Amirchev & DC Strato; with significant contributions from Taxilian.
" end + +function vMode(vis) + return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" +end function expand_all_groups(props, prop, settings) expandcollapse = not expandcollapse obs.obs_property_set_visible(obs.obs_properties_get(script_props,"info_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"prep_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"mng_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"disp_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"src_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"ctrl_grp"), expandcollapse) - local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if expandcollapse then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(expandecollapse) obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) @@ -1905,6 +1904,8 @@ function expand_all_groups(props, prop, settings) end + + function all_vis_equal(props) if (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) and @@ -1912,47 +1913,31 @@ function all_vis_equal(props) obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) or not (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"mng_grp")) or obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) or obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) or obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) then - dbg_inner("change expand") expandcollapse = not expandcollapse - local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if expandcollapse then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(expandecollapse) obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - end + end end function change_info_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"info_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) - local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) all_vis_equal(props) return true end function change_prepared_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"prep_grp") + local pp = obs.obs_properties_get(script_props,"mng_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) all_vis_equal(props) return true @@ -1962,12 +1947,7 @@ function change_options_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"disp_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) all_vis_equal(props) return true @@ -1977,12 +1957,7 @@ function change_src_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"src_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) all_vis_equal(props) return true @@ -1992,12 +1967,7 @@ function change_ctrl_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"ctrl_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) all_vis_equal(props) return true From 72a47bb6870f047bcd95916d70f0b2e5c30652c3 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 14:33:50 -0600 Subject: [PATCH 041/105] Update lyrics.lua More cleanup - just saving timer_exists flag is not required --- lyrics.lua | 54 ++++++++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index d17c067..7c18efb 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1,4 +1,4 @@ ---- Copyright 2020 amirchev +--- Copyright 2020 amirchev/dcstrato -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- added delete single prepared song (WZ) + obs = obslua bit = require("bit") @@ -28,33 +28,29 @@ source_name = "" alternate_source_name = "" static_source_name = "" static_text = "" --- current_scene = "" --- preview_scene = "" title_source_name = "" -- settings windows_os = false first_open = true --- in_timer = false --- in_Load = false --- in_directory = false --- pause_timer = false + display_lines = 0 ensure_lines = true --- visible = false --- lyrics status --- TODO: removed displayed_song and use prepared_songs[prepared_index] --- displayed_song = "" + +-- lyrics/alternate lyrics by page lyrics = {} +alternate = {} + +-- verse indicies if marked verses = {} --- refrain = {} -alternate = {} -page_index = 0 +page_index = 0 -- current page of lyrics being displayed prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected -song_directory = {} -prepared_songs = {} + +song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) +prepared_songs = {} -- holds pre-prepared list of songs to use + link_text = false -- true if Title and Static should fade with text only during hide/show all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide source_song_title = "" -- The song title from a source loaded song @@ -62,8 +58,6 @@ using_source = false -- true when a lyric load song is being used instead of a p source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) load_scene = "" -- name of scene loading a lyric with a source -timer_exists = false ---forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility -- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID @@ -780,17 +774,13 @@ function transition_lyric_text(force_show) end function start_fade_timer() - if not timer_exists then - timer_exists = true obs.timer_add(fade_callback, 50) dbg_inner("started fade timer") - end end function fade_callback() -- if not in a transitory state, exit callback if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then - timer_exists = false obs.remove_current_callback() all_sources_fade = false end @@ -1805,7 +1795,7 @@ function script_properties() obs.obs_properties_add_int_slider(gp, "text_fade_speed", "\tFade Speed", 1, 10, 1) obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) ------------- - obs.obs_properties_add_button(script_props, "src_showing", "▲░ HIDE SOURCE TEXT SELECTIONS ░▲",change_src_visible) + obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲",change_src_visible) gp = obs.obs_properties_create() local source_prop = obs.obs_properties_add_list( @@ -1862,7 +1852,7 @@ function script_properties() end end obs.source_list_release(sources) - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Text Sources", refresh_button_clicked) obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes\t", obs.OBS_GROUP_NORMAL,gp) ----------------- @@ -2225,19 +2215,15 @@ function get_hotkeys(hotkey_array, prefix, leader) elseif string.sub(key,1,5) == "MOUSE" then key = "Mouse " .. string.sub(key,6) end - - local ctrl = obs.obs_data_get_bool(item,"control") - local alt = obs.obs_data_get_bool(item,"alt") - local shft = obs.obs_data_get_bool(item,"shift") - local cmd = obs.obs_data_get_bool(item,"command") + obs.obs_data_release(item) local val = prefix if key ~= nil and key ~= "" then val = val .. " " .. leader .. " " - if ctrl then val = val.."Ctrl + " end - if alt then val = val.."Alt + " end - if shft then val = val.."Shft + " end - if cmd then val = val.."Cmd + " end + if obs.obs_data_get_bool(item,"control") then val = val.."Ctrl + " end + if obs.obs_data_get_bool(item,"alt") then val = val.."Alt + " end + if obs.obs_data_get_bool(item,"shift") then val = val.."Shift + " end + if obs.obs_data_get_bool(item,"command") then val = val.."Cmd + " end val = val .. key end return val From 54167cc0fd2b364a5ab4e6ecc3c84a0fd3121087 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 17:44:22 -0600 Subject: [PATCH 042/105] Update lyrics.lua Now I am just showing off... :) --- lyrics.lua | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 7c18efb..b54da2c 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -1,4 +1,4 @@ ---- Copyright 2020 amirchev/dcstrato +--- Copyright 2020 amirchev/wzaggle -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -117,11 +117,16 @@ source_saved = false -- ick... A saved toggle to keep from repeating the sa editVisSet = false -- simple debugging/print mechanism -DEBUG = true -- on/off switch for entire debugging mechanism +DEBUG = true -- on switch for entire debugging mechanism +--DEBUG = false -- on switch for entire debugging mechanism DEBUG_METHODS = true -- print method names -DEBUG_INNER = true -- print inner method breakpoints -DEBUG_CUSTOM = true -- print custom debugging messages -DEBUG_BOOL = true -- print message with bool state true/false +--DEBUG_METHODS = false -- print method names +--DEBUG_INNER = true -- print inner method breakpoints +DEBUG_INNER = false -- print inner method breakpoints +--DEBUG_CUSTOM = true -- print custom debugging messages +DEBUG_CUSTOM = false -- print custom debugging messages +--DEBUG_BOOL = true -- print message with bool state true/false +DEBUG_BOOL = false -- print message with bool state true/false -------- ---------------- @@ -1735,9 +1740,9 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Songs by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(gps, "prop_edit_metatags", "\tFilter MetaTags", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(gp, "meta", "\t Filter Songs", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","\tPrepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) for _, name in ipairs(prepared_songs) do @@ -1869,7 +1874,7 @@ end -- the user function script_description() - return "Manage song Lyrics and Other Paged Text (Version: Nov 2021)
Authors: Amirchev & DC Strato; with significant contributions from Taxilian.
" + return description end function vMode(vis) @@ -2598,5 +2603,8 @@ function dbg_bool(name, value) end end - obs.obs_register_source(source_def) + +description = [[ +

OBS Lyrics+ Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
+]] \ No newline at end of file From 31d4fd7bf502c7d6f37790ca97f5829be333356c Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 17:57:46 -0600 Subject: [PATCH 043/105] Update lyrics.lua Slightly smaller logo (Height 70px) --- lyrics.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyrics.lua b/lyrics.lua index b54da2c..ed11123 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2606,5 +2606,5 @@ end obs.obs_register_source(source_def) description = [[ -

OBS Lyrics+ Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
+

OBS Lyrics+ Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
]] \ No newline at end of file From 776626bb1a45d4c6ecfc530c6a374b96d47b64f7 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 18:00:23 -0600 Subject: [PATCH 044/105] Update lyrics.lua Logo Height of 73px seems to look best --- lyrics.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyrics.lua b/lyrics.lua index ed11123..48da4dd 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -2606,5 +2606,5 @@ end obs.obs_register_source(source_def) description = [[ -

OBS Lyrics+ Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
+

OBS Lyrics+ Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
]] \ No newline at end of file From aaf15d18363ddfdb363f088563e1a9454392abec Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 19:02:19 -0600 Subject: [PATCH 045/105] Update lyrics.lua Fade Changes for Show/HIde --- lyrics.lua | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 48da4dd..b160d71 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -718,21 +718,6 @@ function set_text_visibility(end_status) if text_status == end_status then return end - -- change visibility immediately (fade or no fade) - if end_status == TEXT_HIDDEN then - text_opacity = 0 - text_status = end_status - elseif end_status == TEXT_VISIBLE then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - end - if text_status == end_status then - apply_source_opacity() - update_source_text() - all_sources_fade = false - return - end if text_fade_enabled then -- if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then @@ -742,8 +727,19 @@ function set_text_visibility(end_status) end all_sources_fade = true start_fade_timer() - else - update_source_text() + else -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + end + apply_source_opacity() + update_source_text() + all_sources_fade = false + return end end From ec6966a2f5c3df2dec469ffa1d290d235cee2f73 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 19:33:49 -0600 Subject: [PATCH 046/105] Update lyrics.lua Smaller Help Button Text --- lyrics.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index b160d71..627eef1 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -599,7 +599,7 @@ function prepare_selected(name) all_sources_fade = true end - transition_lyric_text(using_source) + transition_lyric_text(using_source) else @@ -1694,9 +1694,9 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself -local help = "============ MARKUP SYNTAX HELP ============\n\n" .. - "Markup      Syntax        Markup       Syntax\n" .. - "==========  ==========    ==========  ==========\n" .. +local help = "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. + " Markup      Syntax         Markup      Syntax \n" .. + "============ ==========   ============ ==========\n" .. " Display n Lines    #L:n      End Page after Line   Line ###\n" .. " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. @@ -1705,8 +1705,8 @@ local help = "============ MARKUP SYNTAX HELP ============\n\n" .. "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. "Comment Line    // Line       Block Comments    //[ and //] \n" .. "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line\n\n" .. - "▲-------- CLICK TO CLOSE --------▲" + "Optional comma delimited meta tags follow '//meta ' on 1st line" + function script_properties() dbg_method("script_properties") From e240e2b6e0d19de4cca441c653c974e833fbc246 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 19:45:10 -0600 Subject: [PATCH 047/105] Update lyrics.lua Show/Hide now honoring Linking text vis to static/title vis --- lyrics.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 627eef1..d35d1ae 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -324,6 +324,9 @@ function toggle_lyrics_visibility(pressed) if not pressed then return end + if link_text then + all_sources_fade = true + end if text_status ~= TEXT_HIDDEN then dbg_inner("hiding") set_text_visibility(TEXT_HIDDEN) @@ -688,7 +691,7 @@ function apply_source_opacity() obs.obs_data_release(settings) dbg_bool("All Sources Fade:", all_sources_fade) dbg_bool("Link Text:", link_text) - if all_sources_fade or link_text then + if all_sources_fade then local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -725,7 +728,7 @@ function set_text_visibility(end_status) elseif end_status == TEXT_VISIBLE then text_status = TEXT_SHOWING end - all_sources_fade = true + --all_sources_fade = true start_fade_timer() else -- change visibility immediately (fade or no fade) if end_status == TEXT_HIDDEN then From e391221b9c1d4a961449a05a227a50a4622801eb Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 12 Oct 2021 23:33:27 -0600 Subject: [PATCH 048/105] Update lyrics.lua Moved some methods around to follow them better. Not that many changes. Added two more status to text. TEXT_HIDE and TEXT_SHOW Only using TEXT_HIDE for now. but both instantly change the text visibility and ignore the fade if set. All the other end states honor fades. When transitioning via a source that has loaded the text in preview, it needs to instantly turn off the text before the transition so it can play nice and fade in again. Okay back to documenting. --- lyrics.lua | 568 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 317 insertions(+), 251 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index d35d1ae..53417af 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -58,6 +58,7 @@ using_source = false -- true when a lyric load song is being used instead of a p source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) load_scene = "" -- name of scene loading a lyric with a source +last_prepared_song = "" -- name of the last prepared song (prevents duplicate loading of already loaded song) -- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID @@ -101,6 +102,9 @@ TEXT_SHOWING = 3 -- going from hidden -> visible TEXT_HIDING = 4 -- going from visible -> hidden TEXT_TRANSITION_OUT = 5 -- fade out transition to next lyric TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change +TEXT_HIDE = 7 -- turn off the text and ignore fade if selected +TEXT_SHOW = 8 -- turn on the text and ignore fade if selected + text_status = TEXT_VISIBLE text_opacity = 100 text_fade_speed = 1 @@ -118,109 +122,17 @@ editVisSet = false -- simple debugging/print mechanism DEBUG = true -- on switch for entire debugging mechanism ---DEBUG = false -- on switch for entire debugging mechanism -DEBUG_METHODS = true -- print method names ---DEBUG_METHODS = false -- print method names +--DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints -DEBUG_INNER = false -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages -DEBUG_CUSTOM = false -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false -DEBUG_BOOL = false -- print message with bool state true/false -------- ---------------- ------------------------ CALLBACKS ---------------- -------- -function anythingShowing() - return sourceShowing() or alternateShowing() or titleShowing() or staticShowing() -end -function sourceShowing() - local source = obs.obs_get_source_by_name(source_name) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function alternateShowing() - local source = obs.obs_get_source_by_name(alternate_source_name) - local showing = false - if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function titleShowing() - local source = obs.obs_get_source_by_name(title_source_name) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function staticShowing() - local source = obs.obs_get_source_by_name(static_source_name) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function anythingActive() - return sourceActive() or alternateActive() or titleActive() or staticActive() -end - -function sourceActive() - local source = obs.obs_get_source_by_name(source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end - -function alternateActive() - local source = obs.obs_get_source_by_name(alternate_source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end - -function titleActive() - local source = obs.obs_get_source_by_name(title_source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end - -function staticActive() - local source = obs.obs_get_source_by_name(static_source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end function next_lyric(pressed) if not pressed then @@ -721,7 +633,19 @@ function set_text_visibility(end_status) if text_status == end_status then return end - if text_fade_enabled then + if end_status == TEXT_HIDE then + text_opacity = 0 + text_status = end_status + apply_source_opacity() + return + elseif end_status == TEXT_SHOW then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + apply_source_opacity() + return + end + if text_fade_enabled then -- if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then text_status = TEXT_HIDING @@ -740,7 +664,7 @@ function set_text_visibility(end_status) all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden end apply_source_opacity() - update_source_text() + --update_source_text() all_sources_fade = false return end @@ -749,7 +673,7 @@ end -- transition to the next lyrics, use fade if enabled -- if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) - dbg_method("transition_lyric_text") + dbgsp("transition_lyric_text") dbg_bool("force show", force_show) -- update the lyrics display immediately on 2 conditions -- a) the text is hidden or hiding, and we will not force it to show @@ -764,22 +688,145 @@ function transition_lyric_text(force_show) end dbg_inner("hidden") elseif not text_fade_enabled then + dbg_custom("Instant On") -- if text fade is not enabled, then we can cancel the all_sources_fade all_sources_fade = false - set_text_visibility(TEXT_VISIBLE) + set_text_visibility(TEXT_VISIBLE) -- does update_source_text() update_source_text() dbg_inner("no text fade") else -- initiate fade out/in + dbg_custom("Transition Timer") text_status = TEXT_TRANSITION_OUT - --set_text_visibility(TEXT_VISIBLE) start_fade_timer() end dbg_bool("using_source", using_source) end +-- updates the selected lyrics +function update_source_text() + dbg_method("update_source_text") + dbg_custom("Page Index: " .. page_index) + dbg_traceback() + local text = "" + local alttext = "" + local next_lyric = "" + local next_alternate = "" + local static = static_text + local mstatic = static -- save static for use with monitor + local title = "" + + + if alt_title ~= "" then + title = alt_title + else + if not using_source then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Update from prepared: " .. prepared_index) + title = prepared_songs[prepared_index] + end + else + dbg_custom("Updatefrom source: " .. source_song_title) + title = source_song_title + end + end + + local source = obs.obs_get_source_by_name(source_name) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) + + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then + if #lyrics > 0 then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] + end + end + if #alternate > 0 then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] + end + end + + if link_text then + if string.len(text) == 0 and string.len(alttext) == 0 then + --static = "" + --title = "" + end + end + end + -- update source texts + if source ~= nil then + dbg_inner("Title Load") + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + next_lyric = lyrics[page_index + 1] + if (next_lyric == nil) then + next_lyric = "" + end + end + if alt_source ~= nil then + local settings = obs.obs_data_create() -- setup TEXT settings with opacity values + obs.obs_data_set_string(settings, "text", alttext) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" + end + end + if stat_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) + end + if title_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) + end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) + obs.obs_source_release(title_source) + + local next_prepared = "" + if using_source then + next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song + elseif prepared_index < #prepared_songs then + next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song + else + if source_active then + next_prepared = source_song_title -- plan to go back to source loaded song + else + next_prepared = prepared_songs[1] -- plan to loop around to first prepared song + end + end + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i]+1 then + mon_verse = i + end + end -- v = current verse number for this page + end + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared + + update_monitor() +end + function start_fade_timer() + dbgsp("started fade timer") obs.timer_add(fade_callback, 50) - dbg_inner("started fade timer") end function fade_callback() @@ -834,6 +881,7 @@ function prepare_song_by_name(name) if name == nil then return false end + last_prepared_song = name -- if using transition on lyric change, first transition -- would be reset with new song prepared transition_completed = false @@ -1427,127 +1475,6 @@ function save_prepared() end --- updates the selected lyrics -function update_source_text() - dbg_method("update_source_text") - dbg_inner("Page Index: " .. page_index) - local text = "" - local alttext = "" - local next_lyric = "" - local next_alternate = "" - local static = static_text - local mstatic = static -- save static for use with monitor - local title = "" - - - if alt_title ~= "" then - title = alt_title - else - if not using_source then - if prepared_index ~= nil and prepared_index ~= 0 then - dbg_custom("Load title from prepared: " .. prepared_index) - title = prepared_songs[prepared_index] - end - else - dbg_custom("Load title from source: " .. source_song_title) - title = source_song_title - end - end - - local source = obs.obs_get_source_by_name(source_name) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - local stat_source = obs.obs_get_source_by_name(static_source_name) - local title_source = obs.obs_get_source_by_name(title_source_name) - - if using_source or (prepared_index ~= nil and prepared_index ~= 0) then - if #lyrics > 0 then - if lyrics[page_index] ~= nil then - text = lyrics[page_index] - end - end - if #alternate > 0 then - if alternate[page_index] ~= nil then - alttext = alternate[page_index] - end - end - - if link_text then - if string.len(text) == 0 and string.len(alttext) == 0 then - --static = "" - --title = "" - end - end - end - -- update source texts - if source ~= nil then - dbg_inner("Title Load") - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", text) - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) - next_lyric = lyrics[page_index + 1] - if (next_lyric == nil) then - next_lyric = "" - end - end - if alt_source ~= nil then - local settings = obs.obs_data_create() -- setup TEXT settings with opacity values - obs.obs_data_set_string(settings, "text", alttext) - obs.obs_source_update(alt_source, settings) - obs.obs_data_release(settings) - next_alternate = alternate[page_index + 1] - if (next_alternate == nil) then - next_alternate = "" - end - end - if stat_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", static) - obs.obs_source_update(stat_source, settings) - obs.obs_data_release(settings) - end - if title_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", title) - obs.obs_source_update(title_source, settings) - obs.obs_data_release(settings) - end - -- release source references - obs.obs_source_release(source) - obs.obs_source_release(alt_source) - obs.obs_source_release(stat_source) - obs.obs_source_release(title_source) - - local next_prepared = "" - if using_source then - next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song - elseif prepared_index < #prepared_songs then - next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song - else - if source_active then - next_prepared = source_song_title -- plan to go back to source loaded song - else - next_prepared = prepared_songs[1] -- plan to loop around to first prepared song - end - end - mon_verse = 0 - if #verses ~= nil then --find valid page Index - for i = 1, #verses do - if page_index >= verses[i]+1 then - mon_verse = i - end - end -- v = current verse number for this page - end - mon_song = title - mon_lyric = text:gsub("\n", "
• ") - mon_nextlyric = next_lyric:gsub("\n", "
• ") - mon_alt = alttext:gsub("\n", "
• ") - mon_nextalt = next_alternate:gsub("\n", "
• ") - mon_nextsong = next_prepared - - update_monitor() -end - function update_monitor() dbg_method("update_monitor") @@ -1996,7 +1923,6 @@ function setEditVis(props, prop, settings) -- hides edit group on initial showin pp = obs.obs_properties_get(props,"meta") obs.obs_property_set_visible(pp, false) editVisSet = true - -- do this only once end end @@ -2195,7 +2121,88 @@ function script_save(settings) obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) end +--- +------ +--------- Source Showing or Source Active Helper Functions +--------- Return true if sourcename given is showing anywhere or on in the Active scene +------ +--- +function isShowing(sourceName) + local source = obs.obs_get_source_by_name(sourceName) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function isActive(sourceName) + local source = obs.obs_get_source_by_name(sourceName) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function anythingShowing() + return isShowing(source_name) or isShowing(alternate_source_name) + or isShowing(title_source_name) or isShowing(static_source_name) +end + +function sourceShowing() + return isShowing(source_name) +end + +function alternateShowing() + return isShowing(alternate_source_name) +end + +function titleShowing() + return isShowing(title_source_name) +end + +function staticShowing() + return isShowing(static_source_name) +end + +function anythingActive() + return isActive(source_name) or isActive(alternate_source_name) + or isActive(title_source_name) or isActive(static_source_name) +end + +function sourceActive() + return isActive(source_name) +end + +function alternateActive() + return isActive(alternate_source_name) +end + +function titleActive() + return isActive(title_source_name) +end + +function staticActive() + return isActive(static_source_name) +end + +--- +------ +--------- Initialization Functions +--------- Manages defined Hotkey Save, Load, Translate and Button rename +--------- Loads inital song directory and any previously prepared lyrics +------ +--- +---------------------------------------------------------------------------------------------------------- +-- get_hotkeys(loaded hotkey array, desired prefix text, leader text (between prefix and hotkey label) +-- Returns translated hotkey text label with prefix and leader +-- e.g. if HotKeyArray contains an assigned hotkey Shift and F1 key combo, then +-- get_hotkeys(HotKeyArray," ....... ", "HotKey") returns "Hotkey ....... Shift + F1" +---------------------------------------------------------------------------------------------------------- function get_hotkeys(hotkey_array, prefix, leader) local Translate = {["NUMLOCK"] = "Num Lock", ["NUMSLASH"] = "Num /", ["NUMASTERISK"] = "Num *", @@ -2233,6 +2240,8 @@ function get_hotkeys(hotkey_array, prefix, leader) return val end +-- name_hotkeys function renames the seven hotkeys to include their defined key text +-- function name_hotkeys() obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) @@ -2244,7 +2253,9 @@ function name_hotkeys() end --- a function named script_load will be called on startup +-- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS +-- sets callback to obs_frontend Event Callback +-- function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) @@ -2304,12 +2315,7 @@ function script_load(settings) --prepared_index = 1 file:close() end - local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") - local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") - local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") - obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) - obs.obs_property_set_enabled(fade_speed_prop, not transition_set) - transition_enabled = transition_set + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end @@ -2319,7 +2325,8 @@ end ---------------- -------- --- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) +-- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change +-- function rename_source() -- pause_timer = true local sources = obs.obs_enum_sources() @@ -2394,29 +2401,39 @@ function rename_source() -- pause_timer = false end +-- Names the initial "Prepare Lyric" source (prior to being renamed to "Load Lyrics for: {song name} +-- source_def.get_name = function() return "Prepare Lyric" end +-- Called when OBS is saving data. This will be called on each copy of Load Lyric source +-- Used to initiate rename_source() function when the source dialog closes +-- saved flag prevents it from being called by every source each time. +-- source_def.save = function(data, settings) - - if saved then return end -- obs calls save for every load source in all scenes and we only need it once (So we could probably do rename here eventually) + if saved then return end -- we only need it once, not for every load lyric source copy dbg_method("Source_save") saved = true using_source = true - rename_source() -- Rename and Mark sources instantly on update (WZ) end +-- Called when a change is made in the source dialog (Currently Not Used) +-- source_def.update = function(data, settings) dbg_method("update") - end +-- Called when the source dialog is loaded (Currently not Used) +-- source_def.load = function(data) dbg_method("load") end +-- Called when the refresh button is pressed in the source dialog +-- It reloads the song directory and applies any meta-tag filters if entered +-- function source_refresh_button_clicked(props, p) dbg_method("source_refresh_button") source_filter = true @@ -2432,12 +2449,18 @@ function source_refresh_button_clicked(props, p) return true end +-- Keeps variable source-meta-tags up-to-date +-- Note: This could be done only when refreshing the directory (see source_refresh_button_clicked) +-- function update_source_metatags(props, p, settings) - source_meta_tags = obs.obs_data_get_string(settings,"metatags") return true end +-- Called when a user makes a song selection in the source dialog +-- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens +-- Saved flag is cleared to mark changes have occured for save event +-- function source_selection_made(props, prop, settings) dbg_method("source_selection") local name = obs.obs_data_get_string(settings,"songs") @@ -2447,6 +2470,8 @@ dbg_method("source_selection") return true end +-- Standard OBS get Properties function for OBS source dialog +-- source_def.get_properties = function(data) source_filter = true load_source_song_directory(true) @@ -2478,11 +2503,14 @@ source_def.get_properties = function(data) end +-- Called when the source is created +-- saves pointer to settings in global sourc_sets for convienence +-- Sets callbacks for active, showing, deactive, and updated callbacks +-- source_def.create = function(settings, source) dbg_method("create") data = {} source_sets = settings - obs.obs_data_set_bool(settings,"isLLS", true) obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback @@ -2490,54 +2518,73 @@ dbg_method("create") return data end +-- Sets default settings for Activate Source in Preview +-- source_def.get_defaults = function(settings) -source_sets = settings obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) - obs.obs_data_set_default_string(settings, "index", "0") end +-- On Event Functions +-- These manage keeping the HTML monitor page updated when changes happen like scene changes that remove +-- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are +-- either created or deleted. Rename changes color and marks with *, sources that are reference copies of the same source +-- as accidentally changing the settings like the loaded song in one will change it in the reference copies. +-- + +-- Called via the timed callback, removes the callback and updates the HTML monitor page +-- function update_source_callback() obs.remove_current_callback() update_monitor() end +-- called via the timed callback, removes the callback and renames all the load sources +-- function rename_callback() obs.remove_current_callback() rename_source() end +-- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) dbg_method("on_event: " .. event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page dbg_bool("Active:",source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end - if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then + if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes dbg_inner("Scene Change") - obs.timer_add(rename_callback, 1000) + obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change end end +-- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. +-- function load_source_song(source, preview) - dbg_method("load_source_song") + dbgsp("load_source_song") local settings = obs.obs_source_get_settings(source) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - using_source = true - load_source = source - set_text_visibility(TEXT_HIDDEN) - prepare_selected(song) + using_source = true + load_source = source + all_sources_fade = true -- fade title and source the first time + set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in + if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles + prepare_selected(song) + end transition_lyric_text() - if obs.obs_data_get_bool(settings, "source_home_on_active") then - home_prepared(true) - end + if obs.obs_data_get_bool(settings, "source_home_on_active") then + home_prepared(true) + end end obs.obs_data_release(settings) end - +-- Call back when load source (not text source) goes to the Active Scene +-- loads the selected song and sets the current scene name for the HTML monitor +-- function source_isactive(cd) - dbg_method("source_active") + dbg_custom("source_active") local source = obs.calldata_source(cd, "source") if source == nil then return @@ -2548,6 +2595,9 @@ function source_isactive(cd) source_active = true -- using source lyric end +-- Call back when load source leaves the current Active Scene +-- just resets the source_active flag +-- function source_inactive(cd) dbg_inner("source inactive") local source = obs.calldata_source(cd, "source") @@ -2557,8 +2607,11 @@ function source_inactive(cd) source_active = false -- indicates source loading lyric is active (but using prepared lyrics is still possible) end +-- Call back when load source (not text source) goes to the Active +-- loads the selected song and sets the current scene name for the HTML monitor +-- function source_showing(cd) - dbg_method("source_showing") + dbg_custom("source_showing") local source = obs.calldata_source(cd, "source") if source == nil then return @@ -2566,6 +2619,14 @@ function source_showing(cd) load_source_song(source, true) end +-- dbg functions +-- +function dbg_traceback() + if DEBUG then + print("Trace: " .. debug.traceback()) + end +end + function dbg(message) if DEBUG then print(message) @@ -2584,6 +2645,11 @@ function dbg_method(message) end end +function dbgsp(message) +if DEBUG then + dbg("====SPECIAL=====================>> " .. message) +end +end function dbg_custom(message) if DEBUG_CUSTOM then dbg("CUST: " .. message) From 15a72860627f846f7bd08db9a431ee07cf52b19b Mon Sep 17 00:00:00 2001 From: wzaggle Date: Wed, 13 Oct 2021 03:59:31 -0600 Subject: [PATCH 049/105] Update lyrics.lua General Cleanup --- lyrics.lua | 89 ++++++++++++++++-------------------------------------- 1 file changed, 26 insertions(+), 63 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 53417af..cffb953 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -478,7 +478,7 @@ end -- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) - obs.obs_property_set_description(obs.obs_properties_get(props, "prep_grp"), " Prepared Songs (" .. #prepared_songs .. ")") + obs.obs_property_set_description(obs.obs_properties_get(props, "prep_grp"), " Prepared Songs/Text (" .. #prepared_songs .. ")") dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false @@ -1637,7 +1637,6 @@ local help = "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO C "Mark Verses     ##V        Override Title     #T: text\n\n" .. "Optional comma delimited meta tags follow '//meta ' on 1st line" - function script_properties() dbg_method("script_properties") editVisSet = false @@ -1663,27 +1662,27 @@ function script_properties() obs.obs_property_list_add_string(prop_dir_list, name, name) end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) - obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Songs by Meta Tags", filter_songs_clicked) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "\tFilter MetaTags", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - obs.obs_properties_add_group(gp, "meta", "\t Filter Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(gp, "meta", "\t Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","\tPrepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) - obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) - obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared Songs List",edit_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List",edit_prepared_clicked) local eps = obs.obs_properties_create() - local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "\tPrepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) + local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "\tPrepared Songs/Text", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) obs.obs_property_set_modified_callback(edit_prop, setEditVis) obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) - obs.obs_properties_add_group(gps,"edit_grp","\tEdit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) + obs.obs_properties_add_group(gps,"edit_grp","\tEdit Prepared Songs - Manually entered Titles (Filenames) must be in directory", obs.OBS_GROUP_NORMAL,eps) obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) - obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) + obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL,gp) ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲",change_ctrl_visible) hotkey_props = obs.obs_properties_create() @@ -1931,7 +1930,7 @@ function filter_songs_clicked(props, p) if not obs.obs_property_visible(pp) then obs.obs_property_set_visible(pp, true) local mpb = obs.obs_properties_get(props, "filter_songs_button") - obs.obs_property_set_description(mpb, "Clear Song Filters") -- change button function + obs.obs_property_set_description(mpb, "Clear Filters") -- change button function meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") refresh_directory() else @@ -1939,7 +1938,7 @@ function filter_songs_clicked(props, p) meta_tags = "" -- clear meta tags refresh_directory() local mpb = obs.obs_properties_get(props, "filter_songs_button") -- - obs.obs_property_set_description(mpb, "Filter Songs by Meta Tags") -- reset button function + obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function end return true end @@ -1949,15 +1948,13 @@ function edit_prepared_clicked(props, p) if obs.obs_property_visible(pp) then obs.obs_property_set_visible(pp, false) local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + obs.obs_property_set_description(mpb, "Edit Prepared List") return true end local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") local count = obs.obs_property_list_item_count(prop_prep_list) - dbg_inner("count: " .. count) local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) - dbg_inner("count2: " .. count2) if count2 > 0 then for i = 0, count2 do obs.obs_data_array_erase(songNames,0) @@ -1975,7 +1972,7 @@ function edit_prepared_clicked(props, p) obs.obs_data_array_release(songNames) obs.obs_property_set_visible(pp, true) local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Cancel Prepared Song Edits") + obs.obs_property_set_description(mpb, "Cancel Prepared Edits") return true end @@ -2024,52 +2021,18 @@ function change_transition_property(props, prop, settings) transition_enabled = transition_set return true end --- A function named script_update will be called when settings are changed + +-- script_update is called when settings are changed function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") -- Fade Enable (WZ) - text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") -- Fade Speed (WZ) - reload = false - local cur_display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") - if display_lines ~= cur_display_lines then - display_lines = cur_display_lines - reload = true - end - local cur_source_name = obs.obs_data_get_string(settings, "prop_source_list") - if source_name ~= cur_source_name then - source_name = cur_source_name - reload = true - end - local alt_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") - if alternate_source_name ~= alt_source_name then - alternate_source_name = alt_source_name - reload = true - end - local stat_source_name = obs.obs_data_get_string(settings, "prop_static_list") - if static_source_name ~= stat_source_name then - static_source_name = stat_source_name - reload = true - end - local cur_title_source = obs.obs_data_get_string(settings, "prop_title_list") - if title_source_name ~= cur_title_source then - title_source_name = cur_title_source - reload = true - end - local cur_ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") - if cur_ensure_lines ~= ensure_lines then - ensure_lines = cur_ensure_lines - reload = true - end + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") + text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") + display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + source_name = obs.obs_data_get_string(settings, "prop_source_list") + alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") + static_source_name = obs.obs_data_get_string(settings, "prop_static_list") + title_source_name = obs.obs_data_get_string(settings, "prop_title_list") + ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") - dbg_bool("link Text and Title", link_text) - - - if reload then - if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then - prepare_selected(prepared_songs[prepared_index]) - end - end - - name_hotkeys() end @@ -2315,7 +2278,7 @@ function script_load(settings) --prepared_index = 1 file:close() end - + name_hotkeys() obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end @@ -2670,6 +2633,6 @@ end obs.obs_register_source(source_def) -description = [[ -

OBS Lyrics+ Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
+description = [[ +

OBS Lyrics+
Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
]] \ No newline at end of file From 962b32a9dc93f03466518cb481ad77830dd0de2e Mon Sep 17 00:00:00 2001 From: wzaggle Date: Fri, 15 Oct 2021 14:19:07 -0600 Subject: [PATCH 050/105] More cleanup and additional Extra Linked Hide/Show sources. I made some additions to clean up some more code and added additional linked sources. Extra sources will transition with Hide/Show and can also be linked to Lyrics Visibility. Text sources will fade by default. Other sources will fade if they have a Color Correction Filter named "Color Correction" applied. Others sources without the filter will have visibility turned on or off at above or below 50% opacity. --- lyrics.lua | 419 +++++++++----- lyricstrial.lua | 1458 ++++++++++++++++++++++++++++------------------- 2 files changed, 1159 insertions(+), 718 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index cffb953..a264ecb 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -50,8 +50,10 @@ prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_s song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) prepared_songs = {} -- holds pre-prepared list of songs to use +extra_sources = {} -- holder for extra sources settings link_text = false -- true if Title and Static should fade with text only during hide/show +link_extras = false -- extras fade with text always when true, only during hide/show when false all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song @@ -122,7 +124,7 @@ editVisSet = false -- simple debugging/print mechanism DEBUG = true -- on switch for entire debugging mechanism ---DEBUG_METHODS = true -- print method names +DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false @@ -423,18 +425,27 @@ function refresh_button_clicked(props, p) local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") local static_source_prop = obs.obs_properties_get(props, "prop_static_list") local title_source_prop = obs.obs_properties_get(props, "prop_title_list") + local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") + obs.obs_property_list_clear(source_prop) -- clear current properties list obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list obs.obs_property_list_clear(static_source_prop) -- clear current properties list obs.obs_property_list_clear(title_source_prop) -- clear current properties list - + obs.obs_property_list_clear(extra_source_prop) -- clear extra sources list + + obs.obs_property_list_add_string(extra_source_prop, "", "") + local sources = obs.obs_enum_sources() if sources ~= nil then local n = {} for _, source in ipairs(sources) do + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n + 1] = obs.obs_source_get_name(source) + n[#n + 1] = name end end table.sort(n) @@ -624,7 +635,49 @@ function apply_source_opacity() obs.obs_source_release(static_source) obs.obs_data_release(settings) end - + if link_extras or all_sources_fade then + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count-1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + print(source_name) + local extra_source = obs.obs_get_source_by_name(source_name) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(extra_source, settings) -- merge new opacity values + obs.obs_data_release(settings) + else -- check for filter named "Color Correction" + local color_filter = obs.obs_source_get_filter_by_name(extra_source,"Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_source_get_settings(color_filter) + obs.obs_data_set_double(filter_settings,"opacity",text_opacity/100) + obs.obs_source_update(color_filter,filter_settings) + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + else -- try to just change visibility in the scene + print("No Filter") + local sceneSource = obs.obs_frontend_get_current_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source(sceneObj,source_name) + obs.obs_source_release(scene) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end + end + end function set_text_visibility(end_status) @@ -1645,7 +1698,7 @@ function script_properties() ----------- obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲",change_info_visible) local gp = obs.obs_properties_create() - obs.obs_properties_add_text(gp, "prop_edit_song_title", "\tSong Title (Filename)", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) obs.obs_properties_add_text(gp, "prop_edit_song_text", "\tSong Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) @@ -1656,7 +1709,7 @@ function script_properties() ------------ obs.obs_properties_add_button(script_props, "prepared_showing", "▲- HIDE PREPARED SONGS -▲",change_prepared_visible) gp = obs.obs_properties_create() - local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","\tSong Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(prop_dir_list, name, name) @@ -1665,11 +1718,11 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "\tFilter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - obs.obs_properties_add_group(gp, "meta", "\t Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) + local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() - local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","\tPrepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) + local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end @@ -1677,16 +1730,16 @@ function script_properties() obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List",edit_prepared_clicked) local eps = obs.obs_properties_create() - local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "\tPrepared Songs/Text", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) + local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs/Text", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) obs.obs_property_set_modified_callback(edit_prop, setEditVis) obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) - obs.obs_properties_add_group(gps,"edit_grp","\tEdit Prepared Songs - Manually entered Titles (Filenames) must be in directory", obs.OBS_GROUP_NORMAL,eps) + local edit_group_prop = obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", obs.OBS_GROUP_NORMAL,eps) obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL,gp) ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲",change_ctrl_visible) hotkey_props = obs.obs_properties_create() - + local hktitletext = obs.obs_properties_add_text(hotkey_props,"hotkey-title", "\t", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) @@ -1704,14 +1757,11 @@ function script_properties() lines_prop, "Sets default lines per page of lyric, overwritten by Markup: #L:n" ) - local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") - local link_prop = - obs.obs_properties_add_bool(gp, "do_link_text", "Only show title and static text with lyrics") + obs.obs_properties_add_bool(gp, "do_link_text", "Link title & static text visibility with lyric text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") - local transition_prop = obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") obs.obs_property_set_modified_callback(transition_prop, change_transition_property) @@ -1719,7 +1769,6 @@ function script_properties() transition_prop, "Use with Studio Mode, duplicate sources, and OBS source transitions" ) - local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) obs.obs_properties_add_int_slider(gp, "text_fade_speed", "\tFade Speed", 1, 10, 1) @@ -1727,11 +1776,12 @@ function script_properties() ------------- obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲",change_src_visible) gp = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) local source_prop = obs.obs_properties_add_list( gp, "prop_source_list", - "\tText Source", + "Text Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1739,7 +1789,7 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_title_list", - "\tTitle Source", + "Title Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1747,7 +1797,7 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_alternate_list", - "\tAlternate Source", + "Alternate Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1755,18 +1805,42 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_static_list", - "\tStatic Source", + "Static Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) + obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + xgp = obs.obs_properties_create() + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Also Link Sources to Lyrics Text Visibility") + local extra_linked_prop = obs.obs_properties_add_list(xgp,"extra_linked_list","Linked Sources ",obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) + -- initialize previously loaded extra properties from table + for _, sourceName in ipairs(extra_sources) do + obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) + end + local extra_source_prop = obs.obs_properties_add_list(xgp,"extra_source_list"," Select Source:",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) + local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) + local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Hide/Show Visibility Linked Sources ", obs.OBS_GROUP_NORMAL,xgp) + obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) + local count = obs.obs_property_list_item_count(extra_linked_prop) + if count > 0 then + obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") + else + obs.obs_property_set_visible(extra_group_prop, false) + end local sources = obs.obs_enum_sources() + obs.obs_property_list_add_string(extra_source_prop, "", "") if sources ~= nil then local n = {} for _, source in ipairs(sources) do + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n + 1] = obs.obs_source_get_name(source) + n[#n + 1] = name end end table.sort(n) @@ -1782,19 +1856,97 @@ function script_properties() end end obs.source_list_release(sources) - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Text Sources", refresh_button_clicked) - obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes\t", obs.OBS_GROUP_NORMAL,gp) + ----------------- - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + obs.obs_property_set_enabled(hktitletext,false) + obs.obs_property_set_visible(edit_group_prop, false) + obs.obs_property_set_visible(meta_group_prop, false) + return script_props +end + +-- script_update is called when settings are changed +function script_update(settings) + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") + text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") + display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + source_name = obs.obs_data_get_string(settings, "prop_source_list") + alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") + static_source_name = obs.obs_data_get_string(settings, "prop_static_list") + title_source_name = obs.obs_data_get_string(settings, "prop_title_list") + ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") + link_text = obs.obs_data_get_bool(settings, "do_link_text") + link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") +end + +-- A function named script_defaults will be called to set the default settings +function script_defaults(settings) + dbg_method("script_defaults") + obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) + obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + if windows_os then + os.execute('mkdir "' .. get_songs_folder_path() .. '"') + else + os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') end +end - obs.obs_properties_apply_settings(script_props, script_sets) - return script_props +--verify source has an opacity setting +function isValid(source) + if source ~= nil then + local flags = obs.obs_source_get_output_flags(source) + print(obs.obs_source_get_name(source) .. " - " .. flags) + local targetFlag = obs.OBS_SOURCE_VIDEO+obs.OBS_SOURCE_CUSTOM_DRAW+obs.OBS_SOURCE_SRGB + if bit.band(flags, 32777) == 32777 then + return true + end + end + return false +end + + +-- adds an extra linked source. +-- Source must be text source, or have 'Color Correction' Filter applied +function link_source_selected(props, prop, settings) + dbg_method("link_source_selected") + local extra_source = obs.obs_data_get_string(settings, "extra_source_list") + if extra_source ~= nil and extra_source ~= "" then + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) + obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) + obs.obs_data_set_string(script_sets, "extra_source_list", "") + obs.obs_property_set_description(extra_linked_list, "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")") + end + return true end +-- removes linked sources +function do_linked_clicked(props, p) + dbg_method("do_link_clicked") + obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), true) + obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), false) + obs.obs_properties_apply_settings(props, script_sets) + + return true +end + +-- removes linked sources +function clear_linked_clicked(props, p) + dbg_method("clear_linked_clicked") + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_clear(extra_linked_list) + obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), true) + obs.obs_property_set_description(extra_linked_list, "Linked Sources ") + + return true +end + + -- A function named script_description returns the description shown to -- the user @@ -2022,35 +2174,6 @@ function change_transition_property(props, prop, settings) return true end --- script_update is called when settings are changed -function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") - text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") - display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") - source_name = obs.obs_data_get_string(settings, "prop_source_list") - alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") - static_source_name = obs.obs_data_get_string(settings, "prop_static_list") - title_source_name = obs.obs_data_get_string(settings, "prop_title_list") - ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") - link_text = obs.obs_data_get_bool(settings, "do_link_text") -end - - - --- A function named script_defaults will be called to set the default settings -function script_defaults(settings) - dbg_method("script_defaults") - obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - if windows_os then - os.execute('mkdir "' .. get_songs_folder_path() .. '"') - else - os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') - end - -end -- A function named script_save will be called when the script is saved function script_save(settings) @@ -2083,7 +2206,113 @@ function script_save(settings) hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) + --- + --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded + --- + local extra_sources_array = obs.obs_data_array_create() + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + for i = 0, count-1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", source_name) + obs.obs_data_array_push_back(extra_sources_array,array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) + obs.obs_data_array_release(extra_sources_array) +end + + +-- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS +-- sets callback to obs_frontend Event Callback +-- +function script_load(settings) + dbg_method("script_load") + hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric", " ......................") + obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") + hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric", " ..................") + obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") + hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics", " ..............") + obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") + hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Prepared", " ................") + obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") + hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared", "............") + obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) + hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") + hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start", " ..........") + obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") + hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared", " .......") + obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + script_sets = settings + source_name = obs.obs_data_get_string(settings, "prop_source_list") + + extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + + -- load previously defined extra sources from settings array into table + -- script_properties function will take them from the table and restore them as UI properties + -- + local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + local count = obs.obs_data_array_count(extra_sources_array) + if count > 0 then + for i = 0, count do + local item = obs.obs_data_array_item(extra_sources_array, i); + local sourceName = obs.obs_data_get_string(item, "value"); + if sourceName ~= "" then + extra_sources[#extra_sources + 1] = sourceName + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(extra_sources_array) + + + -- load prepared songs from stored file + -- + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + load_source_song_directory(false) + -- load prepared songs from previous + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") + if file ~= nil then + for line in file:lines() do + prepared_songs[#prepared_songs + 1] = line + end + file:close() + end + name_hotkeys() + + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end + --- ------ --------- Source Showing or Source Active Helper Functions @@ -2168,14 +2397,14 @@ end ---------------------------------------------------------------------------------------------------------- function get_hotkeys(hotkey_array, prefix, leader) - local Translate = {["NUMLOCK"] = "Num Lock", ["NUMSLASH"] = "Num /", ["NUMASTERISK"] = "Num *", - ["NUMMINUS"] = "Num -", ["NUMPLUS"] = "Num +", - ["NUMPERIOD"] = "Num Del", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page-Down", + local Translate = {["NUMLOCK"] = "NumLock", ["NUMSLASH"] = "Num/", ["NUMASTERISK"] = "Num*", + ["NUMMINUS"] = "Num-", ["NUMPLUS"] = "Num+", + ["NUMPERIOD"] = "NumDel", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page-Down", ["PAGEUP"] = "Page-Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", ["UP"] = "Up", ["DOWN"] = "Down", ["RIGHT"] = "Right", ["LEFT"] = "Left", ["SCROLLLOCK"] = "Scroll-Lock", ["BACKSPACE"] = "Backspace", ["ESCAPE"] = "Esc", ["MENU"] = "Menu", ["META"] = "Meta", ["PRINT"] = "Prt", ["TAB"] = "Tab", - ["DELETE"] = "Del", ["CAPSLOCK"] = "Caps-Lock", ["NUMEQUAL"] = "Num =", ["PAUSE"] = "Pause", + ["DELETE"] = "Del", ["CAPSLOCK"] = "Caps-Lock", ["NUMEQUAL"] = "Num=", ["PAUSE"] = "Pause", ["VK_VOLUME_MUTE"] = "Vol Mute", ["VK_VOLUME_DOWN"] = "Vol Dwn", ["VK_VOLUME_UP"] = "Vol Up", ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", ["VK_MEDIA_STOP"] = "Media Stop", ["VK_MEDIA_PREV_TRACK"] = "Media Prev", ["VK_MEDIA_NEXT_TRACK"] = "Media Next"} @@ -2216,72 +2445,6 @@ function name_hotkeys() end --- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS --- sets callback to obs_frontend Event Callback --- -function script_load(settings) - dbg_method("script_load") - hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric", " ......................") - obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") - hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric", " ..................") - obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") - hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics", " ..............") - obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") - hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Prepared", " ................") - obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") - hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared", "............") - obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) - hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") - hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start", " ..........") - obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") - hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared", " .......") - obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - script_sets = settings - source_name = obs.obs_data_get_string(settings, "prop_source_list") - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - load_source_song_directory(false) - -- load prepared songs from previous - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") - if file ~= nil then - for line in file:lines() do - prepared_songs[#prepared_songs + 1] = line - end - --prepared_index = 1 - file:close() - end - name_hotkeys() - obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture -end - -------- ---------------- ------------------------ SOURCE FUNCTIONS @@ -2634,5 +2797,5 @@ end obs.obs_register_source(source_def) description = [[ -

OBS Lyrics+
Manages song lyrics and other paged text

Version: 2.0   •   Authors: Amirchev & DC Strato; with contributions from Taxilian.
+

OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian

]] \ No newline at end of file diff --git a/lyricstrial.lua b/lyricstrial.lua index addac23..b97fe50 100644 --- a/lyricstrial.lua +++ b/lyricstrial.lua @@ -1,4 +1,4 @@ ---- Copyright 2020 amirchev +--- Copyright 2020 amirchev/wzaggle -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -12,7 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- added delete single prepared song (WZ) obs = obslua bit = require("bit") @@ -29,42 +28,39 @@ source_name = "" alternate_source_name = "" static_source_name = "" static_text = "" --- current_scene = "" --- preview_scene = "" title_source_name = "" -- settings windows_os = false first_open = true --- in_timer = false --- in_Load = false --- in_directory = false --- pause_timer = false + display_lines = 0 ensure_lines = true --- visible = false --- lyrics status --- TODO: removed displayed_song and use prepared_songs[prepared_index] --- displayed_song = "" + +-- lyrics/alternate lyrics by page lyrics = {} +alternate = {} + +-- verse indicies if marked verses = {} --- refrain = {} -alternate = {} -page_index = 0 +page_index = 0 -- current page of lyrics being displayed prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected -song_directory = {} -prepared_songs = {} + +song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) +prepared_songs = {} -- holds pre-prepared list of songs to use +extra_sources = {} -- holder for extra sources settings + link_text = false -- true if Title and Static should fade with text only during hide/show +link_extras = false -- extras fade with text always when true, only during hide/show when false all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) load_scene = "" -- name of scene loading a lyric with a source -timer_exists = false ---forceNoFade = false -- allows for instant opacity change even if fade is enabled - Reset each time by set_text_visibility +last_prepared_song = "" -- name of the last prepared song (prevents duplicate loading of already loaded song) -- hotkeys hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID @@ -75,11 +71,20 @@ hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_n_key = "" +hotkey_p_key = "" +hotkey_c_key = "" +hotkey_n_p_key = "" +hotkey_p_p_key = "" +hotkey_home_key = "" +hotkey_reset_key = "" + -- script placeholders script_sets = nil script_props = nil source_sets = nil source_props = nil +hotkey_props = nil --monitor variables mon_song = "" @@ -90,6 +95,7 @@ mon_alt = "" mon_nextalt = "" mon_nextsong = "" meta_tags = "" +source_meta_tags = "" -- text status & fade TEXT_VISIBLE = 0 -- text is visible @@ -98,6 +104,9 @@ TEXT_SHOWING = 3 -- going from hidden -> visible TEXT_HIDING = 4 -- going from visible -> hidden TEXT_TRANSITION_OUT = 5 -- fade out transition to next lyric TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change +TEXT_HIDE = 7 -- turn off the text and ignore fade if selected +TEXT_SHOW = 8 -- turn on the text and ignore fade if selected + text_status = TEXT_VISIBLE text_opacity = 100 text_fade_speed = 1 @@ -109,109 +118,23 @@ showhelp = false transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false -editVisSet = false +source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now +editVisSet = false -- simple debugging/print mechanism -DEBUG = true -- on/off switch for entire debugging mechanism +DEBUG = true -- on switch for entire debugging mechanism DEBUG_METHODS = true -- print method names -DEBUG_INNER = true -- print inner method breakpoints -DEBUG_CUSTOM = false -- print custom debugging messages -DEBUG_BOOL = true -- print message with bool state true/false +--DEBUG_INNER = true -- print inner method breakpoints +--DEBUG_CUSTOM = true -- print custom debugging messages +--DEBUG_BOOL = true -- print message with bool state true/false -------- ---------------- ------------------------ CALLBACKS ---------------- -------- -function anythingShowing() - return sourceShowing() or alternateShowing() or titleShowing() or staticShowing() -end - -function sourceShowing() - local source = obs.obs_get_source_by_name(source_name) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function alternateShowing() - local source = obs.obs_get_source_by_name(alternate_source_name) - local showing = false - if source ~= nil then - -- obs.os_sleep_ms(10) -- Workaround Timing Bug in OBS Lua that delays correctly reporting source status - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function titleShowing() - local source = obs.obs_get_source_by_name(title_source_name) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function staticShowing() - local source = obs.obs_get_source_by_name(static_source_name) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function anythingActive() - return sourceActive() or alternateActive() or titleActive() or staticActive() -end - -function sourceActive() - local source = obs.obs_get_source_by_name(source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end - -function alternateActive() - local source = obs.obs_get_source_by_name(alternate_source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end - -function titleActive() - local source = obs.obs_get_source_by_name(title_source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end -function staticActive() - local source = obs.obs_get_source_by_name(static_source_name) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end function next_lyric(pressed) if not pressed then @@ -288,19 +211,23 @@ function next_prepared(pressed) end if using_source then using_source = false + dbg_custom("do current prepared") prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song return end if prepared_index < #prepared_songs then using_source = false + dbg_custom("do next prepared") prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared return end if not source_active or using_source then using_source = false + dbg_custom("do first prepared") prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available else using_source = true + dbg_custom("do source prepared") prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source load_source_song(load_source, false) end @@ -311,6 +238,9 @@ function toggle_lyrics_visibility(pressed) if not pressed then return end + if link_text then + all_sources_fade = true + end if text_status ~= TEXT_HIDDEN then dbg_inner("hiding") set_text_visibility(TEXT_HIDDEN) @@ -483,9 +413,8 @@ function prepare_song_clicked(props, p) local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - -- prepare_song_by_index(#prepared_songs) - --end + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) + obs.obs_properties_apply_settings(props, script_sets) save_prepared() return true @@ -496,18 +425,27 @@ function refresh_button_clicked(props, p) local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") local static_source_prop = obs.obs_properties_get(props, "prop_static_list") local title_source_prop = obs.obs_properties_get(props, "prop_title_list") + local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") + obs.obs_property_list_clear(source_prop) -- clear current properties list obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list obs.obs_property_list_clear(static_source_prop) -- clear current properties list obs.obs_property_list_clear(title_source_prop) -- clear current properties list - + obs.obs_property_list_clear(extra_source_prop) -- clear extra sources list + + obs.obs_property_list_add_string(extra_source_prop, "", "") + local sources = obs.obs_enum_sources() if sources ~= nil then local n = {} for _, source in ipairs(sources) do + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n + 1] = obs.obs_source_get_name(source) + n[#n + 1] = name end end table.sort(n) @@ -522,6 +460,7 @@ function refresh_button_clicked(props, p) obs.obs_property_list_add_string(static_source_prop, name, name) end end + obs.source_list_release(sources) refresh_directory() return true @@ -535,8 +474,7 @@ end function refresh_directory() local prop_dir_list = obs.obs_properties_get(script_props,"prop_directory_list") - local source_prop = obs.obs_properties - obs.source_list_release(sources) + local source_prop = obs.obs_properties_get(props, "prop_source_list") source_filter = false load_source_song_directory(true) table.sort(song_directory) @@ -548,7 +486,10 @@ function refresh_directory() obs.obs_properties_apply_settings(script_props, script_sets) end + +-- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) + obs.obs_property_set_description(obs.obs_properties_get(props, "prep_grp"), " Prepared Songs/Text (" .. #prepared_songs .. ")") dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false @@ -581,10 +522,12 @@ function prepare_selected(name) prepared_index = get_index_in_list(prepared_songs, name) else source_song_title = name + all_sources_fade = true end - all_sources_fade = true - -- if using source, then force show the new lyrics, even if lyrics were previously hidden - transition_lyric_text(using_source) + + transition_lyric_text(using_source) + + else -- hide everything if unable to prepare song -- TODO: clear lyrics entirely after text is hidden @@ -649,6 +592,7 @@ end function apply_source_opacity() -- dbg_method("apply_source_visiblity") + local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -657,24 +601,83 @@ function apply_source_opacity() obs.obs_source_update(source, settings) end obs.obs_source_release(source) + obs.obs_data_release(settings) + + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local alt_source = obs.obs_get_source_by_name(alternate_source_name) if alt_source ~= nil then obs.obs_source_update(alt_source, settings) end obs.obs_source_release(alt_source) - if all_sources_fade and link_text then + obs.obs_data_release(settings) + dbg_bool("All Sources Fade:", all_sources_fade) + dbg_bool("Link Text:", link_text) + if all_sources_fade then + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then obs.obs_source_update(title_source, settings) end - obs.obs_source_release(title_source) + obs.obs_source_release(title_source) + obs.obs_data_release(settings) + + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local static_source = obs.obs_get_source_by_name(static_source_name) if static_source ~= nil then obs.obs_source_update(static_source, settings) end obs.obs_source_release(static_source) - end - obs.obs_data_release(settings) + obs.obs_data_release(settings) + end + if link_extras or all_sources_fade then + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count-1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + print(source_name) + local extra_source = obs.obs_get_source_by_name(source_name) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(extra_source, settings) -- merge new opacity values + obs.obs_data_release(settings) + else -- check for filter named "Color Correction" + local color_filter = obs.obs_source_get_filter_by_name(extra_source,"Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_source_get_settings(color_filter) + obs.obs_data_set_double(filter_settings,"opacity",text_opacity/100) + obs.obs_source_update(color_filter,filter_settings) + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + else -- try to just change visibility in the scene + print("No Filter") + local sceneSource = obs.obs_frontend_get_current_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source(sceneObj,source_name) + obs.obs_source_release(scene) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end + end + end function set_text_visibility(end_status) @@ -683,37 +686,48 @@ function set_text_visibility(end_status) if text_status == end_status then return end - -- change visibility immediately (fade or no fade) - if end_status == TEXT_HIDDEN then + if end_status == TEXT_HIDE then text_opacity = 0 text_status = end_status - elseif end_status == TEXT_VISIBLE then + apply_source_opacity() + return + elseif end_status == TEXT_SHOW then text_opacity = 100 text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + apply_source_opacity() + return end - if text_status == end_status then - apply_source_opacity() - update_source_text() - return - end - --if text_fade_enabled then + if text_fade_enabled then -- if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then text_status = TEXT_HIDING elseif end_status == TEXT_VISIBLE then text_status = TEXT_SHOWING end - all_sources_fade = true + --all_sources_fade = true start_fade_timer() - --end - update_source_text() + else -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + end + apply_source_opacity() + --update_source_text() + all_sources_fade = false + return + end end -- transition to the next lyrics, use fade if enabled -- if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) - dbg_method("transition_lyric_text") - dbg_bool("using_source", using_source) + dbgsp("transition_lyric_text") + dbg_bool("force show", force_show) -- update the lyrics display immediately on 2 conditions -- a) the text is hidden or hiding, and we will not force it to show -- b) text fade is not enabled @@ -727,30 +741,150 @@ function transition_lyric_text(force_show) end dbg_inner("hidden") elseif not text_fade_enabled then + dbg_custom("Instant On") -- if text fade is not enabled, then we can cancel the all_sources_fade all_sources_fade = false - set_text_visibility(TEXT_VISIBLE) + set_text_visibility(TEXT_VISIBLE) -- does update_source_text() update_source_text() dbg_inner("no text fade") - else + else -- initiate fade out/in + dbg_custom("Transition Timer") text_status = TEXT_TRANSITION_OUT start_fade_timer() end dbg_bool("using_source", using_source) end +-- updates the selected lyrics +function update_source_text() + dbg_method("update_source_text") + dbg_custom("Page Index: " .. page_index) + dbg_traceback() + local text = "" + local alttext = "" + local next_lyric = "" + local next_alternate = "" + local static = static_text + local mstatic = static -- save static for use with monitor + local title = "" + + + if alt_title ~= "" then + title = alt_title + else + if not using_source then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Update from prepared: " .. prepared_index) + title = prepared_songs[prepared_index] + end + else + dbg_custom("Updatefrom source: " .. source_song_title) + title = source_song_title + end + end + + local source = obs.obs_get_source_by_name(source_name) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) + + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then + if #lyrics > 0 then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] + end + end + if #alternate > 0 then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] + end + end + + if link_text then + if string.len(text) == 0 and string.len(alttext) == 0 then + --static = "" + --title = "" + end + end + end + -- update source texts + if source ~= nil then + dbg_inner("Title Load") + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + next_lyric = lyrics[page_index + 1] + if (next_lyric == nil) then + next_lyric = "" + end + end + if alt_source ~= nil then + local settings = obs.obs_data_create() -- setup TEXT settings with opacity values + obs.obs_data_set_string(settings, "text", alttext) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" + end + end + if stat_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) + end + if title_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) + end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) + obs.obs_source_release(title_source) + + local next_prepared = "" + if using_source then + next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song + elseif prepared_index < #prepared_songs then + next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song + else + if source_active then + next_prepared = source_song_title -- plan to go back to source loaded song + else + next_prepared = prepared_songs[1] -- plan to loop around to first prepared song + end + end + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i]+1 then + mon_verse = i + end + end -- v = current verse number for this page + end + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared + + update_monitor() +end + function start_fade_timer() - if not timer_exists then - timer_exists = true + dbgsp("started fade timer") obs.timer_add(fade_callback, 50) - dbg_inner("started fade timer") - end end function fade_callback() -- if not in a transitory state, exit callback if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then - timer_exists = false obs.remove_current_callback() all_sources_fade = false end @@ -800,6 +934,7 @@ function prepare_song_by_name(name) if name == nil then return false end + last_prepared_song = name -- if using transition on lyric change, first transition -- would be reset with new song prepared transition_completed = false @@ -1157,9 +1292,11 @@ function load_source_song_directory(use_filter) dbg_method("load_source_song_directory") local keytext = meta_tags if source_filter then - keytext = obs.obs_data_get_string(source_sets, "prop_edit_metatags") + keytext = source_meta_tags end + dbg_inner(keytext) local keys = ParseCSVLine(keytext) + song_directory = {} local filenames = {} local tags = {} @@ -1261,6 +1398,7 @@ function ParseCSVLine (line) if (c == '"') then txt = txt..'"' end until (c ~= '"') txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. txt) table.insert(res,txt) assert(c == sep or c == "") pos = pos + 1 @@ -1269,11 +1407,13 @@ function ParseCSVLine (line) if (startp) then local t = string.sub(line,pos,startp-1) t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) table.insert(res,t) pos = endp + 1 else local t = string.sub(line,pos) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) table.insert(res,t) break end @@ -1388,131 +1528,10 @@ function save_prepared() end --- updates the selected lyrics -function update_source_text() - dbg_method("update_source_text") - dbg_inner("Page Index: " .. page_index) - local text = "" - local alttext = "" - local next_lyric = "" - local next_alternate = "" - local static = static_text - local mstatic = static -- save static for use with monitor - local title = "" - - - if alt_title ~= "" then - title = alt_title - else - if not using_source then - if prepared_index ~= nil and prepared_index ~= 0 then - dbg_custom("Load title from prepared: " .. prepared_index) - title = prepared_songs[prepared_index] - end - else - dbg_custom("Load title from source") - title = source_song_title - end - end - local mtitle = title -- save title for use with monitor - - local source = obs.obs_get_source_by_name(source_name) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - local stat_source = obs.obs_get_source_by_name(static_source_name) - local title_source = obs.obs_get_source_by_name(title_source_name) - - if using_source or (prepared_index ~= nil and prepared_index ~= 0) then - if #lyrics > 0 then - if lyrics[page_index] ~= nil then - text = lyrics[page_index] - end - end - if #alternate > 0 then - if alternate[page_index] ~= nil then - alttext = alternate[page_index] - end - end - - if link_text then - if string.len(text) == 0 and string.len(alttext) == 0 then - static = "" - title = "" - end - end - end - -- update source texts - if source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", text) - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) - next_lyric = lyrics[page_index + 1] - if (next_lyric == nil) then - next_lyric = "" - end - end - if alt_source ~= nil then - local settings = obs.obs_data_create() -- setup TEXT settings with opacity values - obs.obs_data_set_string(settings, "text", alttext) - obs.obs_source_update(alt_source, settings) - obs.obs_data_release(settings) - next_alternate = alternate[page_index + 1] - if (next_alternate == nil) then - next_alternate = "" - end - end - if stat_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", static) - obs.obs_source_update(stat_source, settings) - obs.obs_data_release(settings) - end - if title_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", title) - obs.obs_source_update(title_source, settings) - obs.obs_data_release(settings) - end - -- release source references - obs.obs_source_release(source) - obs.obs_source_release(alt_source) - obs.obs_source_release(stat_source) - obs.obs_source_release(title_source) - - local next_prepared = "" - if using_source then - next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song - elseif prepared_index < #prepared_songs then - next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song - else - if source_active then - next_prepared = source_song_title -- plan to go back to source loaded song - else - next_prepared = prepared_songs[1] -- plan to loop around to first prepared song - end - end - mon_verse = 0 - if #verses ~= nil then --find valid page Index - for i = 1, #verses do - if page_index >= verses[i]+1 then - mon_verse = i - end - end -- v = current verse number for this page - end - mon_song = title - mon_lyric = text:gsub("\n", "
• ") - mon_nextlyric = next_lyric:gsub("\n", "
• ") - mon_alt = alttext:gsub("\n", "
• ") - mon_nextalt = next_alternate:gsub("\n", "
• ") - mon_nextsong = next_prepared - - update_monitor() -end - -function update_monitor() - - dbg_method("update_monitor") - local tableback = "black" +function update_monitor() + + dbg_method("update_monitor") + local tableback = "black" local text = "" text = text .. "" text = text .. "" @@ -1560,7 +1579,7 @@ function update_monitor() text .. "
" - if mon_song ~= "" and Mon_song ~= nil then + if mon_song ~= "" and mon_song ~= nil then text = text .. "" @@ -1658,9 +1677,9 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself -local help = "░░░░░░░░░░░░░░░ MARKUP SYNTAX HELP ░░░░░░░░░░░░░░░\n\n" .. - "Markup      Syntax        Markup       Syntax\n" .. - "==========  ==========    ==========  ==========\n" .. +local help = "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. + " Markup      Syntax         Markup      Syntax \n" .. + "============ ==========   ============ ==========\n" .. " Display n Lines    #L:n      End Page after Line   Line ###\n" .. " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. @@ -1669,27 +1688,26 @@ local help = "░░░░░░░░░░░░░░░ MARKUP SYNTAX HELP "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. "Comment Line    // Line       Block Comments    //[ and //] \n" .. "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line\n\n" .. - "▲░░░░░░ CLICK TO CLOSE ░░░░░░▲" - + "Optional comma delimited meta tags follow '//meta ' on 1st line" + function script_properties() dbg_method("script_properties") editVisSet = false script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) - obs.obs_properties_add_button(script_props, "expand_all_button", "▲░ HIDE ALL GROUPS ░▲", expand_all_groups) + obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) ----------- - obs.obs_properties_add_button(script_props, "info_showing", "HIDE SONG INFORMATION",change_info_visible) - gp = obs.obs_properties_create() + obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲",change_info_visible) + local gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "\tSong Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) obs.obs_properties_add_button(gp, "prop_opensong_button","Edit Song with System Editor", open_song_clicked) obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------ - obs.obs_properties_add_button(script_props, "prepared_showing", "▲░ HIDE PREPARED SONGS ░▲",change_prepared_visible) + obs.obs_properties_add_button(script_props, "prepared_showing", "▲- HIDE PREPARED SONGS -▲",change_prepared_visible) gp = obs.obs_properties_create() local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) table.sort(song_directory) @@ -1697,44 +1715,53 @@ function script_properties() obs.obs_property_list_add_string(prop_dir_list, name, name) end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song", prepare_song_clicked) - obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Songs by Meta Tags", filter_songs_clicked) - gps = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) + local gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - obs.obs_properties_add_group(gp, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() - local prep_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) --- prepare_props = prep_prop + local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prep_prop, name, name) + obs.obs_property_list_add_string(prepare_prop, name, name) end - obs.obs_property_set_modified_callback(prep_prop, prepare_selection_made) - obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs", clear_prepared_clicked) - obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared Songs List",edit_prepared_clicked) - eps = obs.obs_properties_create() - local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) - obs.obs_property_set_modified_callback(edit_prop, setEditVis) - obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) - obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs", obs.OBS_GROUP_NORMAL,eps) - obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs", obs.OBS_GROUP_NORMAL, gps) - obs.obs_properties_add_group(script_props,"prep_grp","Manage Prepared Songs", obs.OBS_GROUP_NORMAL,gp) + obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List",edit_prepared_clicked) + local eps = obs.obs_properties_create() + local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs/Text", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) + obs.obs_property_set_modified_callback(edit_prop, setEditVis) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) + local edit_group_prop = obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", obs.OBS_GROUP_NORMAL,eps) + obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL,gp) +------------------ + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲",change_ctrl_visible) + hotkey_props = obs.obs_properties_create() + local hktitletext = obs.obs_properties_add_text(hotkey_props,"hotkey-title", "\t", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) + obs.obs_properties_add_button(hotkey_props,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) + ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)", obs.OBS_GROUP_NORMAL,hotkey_props) + obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) ------ - obs.obs_properties_add_button(script_props, "options_showing", "▲░ HIDE DISPLAY OPTIONS ░▲",change_options_visible) + obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲",change_options_visible) gp = obs.obs_properties_create() - local lines_prop = obs.obs_properties_add_int(gp, "prop_lines_counter", "Lines to Display", 1, 100, 1) + local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "\tLines to Display", 1, 50, 1) obs.obs_property_set_long_description( lines_prop, "Sets default lines per page of lyric, overwritten by Markup: #L:n" ) - local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") - local link_prop = - obs.obs_properties_add_bool(gp, "link_text", "Only show title and static text with lyrics") + obs.obs_properties_add_bool(gp, "do_link_text", "Link title & static text visibility with lyric text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") - local transition_prop = obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") obs.obs_property_set_modified_callback(transition_prop, change_transition_property) @@ -1742,14 +1769,14 @@ function script_properties() transition_prop, "Use with Studio Mode, duplicate sources, and OBS source transitions" ) - local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + obs.obs_properties_add_int_slider(gp, "text_fade_speed", "\tFade Speed", 1, 10, 1) obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) ------------- - obs.obs_properties_add_button(script_props, "src_showing", "▲░ HIDE SOURCE TEXT SELECTIONS ░▲",change_src_visible) + obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲",change_src_visible) gp = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) local source_prop = obs.obs_properties_add_list( gp, @@ -1758,7 +1785,6 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(source_prop, "Shows main lyric text") local title_source_prop = obs.obs_properties_add_list( gp, @@ -1767,7 +1793,6 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(title_source_prop, "Shows text from song title") local alternate_source_prop = obs.obs_properties_add_list( gp, @@ -1776,7 +1801,6 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(alternate_source_prop, "Shows text annotated with #A[ and #A]") local static_source_prop = obs.obs_properties_add_list( gp, @@ -1785,14 +1809,34 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_long_description(static_source_prop, "Shows text annotated with #S[ and #S]") + obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + xgp = obs.obs_properties_create() + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Link Visibility to Lyrics") + local extra_linked_prop = obs.obs_properties_add_list(xgp,"extra_linked_list","Linked Sources",obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) + -- initialize previously loaded extra properties from table + for _, sourceName in ipairs(extra_sources) do + obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) + end + local extra_source_prop = obs.obs_properties_add_list(xgp,"extra_source_list"," Valid Sources:",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) + local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) + obs.obs_property_set_modified_callback(clearcall_prop, clear_callback) + local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Sources ", obs.OBS_GROUP_NORMAL,xgp) + obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) + + local sources = obs.obs_enum_sources() + obs.obs_property_list_add_string(extra_source_prop, "", "") if sources ~= nil then local n = {} for _, source in ipairs(sources) do + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n + 1] = obs.obs_source_get_name(source) + n[#n + 1] = name end end table.sort(n) @@ -1808,65 +1852,134 @@ function script_properties() end end obs.source_list_release(sources) - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh Sources", refresh_button_clicked) - obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) ------------------- - obs.obs_properties_add_button(script_props, "ctrl_showing", "▲░ HIDE LYRIC CONTROLS ░▲",change_ctrl_visible) - gp = obs.obs_properties_create() - obs.obs_properties_add_button(gp, "prop_prev_button", "Previous Lyric", prev_button_clicked) - obs.obs_properties_add_button(gp, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(gp, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) - obs.obs_properties_add_button(gp, "prop_home_button", "Reset to Song Start", home_button_clicked) - obs.obs_properties_add_button(gp, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) - obs.obs_properties_add_button(gp, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button(gp,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) - obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Controls (Duplicated by Hot Keys)", obs.OBS_GROUP_NORMAL,gp) + + ----------------- - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + obs.obs_property_set_enabled(hktitletext,false) + obs.obs_property_set_visible(edit_group_prop, false) + obs.obs_property_set_visible(meta_group_prop, false) + print("PROP") + return script_props +end + +-- script_update is called when settings are changed +function script_update(settings) + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") + text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") + display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + source_name = obs.obs_data_get_string(settings, "prop_source_list") + alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") + static_source_name = obs.obs_data_get_string(settings, "prop_static_list") + title_source_name = obs.obs_data_get_string(settings, "prop_title_list") + ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") + link_text = obs.obs_data_get_bool(settings, "do_link_text") + link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") +end + +-- A function named script_defaults will be called to set the default settings +function script_defaults(settings) + dbg_method("script_defaults") + obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) + obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + if windows_os then + os.execute('mkdir "' .. get_songs_folder_path() .. '"') + else + os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') end - pp = obs.obs_properties_get(script_props,"ctrl_grp") - obs.obs_property_set_visible(pp, true) +end - obs.obs_properties_apply_settings(script_props, script_sets) - return script_props +--verify source has an opacity setting +function isValid(source) + if source ~= nil then + local flags = obs.obs_source_get_output_flags(source) + print(obs.obs_source_get_name(source) .. " - " .. flags) + local targetFlag = obs.OBS_SOURCE_VIDEO+obs.OBS_SOURCE_CUSTOM_DRAW+obs.OBS_SOURCE_SRGB + if bit.band(flags, 32777) == 32777 then + return true + end + end + return false +end + + +-- adds an extra linked source. +-- Source must be text source, or have 'Color Correction' Filter applied +function link_source_selected(props, prop, settings) + dbg_method("link_source_selected") + local extra_source = obs.obs_data_get_string(settings, "extra_source_list") + if extra_source ~= nil and extra_source ~= "" then + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) + obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) + obs.obs_data_set_string(script_sets, "extra_source_list", "") + obs.obs_property_set_description(extra_linked_list, "Visibility Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")") + end + return true +end + +-- removes linked sources +function do_linked_clicked(props, p) + dbg_method("do_link_clicked") + obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), true) + obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), false) + obs.obs_properties_apply_settings(props, script_sets) + + return true +end + +-- removes linked sources +function clear_linked_clicked(props, p) + dbg_method("clear_linked_clicked") + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_clear(extra_linked_list) + obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), true) +-- obs.obs_properties_apply_settings(props, script_sets) + + return true +end + + +-- removes linked sources +function clear_callback(props, prop, settings) + dbg_method("clear_link_callback") + clear_linked_clicked(props,prop) + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + print("SET") + if count > 0 then + print("on") + obs.obs_property_set_visible(prop, true) + else + print("off") + obs.obs_property_set_visible(prop, false) + end + return true end -- A function named script_description returns the description shown to -- the user -local description = [[ -"Manage song lyrics to be displayed as subtitles (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from taxilian.
-
Song
Title
-
- - - - - - - -
Markup  Syntax       Markup  Syntax
Display n Lines  #L:nEnd Page after Line  Line ###
Blank(Pad) Line  ##B or ##PBlank(Pad) Lines  #B:n or #P:n
External Refrain  #r[ and #r]In-Line Refrain  #R[ and #R]
Repeat Refrain  ##R or ##rDuplicate Line n times  #D:n Line
Define Static Lines  #S[ and #S]Single Static Line  #S: Line
Define Alternate Text  #A[ and #A]Alt Repeat n Pages  #A:n Line
Comment Line  // LineBlock Comments  //[ and //]
Titles with invalid filename characters are encoded for compatiblity
Option is to markup override title with #T:title text inside lyrics
Optional comma delimeted meta tags following //meta on 1st line
-]] - function script_description() - return "Manage song Lyrics and Other Paged Text (Version: September 2021 (beta2)
Author: Amirchev & DC Strato; with significant contributions from Taxilian.
" + return description end + +function vMode(vis) + return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" +end function expand_all_groups(props, prop, settings) expandcollapse = not expandcollapse obs.obs_property_set_visible(obs.obs_properties_get(script_props,"info_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"prep_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props,"mng_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"disp_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"src_grp"), expandcollapse) obs.obs_property_set_visible(obs.obs_properties_get(script_props,"ctrl_grp"), expandcollapse) - local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if expandcollapse then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(expandecollapse) obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) @@ -1877,52 +1990,42 @@ function expand_all_groups(props, prop, settings) end -function all_vis_equal() - return (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and + + +function all_vis_equal(props) + if (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) and obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) or not (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props,"mng_grp")) or obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) or obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) + obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) then + expandcollapse = not expandcollapse + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + end end function change_info_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"info_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) - local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end function change_prepared_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"prep_grp") + local pp = obs.obs_properties_get(script_props,"mng_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end @@ -1930,17 +2033,9 @@ function change_options_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"disp_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end @@ -1948,17 +2043,9 @@ function change_src_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"src_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end @@ -1966,22 +2053,16 @@ function change_ctrl_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props,"ctrl_grp") local vis = not obs.obs_property_visible(pp) obs.obs_property_set_visible(pp,vis) -local mode1 = "▼░ SHOW " - local mode2 = "░▼" - if vis then - mode1 = "▲░ HIDE " - mode2 = "░▲" - end + local mode1, mode2 = vMode(vis) obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - if all_vis_equal() then - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - expandcollapse = not expandcollapse - end + all_vis_equal(props) return true end function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") + dbg_bool("Fade: ",text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) return true @@ -2005,8 +2086,8 @@ function setEditVis(props, prop, settings) -- hides edit group on initial showin local pp = obs.obs_properties_get(script_props,"edit_grp") obs.obs_property_set_visible(pp, false) pp = obs.obs_properties_get(props,"meta") - obs.obs_property_set_visible(pp, false) - editVisSet = true -- do this only once + obs.obs_property_set_visible(pp, false) + editVisSet = true end end @@ -2015,7 +2096,7 @@ function filter_songs_clicked(props, p) if not obs.obs_property_visible(pp) then obs.obs_property_set_visible(pp, true) local mpb = obs.obs_properties_get(props, "filter_songs_button") - obs.obs_property_set_description(mpb, "Clear Song Filters") -- change button function + obs.obs_property_set_description(mpb, "Clear Filters") -- change button function meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") refresh_directory() else @@ -2023,7 +2104,7 @@ function filter_songs_clicked(props, p) meta_tags = "" -- clear meta tags refresh_directory() local mpb = obs.obs_properties_get(props, "filter_songs_button") -- - obs.obs_property_set_description(mpb, "Filter Songs by Meta Tags") -- reset button function + obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function end return true end @@ -2033,12 +2114,11 @@ function edit_prepared_clicked(props, p) if obs.obs_property_visible(pp) then obs.obs_property_set_visible(pp, false) local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + obs.obs_property_set_description(mpb, "Edit Prepared List") return true end local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") local count = obs.obs_property_list_item_count(prop_prep_list) - dbg_inner("count: " .. count) local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) if count2 > 0 then @@ -2058,7 +2138,7 @@ function edit_prepared_clicked(props, p) obs.obs_data_array_release(songNames) obs.obs_property_set_visible(pp, true) local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Cancel Prepared Song Edits") + obs.obs_property_set_description(mpb, "Cancel Prepared Edits") return true end @@ -2078,8 +2158,10 @@ function save_edits_clicked(props, p) prepared_songs[#prepared_songs+1] = itemName obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) end + obs.obs_data_release(item) end end + obs.obs_data_array_release(songNames) save_prepared() if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) @@ -2105,69 +2187,7 @@ function change_transition_property(props, prop, settings) transition_enabled = transition_set return true end --- A function named script_update will be called when settings are changed -function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") -- Fade Enable (WZ) - text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") -- Fade Speed (WZ) - reload = false - local cur_display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") - if display_lines ~= cur_display_lines then - display_lines = cur_display_lines - reload = true - end - local cur_source_name = obs.obs_data_get_string(settings, "prop_source_list") - if source_name ~= cur_source_name then - source_name = cur_source_name - reload = true - end - local alt_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") - if alternate_source_name ~= alt_source_name then - alternate_source_name = alt_source_name - reload = true - end - local stat_source_name = obs.obs_data_get_string(settings, "prop_static_list") - if static_source_name ~= stat_source_name then - static_source_name = stat_source_name - reload = true - end - local cur_title_source = obs.obs_data_get_string(settings, "prop_title_list") - if title_source_name ~= cur_title_source then - title_source_name = cur_title_source - reload = true - end - local cur_ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") - if cur_ensure_lines ~= ensure_lines then - ensure_lines = cur_ensure_lines - reload = true - end - local cur_link_text = obs.obs_data_get_bool(settings, "link_text") - if cur_link_text ~= link_text then - link_text = cur_link_text - reload = true - end - - if reload then - if #prepared_songs > 0 and prepared_songs[prepared_index] ~= "" then - prepare_selected(prepared_songs[prepared_index]) - end - end - -end - --- A function named script_defaults will be called to set the default settings -function script_defaults(settings) - dbg_method("script_defaults") - obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - if windows_os then - os.execute('mkdir "' .. get_songs_folder_path() .. '"') - else - os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') - end -end -- A function named script_save will be called when the script is saved function script_save(settings) @@ -2200,49 +2220,96 @@ function script_save(settings) hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) + --- + --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded + --- + local extra_sources_array = obs.obs_data_array_create() + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + for i = 0, count-1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", source_name) + obs.obs_data_array_push_back(extra_sources_array,array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) + obs.obs_data_array_release(extra_sources_array) end --- a function named script_load will be called on startup + +-- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS +-- sets callback to obs_frontend Event Callback +-- function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) - local hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric", " ......................") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") + hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric", " ..................") obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") + hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics", " ..............") obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") + hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Prepared", " ................") obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") + hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared", "............") obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") + hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start", " ..........") obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) - hotkey_reset_id = - obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") + hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared", " .......") obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) script_sets = settings source_name = obs.obs_data_get_string(settings, "prop_source_list") + + extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + + -- load previously defined extra sources from settings array into table + -- script_properties function will take them from the table and restore them as UI properties + -- + local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + local count = obs.obs_data_array_count(extra_sources_array) + if count > 0 then + for i = 0, count do + local item = obs.obs_data_array_item(extra_sources_array, i); + local sourceName = obs.obs_data_get_string(item, "value"); + if sourceName ~= "" then + extra_sources[#extra_sources + 1] = sourceName + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(extra_sources_array) + + + -- load prepared songs from stored file + -- if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -2253,119 +2320,302 @@ function script_load(settings) for line in file:lines() do prepared_songs[#prepared_songs + 1] = line end - --prepared_index = 1 file:close() end - local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") - local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") - local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") - obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) - obs.obs_property_set_enabled(fade_speed_prop, not transition_set) - transition_enabled = transition_set + name_hotkeys() + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end +--- +------ +--------- Source Showing or Source Active Helper Functions +--------- Return true if sourcename given is showing anywhere or on in the Active scene +------ +--- +function isShowing(sourceName) + local source = obs.obs_get_source_by_name(sourceName) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function isActive(sourceName) + local source = obs.obs_get_source_by_name(sourceName) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function anythingShowing() + return isShowing(source_name) or isShowing(alternate_source_name) + or isShowing(title_source_name) or isShowing(static_source_name) +end + +function sourceShowing() + return isShowing(source_name) +end + +function alternateShowing() + return isShowing(alternate_source_name) +end + +function titleShowing() + return isShowing(title_source_name) +end + +function staticShowing() + return isShowing(static_source_name) +end + +function anythingActive() + return isActive(source_name) or isActive(alternate_source_name) + or isActive(title_source_name) or isActive(static_source_name) +end + +function sourceActive() + return isActive(source_name) +end + +function alternateActive() + return isActive(alternate_source_name) +end + +function titleActive() + return isActive(title_source_name) +end + +function staticActive() + return isActive(static_source_name) +end + +--- +------ +--------- Initialization Functions +--------- Manages defined Hotkey Save, Load, Translate and Button rename +--------- Loads inital song directory and any previously prepared lyrics +------ +--- + +---------------------------------------------------------------------------------------------------------- +-- get_hotkeys(loaded hotkey array, desired prefix text, leader text (between prefix and hotkey label) +-- Returns translated hotkey text label with prefix and leader +-- e.g. if HotKeyArray contains an assigned hotkey Shift and F1 key combo, then +-- get_hotkeys(HotKeyArray," ....... ", "HotKey") returns "Hotkey ....... Shift + F1" +---------------------------------------------------------------------------------------------------------- + +function get_hotkeys(hotkey_array, prefix, leader) + local Translate = {["NUMLOCK"] = "NumLock", ["NUMSLASH"] = "Num/", ["NUMASTERISK"] = "Num*", + ["NUMMINUS"] = "Num-", ["NUMPLUS"] = "Num+", + ["NUMPERIOD"] = "NumDel", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page-Down", + ["PAGEUP"] = "Page-Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", + ["UP"] = "Up", ["DOWN"] = "Down", ["RIGHT"] = "Right", ["LEFT"] = "Left", + ["SCROLLLOCK"] = "Scroll-Lock", ["BACKSPACE"] = "Backspace", ["ESCAPE"] = "Esc", + ["MENU"] = "Menu", ["META"] = "Meta", ["PRINT"] = "Prt", ["TAB"] = "Tab", + ["DELETE"] = "Del", ["CAPSLOCK"] = "Caps-Lock", ["NUMEQUAL"] = "Num=", ["PAUSE"] = "Pause", + ["VK_VOLUME_MUTE"] = "Vol Mute", ["VK_VOLUME_DOWN"] = "Vol Dwn", ["VK_VOLUME_UP"] = "Vol Up", + ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", ["VK_MEDIA_STOP"] = "Media Stop", + ["VK_MEDIA_PREV_TRACK"] = "Media Prev", ["VK_MEDIA_NEXT_TRACK"] = "Media Next"} + + item = obs.obs_data_array_item(hotkey_array, 0) + local key = string.sub(obs.obs_data_get_string(item,"key"),9) + if Translate[key] ~= nil then + key = Translate[key] + elseif string.sub(key,1,3) == "NUM" then + key = "Num " .. string.sub(key,4) + elseif string.sub(key,1,5) == "MOUSE" then + key = "Mouse " .. string.sub(key,6) + end + + obs.obs_data_release(item) + local val = prefix + if key ~= nil and key ~= "" then + val = val .. " " .. leader .. " " + if obs.obs_data_get_bool(item,"control") then val = val.."Ctrl + " end + if obs.obs_data_get_bool(item,"alt") then val = val.."Alt + " end + if obs.obs_data_get_bool(item,"shift") then val = val.."Shift + " end + if obs.obs_data_get_bool(item,"command") then val = val.."Cmd + " end + val = val .. key + end + return val +end + +-- name_hotkeys function renames the seven hotkeys to include their defined key text +-- +function name_hotkeys() + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) +end + + -------- ---------------- ------------------------ SOURCE FUNCTIONS ---------------- -------- --- Function renames source to a unique descriptive name and marks duplicate sources with * (WZ) -function index_sources() +-- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change +-- +function rename_source() + -- pause_timer = true local sources = obs.obs_enum_sources() if (sources ~= nil) then -- count and index sources local t = 1 for _, source in ipairs(sources) do - local settings = obs.obs_source_get_settings(source) - if obs.obs_data_get_bool(settings,"isLLC") then + local source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "Prepare_Lyrics" then + local settings = obs.obs_source_get_settings(source) obs.obs_data_set_string(settings, "index", t) -- add index to source data t = t + 1 - end - obs.obs_data_release(settings) -- release memory + obs.obs_data_release(settings) -- release memory + end + end + -- Find and mark Duplicates in loadLyric_items table + local loadLyric_items = {} -- Start Table for all load Sources + local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items + if scenes ~= nil then + for _, scenesource in ipairs(scenes) do -- Loop through all scenes + local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer + local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id + if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) + if loadLyric_items[index] == nil then + loadLyric_items[index] = 1 -- First time to find this source so mark with 1 + else + loadLyric_items[index] = loadLyric_items[index]+1 -- Found this source again so increment + end + obs.obs_data_release(settings) -- release memory + end + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + end + obs.source_list_release(scenes) -- Free source list end - end - local loadLyric_items = {} -- Start Table for all load Sources - local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items - if scenes ~= nil then - for _, scenesource in ipairs(scenes) do -- Loop through all scenes - local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer - local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - if obs.obs_data_get_bool(settings,"isLLC") then - local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) - obs.obs_data_set_bool(settings,"duplicate", loadLyric_items[index]) -- false (nil) the first time - loadLyric_items[index] = true -- duplicate (true) if used again - end - obs.obs_data_release(settings) -- release memory - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - end - obs.source_list_release(scenes) -- Free source list - end - obs.source_list_release(sources) -end -function rename_sources() - local sources = obs.obs_enum_sources() - if (sources ~= nil) then - -- count and index sources - local t = 1 + -- Name Source with Song Title + local i = 1 for _, source in ipairs(sources) do - local settings = obs.obs_source_get_settings(source) - if obs.obs_data_get_bool(settings,"isLLC") then - local name = obs.obs_data_get_string(settings, "index") .. ". Load lyrics for: " .. - obs.obs_data_get_string(settings, "songs") .. "" -- use index for compare - if obs.obs_data_get_bool(settings, "duplicate") then - name = '' .. name .. " * " - end - obs.obs_source_set_name(source, name) - end - obs.obs_data_release(settings) -- release memory + local source_id = obs.obs_source_get_unversioned_id(source) -- Get source + if source_id == "Prepare_Lyrics" then -- Skip if not a Load Lyric source + local c_name = obs.obs_source_get_name(source) -- Get current Source Name + local settings = obs.obs_source_get_settings(source) -- Get settings for this source + local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load + local index = obs.obs_data_get_string(settings, "index") -- get index + if (song ~= nil) then + local name = "Load lyrics for: " .. song .. "" -- use index for compare + -- Mark Duplicates + if index ~= nil then + if loadLyric_items[index] > 1 then + name = '' .. name .. " " .. loadLyric_items[index] .. "" + end + if (c_name ~= name) then + obs.obs_source_set_name(source, name) + end + end + i = i + 1 + end + obs.obs_data_release(settings) + end end - obs.source_list_release(sources) end + obs.source_list_release(sources) + -- pause_timer = false end +-- Names the initial "Prepare Lyric" source (prior to being renamed to "Load Lyrics for: {song name} +-- source_def.get_name = function() return "Prepare Lyric" end -saved = false - +-- Called when OBS is saving data. This will be called on each copy of Load Lyric source +-- Used to initiate rename_source() function when the source dialog closes +-- saved flag prevents it from being called by every source each time. +-- source_def.save = function(data, settings) - + if saved then return end -- we only need it once, not for every load lyric source copy + dbg_method("Source_save") + saved = true + using_source = true + rename_source() -- Rename and Mark sources instantly on update (WZ) end +-- Called when a change is made in the source dialog (Currently Not Used) +-- source_def.update = function(data, settings) - saved = false -- mark properties changed - source_sets = settings -- saved so the actual SAVE callback can update the right song +dbg_method("update") +end + +-- Called when the source dialog is loaded (Currently not Used) +-- +source_def.load = function(data) +dbg_method("load") end +-- Called when the refresh button is pressed in the source dialog +-- It reloads the song directory and applies any meta-tag filters if entered +-- function source_refresh_button_clicked(props, p) dbg_method("source_refresh_button") source_filter = true + dbg_inner("tags: " .. source_meta_tags) load_source_song_directory(true) table.sort(song_directory) - local prop_dir_list = obs.obs_properties_get(props,"source_directory_list") + local prop_dir_list = obs.obs_properties_get(props,"songs") obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do + dbg_inner("SLD: " .. name) obs.obs_property_list_add_string(prop_dir_list, name, name) end return true end +-- Keeps variable source-meta-tags up-to-date +-- Note: This could be done only when refreshing the directory (see source_refresh_button_clicked) +-- +function update_source_metatags(props, p, settings) + source_meta_tags = obs.obs_data_get_string(settings,"metatags") + return true +end + +-- Called when a user makes a song selection in the source dialog +-- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens +-- Saved flag is cleared to mark changes have occured for save event +-- +function source_selection_made(props, prop, settings) +dbg_method("source_selection") + local name = obs.obs_data_get_string(settings,"songs") + saved = false -- mark properties changed + using_source = true + prepare_selected(name) + return true +end + +-- Standard OBS get Properties function for OBS source dialog +-- source_def.get_properties = function(data) source_filter = true load_source_song_directory(true) local source_props = obs.obs_properties_create() - index_sources() local source_dir_list = obs.obs_properties_add_list( source_props, @@ -2374,82 +2624,107 @@ source_def.get_properties = function(data) obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) + obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(source_dir_list, name, name) end gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "source_prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) + dbg_inner("props") return source_props + end +-- Called when the source is created +-- saves pointer to settings in global sourc_sets for convienence +-- Sets callbacks for active, showing, deactive, and updated callbacks +-- source_def.create = function(settings, source) dbg_method("create") data = {} source_sets = settings - obs.obs_data_set_string(settings,"index",nil) - obs.obs_data_set_bool(settings,"duplicate",false) - obs.obs_data_set_bool(settings,"isLLC",true) obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "save", source_save) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback return data end +-- Sets default settings for Activate Source in Preview +-- source_def.get_defaults = function(settings) obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) - obs.obs_data_set_default_string(settings, "index", "0") end +-- On Event Functions +-- These manage keeping the HTML monitor page updated when changes happen like scene changes that remove +-- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are +-- either created or deleted. Rename changes color and marks with *, sources that are reference copies of the same source +-- as accidentally changing the settings like the loaded song in one will change it in the reference copies. +-- + +-- Called via the timed callback, removes the callback and updates the HTML monitor page +-- function update_source_callback() obs.remove_current_callback() update_monitor() end +-- called via the timed callback, removes the callback and renames all the load sources +-- function rename_callback() obs.remove_current_callback() - rename_sources() + rename_source() end +-- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) dbg_method("on_event: " .. event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page dbg_bool("Active:",source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end - if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then - dbg_inner("Scene Change") - obs.timer_add(rename_callback, 1000) + if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes + dbg_inner("Scene Change") + obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change end end +-- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. +-- function load_source_song(source, preview) - dbg_method("load_source_song") + dbgsp("load_source_song") local settings = obs.obs_source_get_settings(source) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - using_source = true - load_source = source - set_text_visibility(TEXT_HIDDEN) - prepare_selected(song) + using_source = true + load_source = source + all_sources_fade = true -- fade title and source the first time + set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in + if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles + prepare_selected(song) + end transition_lyric_text() - if obs.obs_data_get_bool(settings, "source_home_on_active") then - home_prepared(true) - end + if obs.obs_data_get_bool(settings, "source_home_on_active") then + home_prepared(true) + end end obs.obs_data_release(settings) end - +-- Call back when load source (not text source) goes to the Active Scene +-- loads the selected song and sets the current scene name for the HTML monitor +-- function source_isactive(cd) - dbg_method("source_active") + dbg_custom("source_active") local source = obs.calldata_source(cd, "source") if source == nil then return @@ -2460,25 +2735,9 @@ function source_isactive(cd) source_active = true -- using source lyric end -function source_save(cd) - dbg_inner("source save") - local source = obs.calldata_source(cd, "source") - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - dbg_inner("Index: " .. obs.obs_data_get_string(settings,"index")) - dbg_bool("Duplicate: ", obs.obs_data_get_bool(settings,"duplicate")) - --using_source = true - --prepare_selected(obs.obs_data_get_string(source_sets, "songs")) -- show song to user - local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load - local index = obs.obs_data_get_string(settings, "index") -- get index - local duplicate = obs.obs_data_get_bool(settings,"duplicate") - obs.obs_data_release(settings) -- release memory - local name = index .. ". Load lyrics for: " .. song .. "" -- use index for compare - if duplicate then - name = '' .. name .. " * " - end - obs.obs_source_set_name(source, name) -end - +-- Call back when load source leaves the current Active Scene +-- just resets the source_active flag +-- function source_inactive(cd) dbg_inner("source inactive") local source = obs.calldata_source(cd, "source") @@ -2488,8 +2747,11 @@ function source_inactive(cd) source_active = false -- indicates source loading lyric is active (but using prepared lyrics is still possible) end +-- Call back when load source (not text source) goes to the Active +-- loads the selected song and sets the current scene name for the HTML monitor +-- function source_showing(cd) - dbg_method("source_showing") + dbg_custom("source_showing") local source = obs.calldata_source(cd, "source") if source == nil then return @@ -2497,6 +2759,14 @@ function source_showing(cd) load_source_song(source, true) end +-- dbg functions +-- +function dbg_traceback() + if DEBUG then + print("Trace: " .. debug.traceback()) + end +end + function dbg(message) if DEBUG then print(message) @@ -2515,6 +2785,11 @@ function dbg_method(message) end end +function dbgsp(message) +if DEBUG then + dbg("====SPECIAL=====================>> " .. message) +end +end function dbg_custom(message) if DEBUG_CUSTOM then dbg("CUST: " .. message) @@ -2533,5 +2808,8 @@ function dbg_bool(name, value) end end - obs.obs_register_source(source_def) + +description = [[ +

OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian

+]] \ No newline at end of file From 86fd88112f44824ab3a79f9c29caed717ee9edd4 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Fri, 15 Oct 2021 23:16:19 -0600 Subject: [PATCH 051/105] Update lyrics.lua Just playing with the Wording in how sources Link --- lyrics.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index a264ecb..666d554 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -726,7 +726,7 @@ end -- transition to the next lyrics, use fade if enabled -- if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) - dbgsp("transition_lyric_text") + dbg_method("transition_lyric_text") dbg_bool("force show", force_show) -- update the lyrics display immediately on 2 conditions -- a) the text is hidden or hiding, and we will not force it to show @@ -759,7 +759,6 @@ end function update_source_text() dbg_method("update_source_text") dbg_custom("Page Index: " .. page_index) - dbg_traceback() local text = "" local alttext = "" local next_lyric = "" @@ -1760,7 +1759,7 @@ function script_properties() local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") local link_prop = - obs.obs_properties_add_bool(gp, "do_link_text", "Link title & static text visibility with lyric text") + obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") local transition_prop = obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") @@ -1811,7 +1810,7 @@ function script_properties() ) obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) xgp = obs.obs_properties_create() - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Also Link Sources to Lyrics Text Visibility") + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") local extra_linked_prop = obs.obs_properties_add_list(xgp,"extra_linked_list","Linked Sources ",obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) -- initialize previously loaded extra properties from table for _, sourceName in ipairs(extra_sources) do @@ -1820,7 +1819,7 @@ function script_properties() local extra_source_prop = obs.obs_properties_add_list(xgp,"extra_source_list"," Select Source:",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) - local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Hide/Show Visibility Linked Sources ", obs.OBS_GROUP_NORMAL,xgp) + local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL,xgp) obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) local count = obs.obs_property_list_item_count(extra_linked_prop) if count > 0 then @@ -1830,7 +1829,7 @@ function script_properties() end local sources = obs.obs_enum_sources() - obs.obs_property_list_add_string(extra_source_prop, "", "") + obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") if sources ~= nil then local n = {} for _, source in ipairs(sources) do @@ -1900,8 +1899,8 @@ function isValid(source) if source ~= nil then local flags = obs.obs_source_get_output_flags(source) print(obs.obs_source_get_name(source) .. " - " .. flags) - local targetFlag = obs.OBS_SOURCE_VIDEO+obs.OBS_SOURCE_CUSTOM_DRAW+obs.OBS_SOURCE_SRGB - if bit.band(flags, 32777) == 32777 then + local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO,obs.OBS_SOURCE_CUSTOM_DRAW) + if bit.band(flags, targetFlag) == targetFlag then return true end end From 646c3a1645de27d010ecba13c2b418adfe8ffdfe Mon Sep 17 00:00:00 2001 From: wzaggle Date: Fri, 15 Oct 2021 23:23:10 -0600 Subject: [PATCH 052/105] Lyrics+ ? Considering renaming to Lyrics+ --- lyricstrial.lua => lyrics+.lua | 49 ++++++++++++---------------------- 1 file changed, 17 insertions(+), 32 deletions(-) rename lyricstrial.lua => lyrics+.lua (98%) diff --git a/lyricstrial.lua b/lyrics+.lua similarity index 98% rename from lyricstrial.lua rename to lyrics+.lua index b97fe50..666d554 100644 --- a/lyricstrial.lua +++ b/lyrics+.lua @@ -726,7 +726,7 @@ end -- transition to the next lyrics, use fade if enabled -- if lyrics are hidden, force_show set to true will make them visible function transition_lyric_text(force_show) - dbgsp("transition_lyric_text") + dbg_method("transition_lyric_text") dbg_bool("force show", force_show) -- update the lyrics display immediately on 2 conditions -- a) the text is hidden or hiding, and we will not force it to show @@ -759,7 +759,6 @@ end function update_source_text() dbg_method("update_source_text") dbg_custom("Page Index: " .. page_index) - dbg_traceback() local text = "" local alttext = "" local next_lyric = "" @@ -1760,7 +1759,7 @@ function script_properties() local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") local link_prop = - obs.obs_properties_add_bool(gp, "do_link_text", "Link title & static text visibility with lyric text") + obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") local transition_prop = obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") @@ -1811,22 +1810,26 @@ function script_properties() ) obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) xgp = obs.obs_properties_create() - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Link Visibility to Lyrics") - local extra_linked_prop = obs.obs_properties_add_list(xgp,"extra_linked_list","Linked Sources",obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") + local extra_linked_prop = obs.obs_properties_add_list(xgp,"extra_linked_list","Linked Sources ",obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) -- initialize previously loaded extra properties from table for _, sourceName in ipairs(extra_sources) do obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) end - local extra_source_prop = obs.obs_properties_add_list(xgp,"extra_source_list"," Valid Sources:",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) + local extra_source_prop = obs.obs_properties_add_list(xgp,"extra_source_list"," Select Source:",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) - obs.obs_property_set_modified_callback(clearcall_prop, clear_callback) - local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Sources ", obs.OBS_GROUP_NORMAL,xgp) + local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL,xgp) obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) - + local count = obs.obs_property_list_item_count(extra_linked_prop) + if count > 0 then + obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") + else + obs.obs_property_set_visible(extra_group_prop, false) + end local sources = obs.obs_enum_sources() - obs.obs_property_list_add_string(extra_source_prop, "", "") + obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") if sources ~= nil then local n = {} for _, source in ipairs(sources) do @@ -1858,7 +1861,6 @@ function script_properties() obs.obs_property_set_enabled(hktitletext,false) obs.obs_property_set_visible(edit_group_prop, false) obs.obs_property_set_visible(meta_group_prop, false) - print("PROP") return script_props end @@ -1897,8 +1899,8 @@ function isValid(source) if source ~= nil then local flags = obs.obs_source_get_output_flags(source) print(obs.obs_source_get_name(source) .. " - " .. flags) - local targetFlag = obs.OBS_SOURCE_VIDEO+obs.OBS_SOURCE_CUSTOM_DRAW+obs.OBS_SOURCE_SRGB - if bit.band(flags, 32777) == 32777 then + local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO,obs.OBS_SOURCE_CUSTOM_DRAW) + if bit.band(flags, targetFlag) == targetFlag then return true end end @@ -1916,7 +1918,7 @@ function link_source_selected(props, prop, settings) obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) obs.obs_data_set_string(script_sets, "extra_source_list", "") - obs.obs_property_set_description(extra_linked_list, "Visibility Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")") + obs.obs_property_set_description(extra_linked_list, "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")") end return true end @@ -1938,29 +1940,12 @@ function clear_linked_clicked(props, p) obs.obs_property_list_clear(extra_linked_list) obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), false) obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), true) --- obs.obs_properties_apply_settings(props, script_sets) + obs.obs_property_set_description(extra_linked_list, "Linked Sources ") return true end --- removes linked sources -function clear_callback(props, prop, settings) - dbg_method("clear_link_callback") - clear_linked_clicked(props,prop) - local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - print("SET") - if count > 0 then - print("on") - obs.obs_property_set_visible(prop, true) - else - print("off") - obs.obs_property_set_visible(prop, false) - end - return true -end - -- A function named script_description returns the description shown to -- the user From b3e419665d6dfe1b74d54fc2746ae58cf0a97426 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Fri, 15 Oct 2021 23:41:45 -0600 Subject: [PATCH 053/105] Just ran it through Beautify again Beautified --- lyrics+.lua | 1834 +++++++++++++++++++++++++++------------------------ lyrics.lua | 1834 +++++++++++++++++++++++++++------------------------ 2 files changed, 1972 insertions(+), 1696 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 666d554..523d8c4 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -12,7 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. - obs = obslua bit = require("bit") @@ -37,7 +36,6 @@ first_open = true display_lines = 0 ensure_lines = true - -- lyrics/alternate lyrics by page lyrics = {} alternate = {} @@ -45,21 +43,21 @@ alternate = {} -- verse indicies if marked verses = {} -page_index = 0 -- current page of lyrics being displayed +page_index = 0 -- current page of lyrics being displayed prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected -song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) -prepared_songs = {} -- holds pre-prepared list of songs to use -extra_sources = {} -- holder for extra sources settings +song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) +prepared_songs = {} -- holds pre-prepared list of songs to use +extra_sources = {} -- holder for extra sources settings link_text = false -- true if Title and Static should fade with text only during hide/show -link_extras = false -- extras fade with text always when true, only during hide/show when false +link_extras = false -- extras fade with text always when true, only during hide/show when false all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) -load_scene = "" -- name of scene loading a lyric with a source +load_scene = "" -- name of scene loading a lyric with a source last_prepared_song = "" -- name of the last prepared song (prevents duplicate loading of already loaded song) -- hotkeys @@ -95,7 +93,7 @@ mon_alt = "" mon_nextalt = "" mon_nextsong = "" meta_tags = "" -source_meta_tags = "" +source_meta_tags = "" -- text status & fade TEXT_VISIBLE = 0 -- text is visible @@ -115,10 +113,10 @@ load_source = nil expandcollapse = true showhelp = false -transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) +transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false -source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now +source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now editVisSet = false @@ -135,7 +133,6 @@ DEBUG_METHODS = true -- print method names ---------------- -------- - function next_lyric(pressed) if not pressed then return @@ -179,58 +176,58 @@ function prev_prepared(pressed) if not pressed then return end - if #prepared_songs == 0 then - return - end - if using_source then - using_source = false - prepare_selected(prepared_songs[prepared_index]) - return - end - if prepared_index > 1 then - using_source = false - prepare_selected(prepared_songs[prepared_index - 1]) - return - end - if not source_active or using_source then - using_source = false - prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared - else - using_source = true - prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source - load_source_song(load_source, false) - end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) + return + end + if prepared_index > 1 then + using_source = false + prepare_selected(prepared_songs[prepared_index - 1]) + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared + else + using_source = true + prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source + load_source_song(load_source, false) + end end function next_prepared(pressed) if not pressed then return end - if #prepared_songs == 0 then - return - end - if using_source then - using_source = false - dbg_custom("do current prepared") - prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song - return - end - if prepared_index < #prepared_songs then - using_source = false - dbg_custom("do next prepared") - prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared - return - end - if not source_active or using_source then - using_source = false - dbg_custom("do first prepared") - prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available - else - using_source = true - dbg_custom("do source prepared") - prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source - load_source_song(load_source, false) - end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + dbg_custom("do current prepared") + prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song + return + end + if prepared_index < #prepared_songs then + using_source = false + dbg_custom("do next prepared") + prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared + return + end + if not source_active or using_source then + using_source = false + dbg_custom("do first prepared") + prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available + else + using_source = true + dbg_custom("do source prepared") + prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source + load_source_song(load_source, false) + end end function toggle_lyrics_visibility(pressed) @@ -238,9 +235,9 @@ function toggle_lyrics_visibility(pressed) if not pressed then return end - if link_text then - all_sources_fade = true - end + if link_text then + all_sources_fade = true + end if text_status ~= TEXT_HIDDEN then dbg_inner("hiding") set_text_visibility(TEXT_HIDDEN) @@ -284,7 +281,7 @@ function home_prepared(pressed) obs.obs_data_set_string(script_sets, "prop_prepared_list", "") end obs.obs_properties_apply_settings(props, script_sets) - prepared_index = 1 + prepared_index = 1 prepare_selected(prepared_songs[prepared_index]) return true end @@ -408,7 +405,7 @@ function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") if #prepared_songs == 0 then set_text_visibility(TEXT_HIDDEN) - end + end prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) @@ -425,24 +422,24 @@ function refresh_button_clicked(props, p) local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") local static_source_prop = obs.obs_properties_get(props, "prop_static_list") local title_source_prop = obs.obs_properties_get(props, "prop_title_list") - local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") + local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") obs.obs_property_list_clear(source_prop) -- clear current properties list obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list obs.obs_property_list_clear(static_source_prop) -- clear current properties list obs.obs_property_list_clear(title_source_prop) -- clear current properties list obs.obs_property_list_clear(extra_source_prop) -- clear extra sources list - - obs.obs_property_list_add_string(extra_source_prop, "", "") - + + obs.obs_property_list_add_string(extra_source_prop, "", "") + local sources = obs.obs_enum_sources() if sources ~= nil then local n = {} for _, source in ipairs(sources) do - local name = obs.obs_source_get_name(source) - if isValid(source) then - obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list - end + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then n[#n + 1] = name @@ -460,36 +457,38 @@ function refresh_button_clicked(props, p) obs.obs_property_list_add_string(static_source_prop, name, name) end end - obs.source_list_release(sources) - refresh_directory() - + obs.source_list_release(sources) + refresh_directory() + return true end function refresh_directory_button_clicked(props, p) -dbg_method("refresh directory") - refresh_directory() + dbg_method("refresh directory") + refresh_directory() return true end function refresh_directory() - local prop_dir_list = obs.obs_properties_get(script_props,"prop_directory_list") + local prop_dir_list = obs.obs_properties_get(script_props, "prop_directory_list") local source_prop = obs.obs_properties_get(props, "prop_source_list") - source_filter = false + source_filter = false load_source_song_directory(true) table.sort(song_directory) - obs.obs_property_list_clear(prop_dir_list) -- clear directories + obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do - dbg_inner(name) + dbg_inner(name) obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_properties_apply_settings(script_props, script_sets) -end - + end + obs.obs_properties_apply_settings(script_props, script_sets) +end --- Called with ANY change to the prepared song list +-- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) - obs.obs_property_set_description(obs.obs_properties_get(props, "prep_grp"), " Prepared Songs/Text (" .. #prepared_songs .. ")") + obs.obs_property_set_description( + obs.obs_properties_get(props, "prep_grp"), + " Prepared Songs/Text (" .. #prepared_songs .. ")" + ) dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false @@ -500,10 +499,10 @@ end -- removes prepared songs function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") - prepared_songs = {} -- required for monitor page - page_index = 0 -- required for monitor page - prepared_index = 0 -- required for monitor page - update_source_text() -- required for monitor page + prepared_songs = {} -- required for monitor page + page_index = 0 -- required for monitor page + prepared_index = 0 -- required for monitor page + update_source_text() -- required for monitor page -- clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) @@ -515,25 +514,23 @@ end function prepare_selected(name) dbg_method("prepare_selected") - -- try to prepare song - if prepare_song_by_name(name) then - page_index = 1 - if not using_source then - prepared_index = get_index_in_list(prepared_songs, name) - else - source_song_title = name - all_sources_fade = true - end - - transition_lyric_text(using_source) - - - else - -- hide everything if unable to prepare song - -- TODO: clear lyrics entirely after text is hidden - set_text_visibility(TEXT_HIDDEN) - end - + -- try to prepare song + if prepare_song_by_name(name) then + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + else + source_song_title = name + all_sources_fade = true + end + + transition_lyric_text(using_source) + else + -- hide everything if unable to prepare song + -- TODO: clear lyrics entirely after text is hidden + set_text_visibility(TEXT_HIDDEN) + end + --update_source_text() return true end @@ -591,7 +588,7 @@ end -------- function apply_source_opacity() --- dbg_method("apply_source_visiblity") + -- dbg_method("apply_source_visiblity") local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero @@ -602,7 +599,7 @@ function apply_source_opacity() end obs.obs_source_release(source) obs.obs_data_release(settings) - + local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -611,73 +608,72 @@ function apply_source_opacity() obs.obs_source_update(alt_source, settings) end obs.obs_source_release(alt_source) - obs.obs_data_release(settings) - dbg_bool("All Sources Fade:", all_sources_fade) - dbg_bool("Link Text:", link_text) + obs.obs_data_release(settings) + dbg_bool("All Sources Fade:", all_sources_fade) + dbg_bool("Link Text:", link_text) if all_sources_fade then - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then obs.obs_source_update(title_source, settings) end - obs.obs_source_release(title_source) - obs.obs_data_release(settings) - - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_release(title_source) + obs.obs_data_release(settings) + + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local static_source = obs.obs_get_source_by_name(static_source_name) if static_source ~= nil then obs.obs_source_update(static_source, settings) end obs.obs_source_release(static_source) - obs.obs_data_release(settings) - end - if link_extras or all_sources_fade then - local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - if count > 0 then - for i = 0, count-1 do - local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - print(source_name) - local extra_source = obs.obs_get_source_by_name(source_name) - if extra_source ~= nil then - source_id = obs.obs_source_get_unversioned_id(extra_source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(extra_source, settings) -- merge new opacity values - obs.obs_data_release(settings) - else -- check for filter named "Color Correction" - local color_filter = obs.obs_source_get_filter_by_name(extra_source,"Color Correction") - if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_source_get_settings(color_filter) - obs.obs_data_set_double(filter_settings,"opacity",text_opacity/100) - obs.obs_source_update(color_filter,filter_settings) - obs.obs_data_release(filter_settings) - obs.obs_source_release(color_filter) - else -- try to just change visibility in the scene - print("No Filter") - local sceneSource = obs.obs_frontend_get_current_scene() - local sceneObj = obs.obs_scene_from_source(sceneSource) - local sceneItem = obs.obs_scene_find_source(sceneObj,source_name) - obs.obs_source_release(scene) - if text_opacity > 50 then - obs.obs_sceneitem_set_visible(sceneItem, true) - else - obs.obs_sceneitem_set_visible(sceneItem, false) - end - end - end - end - obs.obs_source_release(extra_source) -- release source ptr - end - end - end - + obs.obs_data_release(settings) + end + if link_extras or all_sources_fade then + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count - 1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + print(source_name) + local extra_source = obs.obs_get_source_by_name(source_name) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(extra_source, settings) -- merge new opacity values + obs.obs_data_release(settings) + else -- check for filter named "Color Correction" + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_source_get_settings(color_filter) + obs.obs_data_set_double(filter_settings, "opacity", text_opacity / 100) + obs.obs_source_update(color_filter, filter_settings) + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + else -- try to just change visibility in the scene + print("No Filter") + local sceneSource = obs.obs_frontend_get_current_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) + obs.obs_source_release(scene) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end + end end function set_text_visibility(end_status) @@ -686,41 +682,41 @@ function set_text_visibility(end_status) if text_status == end_status then return end - if end_status == TEXT_HIDE then - text_opacity = 0 - text_status = end_status - apply_source_opacity() - return - elseif end_status == TEXT_SHOW then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - apply_source_opacity() - return - end - if text_fade_enabled then + if end_status == TEXT_HIDE then + text_opacity = 0 + text_status = end_status + apply_source_opacity() + return + elseif end_status == TEXT_SHOW then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + apply_source_opacity() + return + end + if text_fade_enabled then -- if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then text_status = TEXT_HIDING elseif end_status == TEXT_VISIBLE then text_status = TEXT_SHOWING end - --all_sources_fade = true + --all_sources_fade = true start_fade_timer() - else -- change visibility immediately (fade or no fade) - if end_status == TEXT_HIDDEN then - text_opacity = 0 - text_status = end_status - elseif end_status == TEXT_VISIBLE then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - end - apply_source_opacity() - --update_source_text() - all_sources_fade = false - return - end + else -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + end + apply_source_opacity() + --update_source_text() + all_sources_fade = false + return + end end -- transition to the next lyrics, use fade if enabled @@ -735,20 +731,20 @@ function transition_lyric_text(force_show) -- fade out transition is complete if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then update_source_text() - -- if text is done hiding, we can cancel the all_sources_fade - if text_status == TEXT_HIDDEN then - all_sources_fade = false - end + -- if text is done hiding, we can cancel the all_sources_fade + if text_status == TEXT_HIDDEN then + all_sources_fade = false + end dbg_inner("hidden") elseif not text_fade_enabled then - dbg_custom("Instant On") - -- if text fade is not enabled, then we can cancel the all_sources_fade - all_sources_fade = false - set_text_visibility(TEXT_VISIBLE) -- does update_source_text() + dbg_custom("Instant On") + -- if text fade is not enabled, then we can cancel the all_sources_fade + all_sources_fade = false + set_text_visibility(TEXT_VISIBLE) -- does update_source_text() update_source_text() dbg_inner("no text fade") - else -- initiate fade out/in - dbg_custom("Transition Timer") + else -- initiate fade out/in + dbg_custom("Transition Timer") text_status = TEXT_TRANSITION_OUT start_fade_timer() end @@ -758,7 +754,7 @@ end -- updates the selected lyrics function update_source_text() dbg_method("update_source_text") - dbg_custom("Page Index: " .. page_index) + dbg_custom("Page Index: " .. page_index) local text = "" local alttext = "" local next_lyric = "" @@ -766,34 +762,33 @@ function update_source_text() local static = static_text local mstatic = static -- save static for use with monitor local title = "" - - - if alt_title ~= "" then - title = alt_title - else - if not using_source then - if prepared_index ~= nil and prepared_index ~= 0 then - dbg_custom("Update from prepared: " .. prepared_index) - title = prepared_songs[prepared_index] - end - else - dbg_custom("Updatefrom source: " .. source_song_title) - title = source_song_title - end - end + + if alt_title ~= "" then + title = alt_title + else + if not using_source then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Update from prepared: " .. prepared_index) + title = prepared_songs[prepared_index] + end + else + dbg_custom("Updatefrom source: " .. source_song_title) + title = source_song_title + end + end local source = obs.obs_get_source_by_name(source_name) local alt_source = obs.obs_get_source_by_name(alternate_source_name) local stat_source = obs.obs_get_source_by_name(static_source_name) local title_source = obs.obs_get_source_by_name(title_source_name) - + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then - if #lyrics > 0 then + if #lyrics > 0 then if lyrics[page_index] ~= nil then text = lyrics[page_index] end end - if #alternate > 0 then + if #alternate > 0 then if alternate[page_index] ~= nil then alttext = alternate[page_index] end @@ -801,14 +796,14 @@ function update_source_text() if link_text then if string.len(text) == 0 and string.len(alttext) == 0 then - --static = "" - --title = "" + --static = "" + --title = "" end end end -- update source texts if source ~= nil then - dbg_inner("Title Load") + dbg_inner("Title Load") local settings = obs.obs_data_create() obs.obs_data_set_string(settings, "text", text) obs.obs_source_update(source, settings) @@ -858,27 +853,27 @@ function update_source_text() next_prepared = prepared_songs[1] -- plan to loop around to first prepared song end end - mon_verse = 0 - if #verses ~= nil then --find valid page Index - for i = 1, #verses do - if page_index >= verses[i]+1 then - mon_verse = i - end - end -- v = current verse number for this page - end - mon_song = title - mon_lyric = text:gsub("\n", "
• ") - mon_nextlyric = next_lyric:gsub("\n", "
• ") - mon_alt = alttext:gsub("\n", "
• ") - mon_nextalt = next_alternate:gsub("\n", "
• ") - mon_nextsong = next_prepared - + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i] + 1 then + mon_verse = i + end + end -- v = current verse number for this page + end + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared + update_monitor() end function start_fade_timer() - dbgsp("started fade timer") - obs.timer_add(fade_callback, 50) + dbgsp("started fade timer") + obs.timer_add(fade_callback, 50) end function fade_callback() @@ -898,9 +893,9 @@ function fade_callback() -- completed fade out, determine next move text_opacity = 0 if text_status == TEXT_TRANSITION_OUT then - -- update to new lyric between fades + -- update to new lyric between fades update_source_text() - -- begin transition back in + -- begin transition back in text_status = TEXT_TRANSITION_IN else text_status = TEXT_HIDDEN @@ -933,15 +928,15 @@ function prepare_song_by_name(name) if name == nil then return false end - last_prepared_song = name + last_prepared_song = name -- if using transition on lyric change, first transition -- would be reset with new song prepared transition_completed = false - -- load song lines + -- load song lines local song_lines = get_song_text(name) - if song_lines == nil then - return false - end + if song_lines == nil then + return false + end local cur_line = 1 local cur_aline = 1 local recordRefrain = false @@ -954,10 +949,10 @@ function prepare_song_by_name(name) local refrain = {} local arefrain = {} lyrics = {} - verses = {} + verses = {} alternate = {} static_text = "" - alt_title = "" + alt_title = "" local adjusted_display_lines = display_lines local refrain_display_lines = display_lines local alternate_display_lines = display_lines @@ -1025,7 +1020,7 @@ function prepare_song_by_name(name) end local static_index = line:find("#S:") if static_index ~= nil then - line = line:sub(static_index+3) + line = line:sub(static_index + 3) static_text = line new_lines = 0 end @@ -1035,12 +1030,12 @@ function prepare_song_by_name(name) line = line:sub(title_indexEnd + 1) alt_title = line new_lines = 0 - end + end local alt_index = line:find("#A:") if alt_index ~= nil then local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) - local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) + local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) line = line:sub(alt_indexEnd + 1) singleAlternate = true end @@ -1093,12 +1088,12 @@ function prepare_song_by_name(name) line = line:sub(1, refrain_index - 1) new_lines = 0 end - + refrain_index = line:find("##R") if refrain_index == nil then - refrain_index = line:find("##r") - end - if refrain_index ~= nil then + refrain_index = line:find("##r") + end + if refrain_index ~= nil then playRefrain = true line = line:sub(1, refrain_index - 1) new_lines = 0 @@ -1112,7 +1107,7 @@ function prepare_song_by_name(name) end newcount_index = line:find("#B:") if newcount_index ~= nil then - new_lines = tonumber(line:sub(newcount_index + 3)) + new_lines = tonumber(line:sub(newcount_index + 3)) line = line:sub(1, newcount_index - 1) end local phantom_index = line:find("##P") @@ -1127,9 +1122,9 @@ function prepare_song_by_name(name) if verse_index ~= nil then line = line:sub(1, verse_index - 1) new_lines = 0 - verses[#verses+1] = #lyrics - dbg_inner("Verse: " .. #lyrics) - end + verses[#verses + 1] = #lyrics + dbg_inner("Verse: " .. #lyrics) + end if line ~= nil then if use_static then if static_text == "" then @@ -1282,80 +1277,83 @@ function delete_song(name) end os.remove(path) table.remove(song_directory, get_index_in_list(song_directory, name)) - source_filter = false + source_filter = false load_source_song_directory(false) end -- loads the song directory function load_source_song_directory(use_filter) -dbg_method("load_source_song_directory") + dbg_method("load_source_song_directory") local keytext = meta_tags - if source_filter then - keytext = source_meta_tags - end - dbg_inner(keytext) - local keys = ParseCSVLine(keytext) - + if source_filter then + keytext = source_meta_tags + end + dbg_inner(keytext) + local keys = ParseCSVLine(keytext) + song_directory = {} local filenames = {} - local tags = {} + local tags = {} local dir = obs.os_opendir(get_songs_folder_path()) -- get_songs_folder_path()) local entry local songExt local songTitle - local goodEntry = true + local goodEntry = true repeat entry = obs.os_readdir(dir) if entry and not entry.directory and - (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") then - songExt = obs.os_get_path_extension(entry.d_name) - songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) - tags = readTags(songTitle) - goodEntry = true - if use_filter and #keys>0 then -- need to check files - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - goodEntry = false -- start assuming file will not be shown - if #tags == 0 then -- check no tagged option - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - else -- have keys and tags so compare them - for k = 1, #keys do - for t = 1, #tags do - if tags[t] == keys[k] then - goodEntry = true -- found match so show file - break - end - end - if goodEntry then -- stop outer key loop on match - break - end - end - end - end - if goodEntry then -- add file if valid match - if songExt == ".enc" then - song_directory[#song_directory + 1] = dec(songTitle) - else - song_directory[#song_directory + 1] = songTitle - end - end - end + (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") + then + songExt = obs.os_get_path_extension(entry.d_name) + songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) + tags = readTags(songTitle) + goodEntry = true + if use_filter and #keys > 0 then -- need to check files + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + goodEntry = false -- start assuming file will not be shown + if #tags == 0 then -- check no tagged option + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + else -- have keys and tags so compare them + for k = 1, #keys do + for t = 1, #tags do + if tags[t] == keys[k] then + goodEntry = true -- found match so show file + break + end + end + if goodEntry then -- stop outer key loop on match + break + end + end + end + end + if goodEntry then -- add file if valid match + if songExt == ".enc" then + song_directory[#song_directory + 1] = dec(songTitle) + else + song_directory[#song_directory + 1] = songTitle + end + end + end until not entry obs.os_closedir(dir) end - +-- +-- reads the first line of each lyric file, looks for the //meta comment and returns any CSV tags that exist +-- function readTags(name) local meta = "" local path = {} @@ -1367,63 +1365,69 @@ function readTags(name) local file = io.open(path, "r") if file ~= nil then for line in file:lines() do - meta = line - break; + meta = line + break end file:close() end local meta_index = meta:find("//meta ") -- Look for meta block Set if meta_index ~= nil then - meta = meta:sub(meta_index + 7) - return ParseCSVLine(meta) + meta = meta:sub(meta_index + 7) + return ParseCSVLine(meta) end return {} end -function ParseCSVLine (line) - local res = {} - local pos = 1 - sep = ',' - while true do - local c = string.sub(line,pos,pos) - if (c == "") then break end - if (c == '"') then - local txt = "" - repeat - local startp,endp = string.find(line,'^%b""',pos) - txt = txt..string.sub(line,startp+1,endp-1) - pos = endp + 1 - c = string.sub(line,pos,pos) - if (c == '"') then txt = txt..'"' end - until (c ~= '"') - txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. txt) - table.insert(res,txt) - assert(c == sep or c == "") - pos = pos + 1 - else - local startp,endp = string.find(line,sep,pos) - if (startp) then - local t = string.sub(line,pos,startp-1) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res,t) - pos = endp + 1 - else - local t = string.sub(line,pos) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res,t) - break - end - end - end - return res +function ParseCSVLine(line) + local res = {} + local pos = 1 + sep = "," + while true do + local c = string.sub(line, pos, pos) + if (c == "") then + break + end + if (c == '"') then + local txt = "" + repeat + local startp, endp = string.find(line, '^%b""', pos) + txt = txt .. string.sub(line, startp + 1, endp - 1) + pos = endp + 1 + c = string.sub(line, pos, pos) + if (c == '"') then + txt = txt .. '"' + end + until (c ~= '"') + txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. txt) + table.insert(res, txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp, endp = string.find(line, sep, pos) + if (startp) then + local t = string.sub(line, pos, startp - 1) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + pos = endp + 1 + else + local t = string.sub(line, pos) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + break + end + end + end + return res end local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet --- encoding +-- encode title/filename if it contains invalid filename characters +-- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup +-- function enc(data) return ((data:gsub( ".", @@ -1448,7 +1452,9 @@ function enc(data) end ) .. ({"", "==", "="})[#data % 3 + 1]) end - +-- +-- decode an encoded title/filename +-- function dec(data) data = string.gsub(data, "[^" .. b .. "=]", "") return (data:gsub( @@ -1526,9 +1532,7 @@ function save_prepared() return true end - function update_monitor() - dbg_method("update_monitor") local tableback = "black" local text = "" @@ -1555,12 +1559,17 @@ function update_monitor() text .. " of " .. #prepared_songs .. "
" end - text = text .. "
Lyric Page: " .. page_index + text = + text .. + "
Lyric Page: " .. + page_index text = text .. " of " .. #lyrics .. "
" - if #verses ~= nil and mon_verse>0 then - text = text .. "
Verse: " .. mon_verse - text = text .. " of " .. #verses .. "
" - end + if #verses ~= nil and mon_verse > 0 then + text = + text .. + "
Verse: " .. mon_verse + text = text .. " of " .. #verses .. "
" + end text = text .. "
" if not anythingActive() then tableback = "#440000" @@ -1590,7 +1599,8 @@ function update_monitor() text = text .. "Current
Page" - text = text .. " • " .. mon_lyric .. "" + text = + text .. " • " .. mon_lyric .. "" end if mon_nextlyric ~= "" and mon_nextlyric ~= nil then text = @@ -1603,7 +1613,8 @@ function update_monitor() text .. "Alt
Lyric" text = - text .. " • " .. mon_alt .. "" + text .. + " • " .. mon_alt .. "" end if mon_nextalt ~= "" and mon_nextalt ~= nil then text = @@ -1661,8 +1672,8 @@ function get_song_text(name) end file:close() else - return nil - end + return nil + end return song_lines end @@ -1676,81 +1687,134 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself -local help = "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. - " Markup      Syntax         Markup      Syntax \n" .. - "============ ==========   ============ ==========\n" .. - " Display n Lines    #L:n      End Page after Line   Line ###\n" .. - " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. - " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. - " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. - " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. - "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. - "Comment Line    // Line       Block Comments    //[ and //] \n" .. - "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line" - +local help = + "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. + " Markup      Syntax         Markup      Syntax \n" .. + "============ ==========   ============ ==========\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line" + function script_properties() dbg_method("script_properties") - editVisSet = false + editVisSet = false script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) ------------ - obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲",change_info_visible) - local gp = obs.obs_properties_create() + obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) + ----------- + obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲", change_info_visible) + local gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) + obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) obs.obs_properties_add_text(gp, "prop_edit_song_text", "\tSong Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) - obs.obs_properties_add_button(gp, "prop_opensong_button","Edit Song with System Editor", open_song_clicked) - obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) - obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------- - obs.obs_properties_add_button(script_props, "prepared_showing", "▲- HIDE PREPARED SONGS -▲",change_prepared_visible) - gp = obs.obs_properties_create() - local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) - obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) - local gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() - local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prepare_prop, name, name) - end - obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) - obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) - obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List",edit_prepared_clicked) - local eps = obs.obs_properties_create() - local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs/Text", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) - obs.obs_property_set_modified_callback(edit_prop, setEditVis) - obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) - local edit_group_prop = obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", obs.OBS_GROUP_NORMAL,eps) - obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) - obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL,gp) ------------------- - obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲",change_ctrl_visible) - hotkey_props = obs.obs_properties_create() - local hktitletext = obs.obs_properties_add_text(hotkey_props,"hotkey-title", "\t", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) + obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) + obs.obs_properties_add_group( + script_props, + "info_grp", + "Song Title (filename) and Lyrics Information", + obs.OBS_GROUP_NORMAL, + gp + ) + ------------ + obs.obs_properties_add_button( + script_props, + "prepared_showing", + "▲- HIDE PREPARED SONGS -▲", + change_prepared_visible + ) + gp = obs.obs_properties_create() + local prop_dir_list = + obs.obs_properties_add_list( + gp, + "prop_directory_list", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) + local gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) + local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + local prepare_prop = + obs.obs_properties_add_list( + gps, + "prop_prepared_list", + "Prepared Songs", + obs.OBS_COMBO_TYPE_EDITABLE, + obs.OBS_COMBO_FORMAT_STRING + ) + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prepare_prop, name, name) + end + obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) + local eps = obs.obs_properties_create() + local edit_prop = + obs.obs_properties_add_editable_list( + eps, + "prep_list", + "Prepared Songs/Text", + obs.OBS_EDITABLE_LIST_TYPE_STRINGS, + nil, + nil + ) + obs.obs_property_set_modified_callback(edit_prop, setEditVis) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes", save_edits_clicked) + local edit_group_prop = + obs.obs_properties_add_group( + gps, + "edit_grp", + "Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", + obs.OBS_GROUP_NORMAL, + eps + ) + obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) + ------------------ + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) + hotkey_props = obs.obs_properties_create() + local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "\t", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button(hotkey_props,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) - ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)", obs.OBS_GROUP_NORMAL,hotkey_props) - obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) ------- - obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲",change_options_visible) - gp = obs.obs_properties_create() + obs.obs_properties_add_button( + hotkey_props, + "prop_reset_button", + "Reset to First Prepared Song", + reset_button_clicked + ) + ctrl_grp_prop = + obs.obs_properties_add_group( + script_props, + "ctrl_grp", + "Lyric Control Buttons (with Assigned HotKeys)", + obs.OBS_GROUP_NORMAL, + hotkey_props + ) + obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) + ------ + obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) + gp = obs.obs_properties_create() local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "\tLines to Display", 1, 50, 1) obs.obs_property_set_long_description( lines_prop, @@ -1758,11 +1822,10 @@ function script_properties() ) local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") - local link_prop = - obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") + local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") local transition_prop = - obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") obs.obs_property_set_modified_callback(transition_prop, change_transition_property) obs.obs_property_set_long_description( transition_prop, @@ -1771,11 +1834,11 @@ function script_properties() local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) obs.obs_properties_add_int_slider(gp, "text_fade_speed", "\tFade Speed", 1, 10, 1) - obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) -------------- - obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲",change_src_visible) - gp = obs.obs_properties_create() - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) + obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) + ------------- + obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) + gp = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) local source_prop = obs.obs_properties_add_list( gp, @@ -1808,35 +1871,51 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) - xgp = obs.obs_properties_create() - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") - local extra_linked_prop = obs.obs_properties_add_list(xgp,"extra_linked_list","Linked Sources ",obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - -- initialize previously loaded extra properties from table - for _, sourceName in ipairs(extra_sources) do - obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) - end - local extra_source_prop = obs.obs_properties_add_list(xgp,"extra_source_list"," Select Source:",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) - obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) - local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) - local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL,xgp) - obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) - local count = obs.obs_property_list_item_count(extra_linked_prop) - if count > 0 then - obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") - else - obs.obs_property_set_visible(extra_group_prop, false) - end + obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + xgp = obs.obs_properties_create() + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") + local extra_linked_prop = + obs.obs_properties_add_list( + xgp, + "extra_linked_list", + "Linked Sources ", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + -- initialize previously loaded extra properties from table + for _, sourceName in ipairs(extra_sources) do + obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) + end + local extra_source_prop = + obs.obs_properties_add_list( + xgp, + "extra_source_list", + " Select Source:", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) + local clearcall_prop = + obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) + local extra_group_prop = + obs.obs_properties_add_group(gp, "xtr_grp", "Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL, xgp) + obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) + local count = obs.obs_property_list_item_count(extra_linked_prop) + if count > 0 then + obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") + else + obs.obs_property_set_visible(extra_group_prop, false) + end local sources = obs.obs_enum_sources() - obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") + obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") if sources ~= nil then local n = {} for _, source in ipairs(sources) do - local name = obs.obs_source_get_name(source) - if isValid(source) then - obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list - end + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then n[#n + 1] = name @@ -1856,33 +1935,32 @@ function script_properties() end obs.source_list_release(sources) - ------------------ - obs.obs_property_set_enabled(hktitletext,false) - obs.obs_property_set_visible(edit_group_prop, false) - obs.obs_property_set_visible(meta_group_prop, false) + ----------------- + obs.obs_property_set_enabled(hktitletext, false) + obs.obs_property_set_visible(edit_group_prop, false) + obs.obs_property_set_visible(meta_group_prop, false) return script_props end -- script_update is called when settings are changed function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") - display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") source_name = obs.obs_data_get_string(settings, "prop_source_list") alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") static_source_name = obs.obs_data_get_string(settings, "prop_static_list") title_source_name = obs.obs_data_get_string(settings, "prop_title_list") ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") - link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") + link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") end -- A function named script_defaults will be called to set the default settings function script_defaults(settings) dbg_method("script_defaults") obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") + obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -1893,42 +1971,43 @@ function script_defaults(settings) end end - --verify source has an opacity setting function isValid(source) - if source ~= nil then - local flags = obs.obs_source_get_output_flags(source) - print(obs.obs_source_get_name(source) .. " - " .. flags) - local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO,obs.OBS_SOURCE_CUSTOM_DRAW) - if bit.band(flags, targetFlag) == targetFlag then - return true - end - end - return false + if source ~= nil then + local flags = obs.obs_source_get_output_flags(source) + print(obs.obs_source_get_name(source) .. " - " .. flags) + local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) + if bit.band(flags, targetFlag) == targetFlag then + return true + end + end + return false end - --- adds an extra linked source. +-- adds an extra linked source. -- Source must be text source, or have 'Color Correction' Filter applied function link_source_selected(props, prop, settings) dbg_method("link_source_selected") local extra_source = obs.obs_data_get_string(settings, "extra_source_list") - if extra_source ~= nil and extra_source ~= "" then - local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") - obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) - obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) - obs.obs_data_set_string(script_sets, "extra_source_list", "") - obs.obs_property_set_description(extra_linked_list, "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")") - end - return true + if extra_source ~= nil and extra_source ~= "" then + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) + obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) + obs.obs_data_set_string(script_sets, "extra_source_list", "") + obs.obs_property_set_description( + extra_linked_list, + "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" + ) + end + return true end -- removes linked sources function do_linked_clicked(props, p) dbg_method("do_link_clicked") - obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), true) - obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), false) - obs.obs_properties_apply_settings(props, script_sets) + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), true) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), false) + obs.obs_properties_apply_settings(props, script_sets) return true end @@ -1938,228 +2017,253 @@ function clear_linked_clicked(props, p) dbg_method("clear_linked_clicked") local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") obs.obs_property_list_clear(extra_linked_list) - obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), true) - obs.obs_property_set_description(extra_linked_list, "Linked Sources ") + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) + obs.obs_property_set_description(extra_linked_list, "Linked Sources ") return true end - -- A function named script_description returns the description shown to -- the user function script_description() - return description - end + return description +end function vMode(vis) - return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" + return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" end - + function expand_all_groups(props, prop, settings) - expandcollapse = not expandcollapse - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"info_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"mng_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"disp_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"src_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"ctrl_grp"), expandcollapse) - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - return true + expandcollapse = not expandcollapse + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "info_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "mng_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "disp_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "src_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "ctrl_grp"), expandcollapse) + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + return true end - - - function all_vis_equal(props) - if (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) or not - (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"mng_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) then - expandcollapse = not expandcollapse - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - end + if + (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "prep_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) or + not (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "mng_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) + then + expandcollapse = not expandcollapse + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description( + obs.obs_properties_get(props, "expand_all_button"), + mode1 .. "ALL GROUPS" .. mode2 + ) + end end function change_info_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"info_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "info_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + all_vis_equal(props) return true end function change_prepared_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"mng_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "mng_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + all_vis_equal(props) return true end function change_options_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"disp_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "disp_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + all_vis_equal(props) return true end function change_src_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"src_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "src_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + all_vis_equal(props) return true end function change_ctrl_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"ctrl_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "ctrl_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + all_vis_equal(props) return true end function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") - dbg_bool("Fade: ",text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) + dbg_bool("Fade: ", text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) return true end - + function show_help_button(props, prop, settings) -dbg_method("show help") - local hb = obs.obs_properties_get(props, "show_help_button") - showhelp = not showhelp - if showhelp then - obs.obs_property_set_description(hb, help) - else - obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") - end - return true + dbg_method("show help") + local hb = obs.obs_properties_get(props, "show_help_button") + showhelp = not showhelp + if showhelp then + obs.obs_property_set_description(hb, help) + else + obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") + end + return true end function setEditVis(props, prop, settings) -- hides edit group on initial showing - dbg_method("setEditVis") - if not editVisSet then - local pp = obs.obs_properties_get(script_props,"edit_grp") - obs.obs_property_set_visible(pp, false) - pp = obs.obs_properties_get(props,"meta") - obs.obs_property_set_visible(pp, false) - editVisSet = true - end + dbg_method("setEditVis") + if not editVisSet then + local pp = obs.obs_properties_get(script_props, "edit_grp") + obs.obs_property_set_visible(pp, false) + pp = obs.obs_properties_get(props, "meta") + obs.obs_property_set_visible(pp, false) + editVisSet = true + end end function filter_songs_clicked(props, p) - local pp = obs.obs_properties_get(props,"meta") - if not obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "filter_songs_button") - obs.obs_property_set_description(mpb, "Clear Filters") -- change button function - meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") - refresh_directory() - else - obs.obs_property_set_visible(pp, false) - meta_tags = "" -- clear meta tags - refresh_directory() - local mpb = obs.obs_properties_get(props, "filter_songs_button") -- - obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function - end - return true + local pp = obs.obs_properties_get(props, "meta") + if not obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "filter_songs_button") + obs.obs_property_set_description(mpb, "Clear Filters") -- change button function + meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + refresh_directory() + else + obs.obs_property_set_visible(pp, false) + meta_tags = "" -- clear meta tags + refresh_directory() + local mpb = obs.obs_properties_get(props, "filter_songs_button") -- + obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function + end + return true end function edit_prepared_clicked(props, p) - local pp = obs.obs_properties_get(props,"edit_grp") - if obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared List") - return true - end + local pp = obs.obs_properties_get(props, "edit_grp") + if obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared List") + return true + end local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") local count = obs.obs_property_list_item_count(prop_prep_list) - local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2 do - obs.obs_data_array_erase(songNames,0) - end - end + if count2 > 0 then + for i = 0, count2 do + obs.obs_data_array_erase(songNames, 0) + end + end - for i = 0, count-1 do + for i = 0, count - 1 do local song = obs.obs_property_list_item_string(prop_prep_list, i) - local array_obj = obs.obs_data_create() - obs.obs_data_set_string(array_obj, "value", song) - obs.obs_data_array_push_back(songNames,array_obj) - obs.obs_data_release(array_obj) - end - obs.obs_data_set_array(script_sets, "prep_list", songNames) - obs.obs_data_array_release(songNames) - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Cancel Prepared Edits") + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song) + obs.obs_data_array_push_back(songNames, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(script_sets, "prep_list", songNames) + obs.obs_data_array_release(songNames) + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Cancel Prepared Edits") return true end -- removes prepared songs function save_edits_clicked(props, p) - load_source_song_directory(false) - prepared_songs = {} - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_clear(prop_prep_list) - local songNames = obs.obs_data_get_array(script_sets, "prep_list") + load_source_song_directory(false) + prepared_songs = {} + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2-1 do - local item = obs.obs_data_array_item(songNames, i); - local itemName = obs.obs_data_get_string(item, "value"); - if get_index_in_list(song_directory, itemName) ~= nil then - prepared_songs[#prepared_songs+1] = itemName - obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) - end - obs.obs_data_release(item) - end - end - obs.obs_data_array_release(songNames) - save_prepared() - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) - prepared_index = 1 - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - prepared_index = 0 - end - pp = obs.obs_properties_get(script_props,"edit_grp") - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared Songs List") - obs.obs_properties_apply_settings(props, script_sets) + if count2 > 0 then + for i = 0, count2 - 1 do + local item = obs.obs_data_array_item(songNames, i) + local itemName = obs.obs_data_get_string(item, "value") + if get_index_in_list(song_directory, itemName) ~= nil then + prepared_songs[#prepared_songs + 1] = itemName + obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(songNames) + save_prepared() + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + prepared_index = 1 + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + prepared_index = 0 + end + pp = obs.obs_properties_get(script_props, "edit_grp") + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + obs.obs_properties_apply_settings(props, script_sets) return true end @@ -2173,10 +2277,9 @@ function change_transition_property(props, prop, settings) return true end - -- A function named script_save will be called when the script is saved -function script_save(settings) - dbg_method("script_save") +function script_save(settings) + dbg_method("script_save") save_prepared() local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) @@ -2205,24 +2308,23 @@ function script_save(settings) hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) - --- - --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded - --- - local extra_sources_array = obs.obs_data_array_create() + --- + --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded + --- + local extra_sources_array = obs.obs_data_array_create() local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") local count = obs.obs_property_list_item_count(extra_linked_list) - for i = 0, count-1 do - local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local array_obj = obs.obs_data_create() - obs.obs_data_set_string(array_obj, "value", source_name) - obs.obs_data_array_push_back(extra_sources_array,array_obj) - obs.obs_data_release(array_obj) - end - obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) - obs.obs_data_array_release(extra_sources_array) + for i = 0, count - 1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", source_name) + obs.obs_data_array_push_back(extra_sources_array, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) + obs.obs_data_array_release(extra_sources_array) end - -- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS -- sets callback to obs_frontend Event Callback -- @@ -2230,71 +2332,71 @@ function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric", " ......................") + hotkey_n_key = get_hotkeys(hotkey_save_array, "Next Lyric", " ......................") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") - hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric", " ..................") + hotkey_p_key = get_hotkeys(hotkey_save_array, "Previous Lyric", " ..................") obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") - hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics", " ..............") + hotkey_c_key = get_hotkeys(hotkey_save_array, "Show/Hide Lyrics", " ..............") obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") - hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Prepared", " ................") + hotkey_n_p_key = get_hotkeys(hotkey_save_array, "Next Prepared", " ................") obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") - hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared", "............") + hotkey_p_p_key = get_hotkeys(hotkey_save_array, "Previous Prepared", "............") obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") - hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start", " ..........") + hotkey_home_key = get_hotkeys(hotkey_save_array, "Reset to Song Start", " ..........") obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) - hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_reset_id = + obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") - hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared", " .......") + hotkey_reset_key = get_hotkeys(hotkey_save_array, "Reset to 1st Prepared", " .......") obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) script_sets = settings source_name = obs.obs_data_get_string(settings, "prop_source_list") - extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - - -- load previously defined extra sources from settings array into table - -- script_properties function will take them from the table and restore them as UI properties - -- - local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - local count = obs.obs_data_array_count(extra_sources_array) - if count > 0 then - for i = 0, count do - local item = obs.obs_data_array_item(extra_sources_array, i); - local sourceName = obs.obs_data_get_string(item, "value"); - if sourceName ~= "" then - extra_sources[#extra_sources + 1] = sourceName - end - obs.obs_data_release(item) - end - end - obs.obs_data_array_release(extra_sources_array) - - - -- load prepared songs from stored file - -- + extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + + -- load previously defined extra sources from settings array into table + -- script_properties function will take them from the table and restore them as UI properties + -- + local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + local count = obs.obs_data_array_count(extra_sources_array) + if count > 0 then + for i = 0, count do + local item = obs.obs_data_array_item(extra_sources_array, i) + local sourceName = obs.obs_data_get_string(item, "value") + if sourceName ~= "" then + extra_sources[#extra_sources + 1] = sourceName + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(extra_sources_array) + + -- load prepared songs from stored file + -- if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -2307,14 +2409,14 @@ function script_load(settings) end file:close() end - name_hotkeys() - + name_hotkeys() + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end --- ------ ---------- Source Showing or Source Active Helper Functions +--------- Source Showing or Source Active Helper Functions --------- Return true if sourcename given is showing anywhere or on in the Active scene ------ --- @@ -2339,16 +2441,16 @@ function isActive(sourceName) end function anythingShowing() - return isShowing(source_name) or isShowing(alternate_source_name) - or isShowing(title_source_name) or isShowing(static_source_name) + return isShowing(source_name) or isShowing(alternate_source_name) or isShowing(title_source_name) or + isShowing(static_source_name) end function sourceShowing() - return isShowing(source_name) + return isShowing(source_name) end function alternateShowing() - return isShowing(alternate_source_name) + return isShowing(alternate_source_name) end function titleShowing() @@ -2360,24 +2462,24 @@ function staticShowing() end function anythingActive() - return isActive(source_name) or isActive(alternate_source_name) - or isActive(title_source_name) or isActive(static_source_name) + return isActive(source_name) or isActive(alternate_source_name) or isActive(title_source_name) or + isActive(static_source_name) end function sourceActive() - return isActive(source_name) + return isActive(source_name) end function alternateActive() - return isActive(alternate_source_name) + return isActive(alternate_source_name) end function titleActive() - return isActive(title_source_name) + return isActive(title_source_name) end function staticActive() - return isActive(static_source_name) + return isActive(static_source_name) end --- @@ -2388,7 +2490,7 @@ end ------ --- ----------------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------------- -- get_hotkeys(loaded hotkey array, desired prefix text, leader text (between prefix and hotkey label) -- Returns translated hotkey text label with prefix and leader -- e.g. if HotKeyArray contains an assigned hotkey Shift and F1 key combo, then @@ -2396,62 +2498,94 @@ end ---------------------------------------------------------------------------------------------------------- function get_hotkeys(hotkey_array, prefix, leader) - local Translate = {["NUMLOCK"] = "NumLock", ["NUMSLASH"] = "Num/", ["NUMASTERISK"] = "Num*", - ["NUMMINUS"] = "Num-", ["NUMPLUS"] = "Num+", - ["NUMPERIOD"] = "NumDel", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page-Down", - ["PAGEUP"] = "Page-Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", - ["UP"] = "Up", ["DOWN"] = "Down", ["RIGHT"] = "Right", ["LEFT"] = "Left", - ["SCROLLLOCK"] = "Scroll-Lock", ["BACKSPACE"] = "Backspace", ["ESCAPE"] = "Esc", - ["MENU"] = "Menu", ["META"] = "Meta", ["PRINT"] = "Prt", ["TAB"] = "Tab", - ["DELETE"] = "Del", ["CAPSLOCK"] = "Caps-Lock", ["NUMEQUAL"] = "Num=", ["PAUSE"] = "Pause", - ["VK_VOLUME_MUTE"] = "Vol Mute", ["VK_VOLUME_DOWN"] = "Vol Dwn", ["VK_VOLUME_UP"] = "Vol Up", - ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", ["VK_MEDIA_STOP"] = "Media Stop", - ["VK_MEDIA_PREV_TRACK"] = "Media Prev", ["VK_MEDIA_NEXT_TRACK"] = "Media Next"} - - item = obs.obs_data_array_item(hotkey_array, 0) - local key = string.sub(obs.obs_data_get_string(item,"key"),9) - if Translate[key] ~= nil then - key = Translate[key] - elseif string.sub(key,1,3) == "NUM" then - key = "Num " .. string.sub(key,4) - elseif string.sub(key,1,5) == "MOUSE" then - key = "Mouse " .. string.sub(key,6) - end - - obs.obs_data_release(item) + local Translate = { + ["NUMLOCK"] = "NumLock", + ["NUMSLASH"] = "Num/", + ["NUMASTERISK"] = "Num*", + ["NUMMINUS"] = "Num-", + ["NUMPLUS"] = "Num+", + ["NUMPERIOD"] = "NumDel", + ["INSERT"] = "Insert", + ["PAGEDOWN"] = "Page-Down", + ["PAGEUP"] = "Page-Up", + ["HOME"] = "Home", + ["END"] = "End", + ["RETURN"] = "Return", + ["UP"] = "Up", + ["DOWN"] = "Down", + ["RIGHT"] = "Right", + ["LEFT"] = "Left", + ["SCROLLLOCK"] = "Scroll-Lock", + ["BACKSPACE"] = "Backspace", + ["ESCAPE"] = "Esc", + ["MENU"] = "Menu", + ["META"] = "Meta", + ["PRINT"] = "Prt", + ["TAB"] = "Tab", + ["DELETE"] = "Del", + ["CAPSLOCK"] = "Caps-Lock", + ["NUMEQUAL"] = "Num=", + ["PAUSE"] = "Pause", + ["VK_VOLUME_MUTE"] = "Vol Mute", + ["VK_VOLUME_DOWN"] = "Vol Dwn", + ["VK_VOLUME_UP"] = "Vol Up", + ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", + ["VK_MEDIA_STOP"] = "Media Stop", + ["VK_MEDIA_PREV_TRACK"] = "Media Prev", + ["VK_MEDIA_NEXT_TRACK"] = "Media Next" + } + + item = obs.obs_data_array_item(hotkey_array, 0) + local key = string.sub(obs.obs_data_get_string(item, "key"), 9) + if Translate[key] ~= nil then + key = Translate[key] + elseif string.sub(key, 1, 3) == "NUM" then + key = "Num " .. string.sub(key, 4) + elseif string.sub(key, 1, 5) == "MOUSE" then + key = "Mouse " .. string.sub(key, 6) + end + + obs.obs_data_release(item) local val = prefix - if key ~= nil and key ~= "" then - val = val .. " " .. leader .. " " - if obs.obs_data_get_bool(item,"control") then val = val.."Ctrl + " end - if obs.obs_data_get_bool(item,"alt") then val = val.."Alt + " end - if obs.obs_data_get_bool(item,"shift") then val = val.."Shift + " end - if obs.obs_data_get_bool(item,"command") then val = val.."Cmd + " end - val = val .. key - end - return val + if key ~= nil and key ~= "" then + val = val .. " " .. leader .. " " + if obs.obs_data_get_bool(item, "control") then + val = val .. "Ctrl + " + end + if obs.obs_data_get_bool(item, "alt") then + val = val .. "Alt + " + end + if obs.obs_data_get_bool(item, "shift") then + val = val .. "Shift + " + end + if obs.obs_data_get_bool(item, "command") then + val = val .. "Cmd + " + end + val = val .. key + end + return val end -- name_hotkeys function renames the seven hotkeys to include their defined key text -- function name_hotkeys() - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) end - -------- ---------------- ------------------------ SOURCE FUNCTIONS ---------------- -------- --- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change --- +-- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change +-- function rename_source() -- pause_timer = true local sources = obs.obs_enum_sources() @@ -2485,7 +2619,7 @@ function rename_source() if loadLyric_items[index] == nil then loadLyric_items[index] = 1 -- First time to find this source so mark with 1 else - loadLyric_items[index] = loadLyric_items[index]+1 -- Found this source again so increment + loadLyric_items[index] = loadLyric_items[index] + 1 -- Found this source again so increment end obs.obs_data_release(settings) -- release memory end @@ -2510,7 +2644,9 @@ function rename_source() -- Mark Duplicates if index ~= nil then if loadLyric_items[index] > 1 then - name = '' .. name .. " " .. loadLyric_items[index] .. "" + name = + '' .. + name .. " " .. loadLyric_items[index] .. "" end if (c_name ~= name) then obs.obs_source_set_name(source, name) @@ -2527,78 +2663,80 @@ function rename_source() end -- Names the initial "Prepare Lyric" source (prior to being renamed to "Load Lyrics for: {song name} --- +-- source_def.get_name = function() return "Prepare Lyric" end -- Called when OBS is saving data. This will be called on each copy of Load Lyric source -- Used to initiate rename_source() function when the source dialog closes --- saved flag prevents it from being called by every source each time. +-- saved flag prevents it from being called by every source each time. -- source_def.save = function(data, settings) - if saved then return end -- we only need it once, not for every load lyric source copy - dbg_method("Source_save") - saved = true + if saved then + return + end -- we only need it once, not for every load lyric source copy + dbg_method("Source_save") + saved = true using_source = true rename_source() -- Rename and Mark sources instantly on update (WZ) end -- Called when a change is made in the source dialog (Currently Not Used) --- +-- source_def.update = function(data, settings) -dbg_method("update") + dbg_method("update") end -- Called when the source dialog is loaded (Currently not Used) -- source_def.load = function(data) -dbg_method("load") + dbg_method("load") end -- Called when the refresh button is pressed in the source dialog -- It reloads the song directory and applies any meta-tag filters if entered --- +-- function source_refresh_button_clicked(props, p) - dbg_method("source_refresh_button") - source_filter = true - dbg_inner("tags: " .. source_meta_tags) + dbg_method("source_refresh_button") + source_filter = true + dbg_inner("tags: " .. source_meta_tags) load_source_song_directory(true) table.sort(song_directory) - local prop_dir_list = obs.obs_properties_get(props,"songs") - obs.obs_property_list_clear(prop_dir_list) -- clear directories + local prop_dir_list = obs.obs_properties_get(props, "songs") + obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do - dbg_inner("SLD: " .. name) + dbg_inner("SLD: " .. name) obs.obs_property_list_add_string(prop_dir_list, name, name) - end + end return true end --- Keeps variable source-meta-tags up-to-date +-- Keeps variable source-meta-tags up-to-date -- Note: This could be done only when refreshing the directory (see source_refresh_button_clicked) --- +-- function update_source_metatags(props, p, settings) - source_meta_tags = obs.obs_data_get_string(settings,"metatags") - return true + source_meta_tags = obs.obs_data_get_string(settings, "metatags") + return true end --- Called when a user makes a song selection in the source dialog --- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens +-- Called when a user makes a song selection in the source dialog +-- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens -- Saved flag is cleared to mark changes have occured for save event --- +-- function source_selection_made(props, prop, settings) -dbg_method("source_selection") - local name = obs.obs_data_get_string(settings,"songs") - saved = false -- mark properties changed - using_source = true - prepare_selected(name) + dbg_method("source_selection") + local name = obs.obs_data_get_string(settings, "songs") + saved = false -- mark properties changed + using_source = true + prepare_selected(name) return true end -- Standard OBS get Properties function for OBS source dialog --- +-- source_def.get_properties = function(data) - source_filter = true + source_filter = true load_source_song_directory(true) local source_props = obs.obs_properties_create() local source_dir_list = @@ -2609,41 +2747,40 @@ source_def.get_properties = function(data) obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) + obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(source_dir_list, name, name) end - gps = obs.obs_properties_create() - source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) - obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) - obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() + gps = obs.obs_properties_create() + source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) + obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) + obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode - obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) - dbg_inner("props") + obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) + dbg_inner("props") return source_props - end --- Called when the source is created --- saves pointer to settings in global sourc_sets for convienence +-- Called when the source is created +-- saves pointer to settings in global sourc_sets for convienence -- Sets callbacks for active, showing, deactive, and updated callbacks --- +-- source_def.create = function(settings, source) -dbg_method("create") + dbg_method("create") data = {} - source_sets = settings + source_sets = settings obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback return data end --- Sets default settings for Activate Source in Preview +-- Sets default settings for Activate Source in Preview -- source_def.get_defaults = function(settings) obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) @@ -2651,7 +2788,7 @@ end -- On Event Functions -- These manage keeping the HTML monitor page updated when changes happen like scene changes that remove --- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are +-- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are -- either created or deleted. Rename changes color and marks with *, sources that are reference copies of the same source -- as accidentally changing the settings like the loaded song in one will change it in the reference copies. -- @@ -2672,35 +2809,35 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) - dbg_method("on_event: " .. event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page - dbg_bool("Active:",source_active) - obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS - end - if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes - dbg_inner("Scene Change") - obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change - end -end - --- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. --- + dbg_method("on_event: " .. event) + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page + dbg_bool("Active:", source_active) + obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS + end + if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes + dbg_inner("Scene Change") + obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change + end +end + +-- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. +-- function load_source_song(source, preview) dbgsp("load_source_song") local settings = obs.obs_source_get_settings(source) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - using_source = true - load_source = source - all_sources_fade = true -- fade title and source the first time - set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in - if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles - prepare_selected(song) - end - transition_lyric_text() - if obs.obs_data_get_bool(settings, "source_home_on_active") then - home_prepared(true) - end + using_source = true + load_source = source + all_sources_fade = true -- fade title and source the first time + set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in + if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles + prepare_selected(song) + end + transition_lyric_text() + if obs.obs_data_get_bool(settings, "source_home_on_active") then + home_prepared(true) + end end obs.obs_data_release(settings) end @@ -2734,7 +2871,7 @@ end -- Call back when load source (not text source) goes to the Active -- loads the selected song and sets the current scene name for the HTML monitor --- +-- function source_showing(cd) dbg_custom("source_showing") local source = obs.calldata_source(cd, "source") @@ -2744,12 +2881,12 @@ function source_showing(cd) load_source_song(source, true) end --- dbg functions --- +-- dbg functions +-- function dbg_traceback() - if DEBUG then - print("Trace: " .. debug.traceback()) - end + if DEBUG then + print("Trace: " .. debug.traceback()) + end end function dbg(message) @@ -2771,9 +2908,9 @@ function dbg_method(message) end function dbgsp(message) -if DEBUG then - dbg("====SPECIAL=====================>> " .. message) -end + if DEBUG then + dbg("====SPECIAL=====================>> " .. message) + end end function dbg_custom(message) if DEBUG_CUSTOM then @@ -2783,18 +2920,19 @@ end function dbg_bool(name, value) if DEBUG_BOOL then - local message = "BOOL: " .. name + local message = "BOOL: " .. name if value then message = message .. " = true" else message = message .. " = false" end - dbg(message) + dbg(message) end end obs.obs_register_source(source_def) -description = [[ +description = + [[

OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian

-]] \ No newline at end of file +]] diff --git a/lyrics.lua b/lyrics.lua index 666d554..523d8c4 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -12,7 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. - obs = obslua bit = require("bit") @@ -37,7 +36,6 @@ first_open = true display_lines = 0 ensure_lines = true - -- lyrics/alternate lyrics by page lyrics = {} alternate = {} @@ -45,21 +43,21 @@ alternate = {} -- verse indicies if marked verses = {} -page_index = 0 -- current page of lyrics being displayed +page_index = 0 -- current page of lyrics being displayed prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected -song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) -prepared_songs = {} -- holds pre-prepared list of songs to use -extra_sources = {} -- holder for extra sources settings +song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) +prepared_songs = {} -- holds pre-prepared list of songs to use +extra_sources = {} -- holder for extra sources settings link_text = false -- true if Title and Static should fade with text only during hide/show -link_extras = false -- extras fade with text always when true, only during hide/show when false +link_extras = false -- extras fade with text always when true, only during hide/show when false all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide source_song_title = "" -- The song title from a source loaded song using_source = false -- true when a lyric load song is being used instead of a pre-prepared song source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) -load_scene = "" -- name of scene loading a lyric with a source +load_scene = "" -- name of scene loading a lyric with a source last_prepared_song = "" -- name of the last prepared song (prevents duplicate loading of already loaded song) -- hotkeys @@ -95,7 +93,7 @@ mon_alt = "" mon_nextalt = "" mon_nextsong = "" meta_tags = "" -source_meta_tags = "" +source_meta_tags = "" -- text status & fade TEXT_VISIBLE = 0 -- text is visible @@ -115,10 +113,10 @@ load_source = nil expandcollapse = true showhelp = false -transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) +transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false -source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now +source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now editVisSet = false @@ -135,7 +133,6 @@ DEBUG_METHODS = true -- print method names ---------------- -------- - function next_lyric(pressed) if not pressed then return @@ -179,58 +176,58 @@ function prev_prepared(pressed) if not pressed then return end - if #prepared_songs == 0 then - return - end - if using_source then - using_source = false - prepare_selected(prepared_songs[prepared_index]) - return - end - if prepared_index > 1 then - using_source = false - prepare_selected(prepared_songs[prepared_index - 1]) - return - end - if not source_active or using_source then - using_source = false - prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared - else - using_source = true - prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source - load_source_song(load_source, false) - end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) + return + end + if prepared_index > 1 then + using_source = false + prepare_selected(prepared_songs[prepared_index - 1]) + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared + else + using_source = true + prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source + load_source_song(load_source, false) + end end function next_prepared(pressed) if not pressed then return end - if #prepared_songs == 0 then - return - end - if using_source then - using_source = false - dbg_custom("do current prepared") - prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song - return - end - if prepared_index < #prepared_songs then - using_source = false - dbg_custom("do next prepared") - prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared - return - end - if not source_active or using_source then - using_source = false - dbg_custom("do first prepared") - prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available - else - using_source = true - dbg_custom("do source prepared") - prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source - load_source_song(load_source, false) - end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + dbg_custom("do current prepared") + prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song + return + end + if prepared_index < #prepared_songs then + using_source = false + dbg_custom("do next prepared") + prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared + return + end + if not source_active or using_source then + using_source = false + dbg_custom("do first prepared") + prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available + else + using_source = true + dbg_custom("do source prepared") + prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source + load_source_song(load_source, false) + end end function toggle_lyrics_visibility(pressed) @@ -238,9 +235,9 @@ function toggle_lyrics_visibility(pressed) if not pressed then return end - if link_text then - all_sources_fade = true - end + if link_text then + all_sources_fade = true + end if text_status ~= TEXT_HIDDEN then dbg_inner("hiding") set_text_visibility(TEXT_HIDDEN) @@ -284,7 +281,7 @@ function home_prepared(pressed) obs.obs_data_set_string(script_sets, "prop_prepared_list", "") end obs.obs_properties_apply_settings(props, script_sets) - prepared_index = 1 + prepared_index = 1 prepare_selected(prepared_songs[prepared_index]) return true end @@ -408,7 +405,7 @@ function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") if #prepared_songs == 0 then set_text_visibility(TEXT_HIDDEN) - end + end prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) @@ -425,24 +422,24 @@ function refresh_button_clicked(props, p) local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") local static_source_prop = obs.obs_properties_get(props, "prop_static_list") local title_source_prop = obs.obs_properties_get(props, "prop_title_list") - local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") + local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") obs.obs_property_list_clear(source_prop) -- clear current properties list obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list obs.obs_property_list_clear(static_source_prop) -- clear current properties list obs.obs_property_list_clear(title_source_prop) -- clear current properties list obs.obs_property_list_clear(extra_source_prop) -- clear extra sources list - - obs.obs_property_list_add_string(extra_source_prop, "", "") - + + obs.obs_property_list_add_string(extra_source_prop, "", "") + local sources = obs.obs_enum_sources() if sources ~= nil then local n = {} for _, source in ipairs(sources) do - local name = obs.obs_source_get_name(source) - if isValid(source) then - obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list - end + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then n[#n + 1] = name @@ -460,36 +457,38 @@ function refresh_button_clicked(props, p) obs.obs_property_list_add_string(static_source_prop, name, name) end end - obs.source_list_release(sources) - refresh_directory() - + obs.source_list_release(sources) + refresh_directory() + return true end function refresh_directory_button_clicked(props, p) -dbg_method("refresh directory") - refresh_directory() + dbg_method("refresh directory") + refresh_directory() return true end function refresh_directory() - local prop_dir_list = obs.obs_properties_get(script_props,"prop_directory_list") + local prop_dir_list = obs.obs_properties_get(script_props, "prop_directory_list") local source_prop = obs.obs_properties_get(props, "prop_source_list") - source_filter = false + source_filter = false load_source_song_directory(true) table.sort(song_directory) - obs.obs_property_list_clear(prop_dir_list) -- clear directories + obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do - dbg_inner(name) + dbg_inner(name) obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_properties_apply_settings(script_props, script_sets) -end - + end + obs.obs_properties_apply_settings(script_props, script_sets) +end --- Called with ANY change to the prepared song list +-- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) - obs.obs_property_set_description(obs.obs_properties_get(props, "prep_grp"), " Prepared Songs/Text (" .. #prepared_songs .. ")") + obs.obs_property_set_description( + obs.obs_properties_get(props, "prep_grp"), + " Prepared Songs/Text (" .. #prepared_songs .. ")" + ) dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false @@ -500,10 +499,10 @@ end -- removes prepared songs function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") - prepared_songs = {} -- required for monitor page - page_index = 0 -- required for monitor page - prepared_index = 0 -- required for monitor page - update_source_text() -- required for monitor page + prepared_songs = {} -- required for monitor page + page_index = 0 -- required for monitor page + prepared_index = 0 -- required for monitor page + update_source_text() -- required for monitor page -- clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) @@ -515,25 +514,23 @@ end function prepare_selected(name) dbg_method("prepare_selected") - -- try to prepare song - if prepare_song_by_name(name) then - page_index = 1 - if not using_source then - prepared_index = get_index_in_list(prepared_songs, name) - else - source_song_title = name - all_sources_fade = true - end - - transition_lyric_text(using_source) - - - else - -- hide everything if unable to prepare song - -- TODO: clear lyrics entirely after text is hidden - set_text_visibility(TEXT_HIDDEN) - end - + -- try to prepare song + if prepare_song_by_name(name) then + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + else + source_song_title = name + all_sources_fade = true + end + + transition_lyric_text(using_source) + else + -- hide everything if unable to prepare song + -- TODO: clear lyrics entirely after text is hidden + set_text_visibility(TEXT_HIDDEN) + end + --update_source_text() return true end @@ -591,7 +588,7 @@ end -------- function apply_source_opacity() --- dbg_method("apply_source_visiblity") + -- dbg_method("apply_source_visiblity") local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero @@ -602,7 +599,7 @@ function apply_source_opacity() end obs.obs_source_release(source) obs.obs_data_release(settings) - + local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -611,73 +608,72 @@ function apply_source_opacity() obs.obs_source_update(alt_source, settings) end obs.obs_source_release(alt_source) - obs.obs_data_release(settings) - dbg_bool("All Sources Fade:", all_sources_fade) - dbg_bool("Link Text:", link_text) + obs.obs_data_release(settings) + dbg_bool("All Sources Fade:", all_sources_fade) + dbg_bool("Link Text:", link_text) if all_sources_fade then - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then obs.obs_source_update(title_source, settings) end - obs.obs_source_release(title_source) - obs.obs_data_release(settings) - - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_release(title_source) + obs.obs_data_release(settings) + + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero local static_source = obs.obs_get_source_by_name(static_source_name) if static_source ~= nil then obs.obs_source_update(static_source, settings) end obs.obs_source_release(static_source) - obs.obs_data_release(settings) - end - if link_extras or all_sources_fade then - local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - if count > 0 then - for i = 0, count-1 do - local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - print(source_name) - local extra_source = obs.obs_get_source_by_name(source_name) - if extra_source ~= nil then - source_id = obs.obs_source_get_unversioned_id(extra_source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_source_update(extra_source, settings) -- merge new opacity values - obs.obs_data_release(settings) - else -- check for filter named "Color Correction" - local color_filter = obs.obs_source_get_filter_by_name(extra_source,"Color Correction") - if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_source_get_settings(color_filter) - obs.obs_data_set_double(filter_settings,"opacity",text_opacity/100) - obs.obs_source_update(color_filter,filter_settings) - obs.obs_data_release(filter_settings) - obs.obs_source_release(color_filter) - else -- try to just change visibility in the scene - print("No Filter") - local sceneSource = obs.obs_frontend_get_current_scene() - local sceneObj = obs.obs_scene_from_source(sceneSource) - local sceneItem = obs.obs_scene_find_source(sceneObj,source_name) - obs.obs_source_release(scene) - if text_opacity > 50 then - obs.obs_sceneitem_set_visible(sceneItem, true) - else - obs.obs_sceneitem_set_visible(sceneItem, false) - end - end - end - end - obs.obs_source_release(extra_source) -- release source ptr - end - end - end - + obs.obs_data_release(settings) + end + if link_extras or all_sources_fade then + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count - 1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + print(source_name) + local extra_source = obs.obs_get_source_by_name(source_name) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_source_update(extra_source, settings) -- merge new opacity values + obs.obs_data_release(settings) + else -- check for filter named "Color Correction" + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_source_get_settings(color_filter) + obs.obs_data_set_double(filter_settings, "opacity", text_opacity / 100) + obs.obs_source_update(color_filter, filter_settings) + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + else -- try to just change visibility in the scene + print("No Filter") + local sceneSource = obs.obs_frontend_get_current_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) + obs.obs_source_release(scene) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end + end end function set_text_visibility(end_status) @@ -686,41 +682,41 @@ function set_text_visibility(end_status) if text_status == end_status then return end - if end_status == TEXT_HIDE then - text_opacity = 0 - text_status = end_status - apply_source_opacity() - return - elseif end_status == TEXT_SHOW then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - apply_source_opacity() - return - end - if text_fade_enabled then + if end_status == TEXT_HIDE then + text_opacity = 0 + text_status = end_status + apply_source_opacity() + return + elseif end_status == TEXT_SHOW then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + apply_source_opacity() + return + end + if text_fade_enabled then -- if fade enabled, begin fade in or out if end_status == TEXT_HIDDEN then text_status = TEXT_HIDING elseif end_status == TEXT_VISIBLE then text_status = TEXT_SHOWING end - --all_sources_fade = true + --all_sources_fade = true start_fade_timer() - else -- change visibility immediately (fade or no fade) - if end_status == TEXT_HIDDEN then - text_opacity = 0 - text_status = end_status - elseif end_status == TEXT_VISIBLE then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - end - apply_source_opacity() - --update_source_text() - all_sources_fade = false - return - end + else -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + end + apply_source_opacity() + --update_source_text() + all_sources_fade = false + return + end end -- transition to the next lyrics, use fade if enabled @@ -735,20 +731,20 @@ function transition_lyric_text(force_show) -- fade out transition is complete if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then update_source_text() - -- if text is done hiding, we can cancel the all_sources_fade - if text_status == TEXT_HIDDEN then - all_sources_fade = false - end + -- if text is done hiding, we can cancel the all_sources_fade + if text_status == TEXT_HIDDEN then + all_sources_fade = false + end dbg_inner("hidden") elseif not text_fade_enabled then - dbg_custom("Instant On") - -- if text fade is not enabled, then we can cancel the all_sources_fade - all_sources_fade = false - set_text_visibility(TEXT_VISIBLE) -- does update_source_text() + dbg_custom("Instant On") + -- if text fade is not enabled, then we can cancel the all_sources_fade + all_sources_fade = false + set_text_visibility(TEXT_VISIBLE) -- does update_source_text() update_source_text() dbg_inner("no text fade") - else -- initiate fade out/in - dbg_custom("Transition Timer") + else -- initiate fade out/in + dbg_custom("Transition Timer") text_status = TEXT_TRANSITION_OUT start_fade_timer() end @@ -758,7 +754,7 @@ end -- updates the selected lyrics function update_source_text() dbg_method("update_source_text") - dbg_custom("Page Index: " .. page_index) + dbg_custom("Page Index: " .. page_index) local text = "" local alttext = "" local next_lyric = "" @@ -766,34 +762,33 @@ function update_source_text() local static = static_text local mstatic = static -- save static for use with monitor local title = "" - - - if alt_title ~= "" then - title = alt_title - else - if not using_source then - if prepared_index ~= nil and prepared_index ~= 0 then - dbg_custom("Update from prepared: " .. prepared_index) - title = prepared_songs[prepared_index] - end - else - dbg_custom("Updatefrom source: " .. source_song_title) - title = source_song_title - end - end + + if alt_title ~= "" then + title = alt_title + else + if not using_source then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Update from prepared: " .. prepared_index) + title = prepared_songs[prepared_index] + end + else + dbg_custom("Updatefrom source: " .. source_song_title) + title = source_song_title + end + end local source = obs.obs_get_source_by_name(source_name) local alt_source = obs.obs_get_source_by_name(alternate_source_name) local stat_source = obs.obs_get_source_by_name(static_source_name) local title_source = obs.obs_get_source_by_name(title_source_name) - + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then - if #lyrics > 0 then + if #lyrics > 0 then if lyrics[page_index] ~= nil then text = lyrics[page_index] end end - if #alternate > 0 then + if #alternate > 0 then if alternate[page_index] ~= nil then alttext = alternate[page_index] end @@ -801,14 +796,14 @@ function update_source_text() if link_text then if string.len(text) == 0 and string.len(alttext) == 0 then - --static = "" - --title = "" + --static = "" + --title = "" end end end -- update source texts if source ~= nil then - dbg_inner("Title Load") + dbg_inner("Title Load") local settings = obs.obs_data_create() obs.obs_data_set_string(settings, "text", text) obs.obs_source_update(source, settings) @@ -858,27 +853,27 @@ function update_source_text() next_prepared = prepared_songs[1] -- plan to loop around to first prepared song end end - mon_verse = 0 - if #verses ~= nil then --find valid page Index - for i = 1, #verses do - if page_index >= verses[i]+1 then - mon_verse = i - end - end -- v = current verse number for this page - end - mon_song = title - mon_lyric = text:gsub("\n", "
• ") - mon_nextlyric = next_lyric:gsub("\n", "
• ") - mon_alt = alttext:gsub("\n", "
• ") - mon_nextalt = next_alternate:gsub("\n", "
• ") - mon_nextsong = next_prepared - + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i] + 1 then + mon_verse = i + end + end -- v = current verse number for this page + end + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared + update_monitor() end function start_fade_timer() - dbgsp("started fade timer") - obs.timer_add(fade_callback, 50) + dbgsp("started fade timer") + obs.timer_add(fade_callback, 50) end function fade_callback() @@ -898,9 +893,9 @@ function fade_callback() -- completed fade out, determine next move text_opacity = 0 if text_status == TEXT_TRANSITION_OUT then - -- update to new lyric between fades + -- update to new lyric between fades update_source_text() - -- begin transition back in + -- begin transition back in text_status = TEXT_TRANSITION_IN else text_status = TEXT_HIDDEN @@ -933,15 +928,15 @@ function prepare_song_by_name(name) if name == nil then return false end - last_prepared_song = name + last_prepared_song = name -- if using transition on lyric change, first transition -- would be reset with new song prepared transition_completed = false - -- load song lines + -- load song lines local song_lines = get_song_text(name) - if song_lines == nil then - return false - end + if song_lines == nil then + return false + end local cur_line = 1 local cur_aline = 1 local recordRefrain = false @@ -954,10 +949,10 @@ function prepare_song_by_name(name) local refrain = {} local arefrain = {} lyrics = {} - verses = {} + verses = {} alternate = {} static_text = "" - alt_title = "" + alt_title = "" local adjusted_display_lines = display_lines local refrain_display_lines = display_lines local alternate_display_lines = display_lines @@ -1025,7 +1020,7 @@ function prepare_song_by_name(name) end local static_index = line:find("#S:") if static_index ~= nil then - line = line:sub(static_index+3) + line = line:sub(static_index + 3) static_text = line new_lines = 0 end @@ -1035,12 +1030,12 @@ function prepare_song_by_name(name) line = line:sub(title_indexEnd + 1) alt_title = line new_lines = 0 - end + end local alt_index = line:find("#A:") if alt_index ~= nil then local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) - local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) + local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) line = line:sub(alt_indexEnd + 1) singleAlternate = true end @@ -1093,12 +1088,12 @@ function prepare_song_by_name(name) line = line:sub(1, refrain_index - 1) new_lines = 0 end - + refrain_index = line:find("##R") if refrain_index == nil then - refrain_index = line:find("##r") - end - if refrain_index ~= nil then + refrain_index = line:find("##r") + end + if refrain_index ~= nil then playRefrain = true line = line:sub(1, refrain_index - 1) new_lines = 0 @@ -1112,7 +1107,7 @@ function prepare_song_by_name(name) end newcount_index = line:find("#B:") if newcount_index ~= nil then - new_lines = tonumber(line:sub(newcount_index + 3)) + new_lines = tonumber(line:sub(newcount_index + 3)) line = line:sub(1, newcount_index - 1) end local phantom_index = line:find("##P") @@ -1127,9 +1122,9 @@ function prepare_song_by_name(name) if verse_index ~= nil then line = line:sub(1, verse_index - 1) new_lines = 0 - verses[#verses+1] = #lyrics - dbg_inner("Verse: " .. #lyrics) - end + verses[#verses + 1] = #lyrics + dbg_inner("Verse: " .. #lyrics) + end if line ~= nil then if use_static then if static_text == "" then @@ -1282,80 +1277,83 @@ function delete_song(name) end os.remove(path) table.remove(song_directory, get_index_in_list(song_directory, name)) - source_filter = false + source_filter = false load_source_song_directory(false) end -- loads the song directory function load_source_song_directory(use_filter) -dbg_method("load_source_song_directory") + dbg_method("load_source_song_directory") local keytext = meta_tags - if source_filter then - keytext = source_meta_tags - end - dbg_inner(keytext) - local keys = ParseCSVLine(keytext) - + if source_filter then + keytext = source_meta_tags + end + dbg_inner(keytext) + local keys = ParseCSVLine(keytext) + song_directory = {} local filenames = {} - local tags = {} + local tags = {} local dir = obs.os_opendir(get_songs_folder_path()) -- get_songs_folder_path()) local entry local songExt local songTitle - local goodEntry = true + local goodEntry = true repeat entry = obs.os_readdir(dir) if entry and not entry.directory and - (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") then - songExt = obs.os_get_path_extension(entry.d_name) - songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) - tags = readTags(songTitle) - goodEntry = true - if use_filter and #keys>0 then -- need to check files - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - goodEntry = false -- start assuming file will not be shown - if #tags == 0 then -- check no tagged option - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - else -- have keys and tags so compare them - for k = 1, #keys do - for t = 1, #tags do - if tags[t] == keys[k] then - goodEntry = true -- found match so show file - break - end - end - if goodEntry then -- stop outer key loop on match - break - end - end - end - end - if goodEntry then -- add file if valid match - if songExt == ".enc" then - song_directory[#song_directory + 1] = dec(songTitle) - else - song_directory[#song_directory + 1] = songTitle - end - end - end + (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") + then + songExt = obs.os_get_path_extension(entry.d_name) + songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) + tags = readTags(songTitle) + goodEntry = true + if use_filter and #keys > 0 then -- need to check files + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + goodEntry = false -- start assuming file will not be shown + if #tags == 0 then -- check no tagged option + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + else -- have keys and tags so compare them + for k = 1, #keys do + for t = 1, #tags do + if tags[t] == keys[k] then + goodEntry = true -- found match so show file + break + end + end + if goodEntry then -- stop outer key loop on match + break + end + end + end + end + if goodEntry then -- add file if valid match + if songExt == ".enc" then + song_directory[#song_directory + 1] = dec(songTitle) + else + song_directory[#song_directory + 1] = songTitle + end + end + end until not entry obs.os_closedir(dir) end - +-- +-- reads the first line of each lyric file, looks for the //meta comment and returns any CSV tags that exist +-- function readTags(name) local meta = "" local path = {} @@ -1367,63 +1365,69 @@ function readTags(name) local file = io.open(path, "r") if file ~= nil then for line in file:lines() do - meta = line - break; + meta = line + break end file:close() end local meta_index = meta:find("//meta ") -- Look for meta block Set if meta_index ~= nil then - meta = meta:sub(meta_index + 7) - return ParseCSVLine(meta) + meta = meta:sub(meta_index + 7) + return ParseCSVLine(meta) end return {} end -function ParseCSVLine (line) - local res = {} - local pos = 1 - sep = ',' - while true do - local c = string.sub(line,pos,pos) - if (c == "") then break end - if (c == '"') then - local txt = "" - repeat - local startp,endp = string.find(line,'^%b""',pos) - txt = txt..string.sub(line,startp+1,endp-1) - pos = endp + 1 - c = string.sub(line,pos,pos) - if (c == '"') then txt = txt..'"' end - until (c ~= '"') - txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. txt) - table.insert(res,txt) - assert(c == sep or c == "") - pos = pos + 1 - else - local startp,endp = string.find(line,sep,pos) - if (startp) then - local t = string.sub(line,pos,startp-1) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res,t) - pos = endp + 1 - else - local t = string.sub(line,pos) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res,t) - break - end - end - end - return res +function ParseCSVLine(line) + local res = {} + local pos = 1 + sep = "," + while true do + local c = string.sub(line, pos, pos) + if (c == "") then + break + end + if (c == '"') then + local txt = "" + repeat + local startp, endp = string.find(line, '^%b""', pos) + txt = txt .. string.sub(line, startp + 1, endp - 1) + pos = endp + 1 + c = string.sub(line, pos, pos) + if (c == '"') then + txt = txt .. '"' + end + until (c ~= '"') + txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. txt) + table.insert(res, txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp, endp = string.find(line, sep, pos) + if (startp) then + local t = string.sub(line, pos, startp - 1) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + pos = endp + 1 + else + local t = string.sub(line, pos) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + break + end + end + end + return res end local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet --- encoding +-- encode title/filename if it contains invalid filename characters +-- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup +-- function enc(data) return ((data:gsub( ".", @@ -1448,7 +1452,9 @@ function enc(data) end ) .. ({"", "==", "="})[#data % 3 + 1]) end - +-- +-- decode an encoded title/filename +-- function dec(data) data = string.gsub(data, "[^" .. b .. "=]", "") return (data:gsub( @@ -1526,9 +1532,7 @@ function save_prepared() return true end - function update_monitor() - dbg_method("update_monitor") local tableback = "black" local text = "" @@ -1555,12 +1559,17 @@ function update_monitor() text .. "
of " .. #prepared_songs .. "
" end - text = text .. "
Lyric Page: " .. page_index + text = + text .. + "
Lyric Page: " .. + page_index text = text .. " of " .. #lyrics .. "
" - if #verses ~= nil and mon_verse>0 then - text = text .. "
Verse: " .. mon_verse - text = text .. " of " .. #verses .. "
" - end + if #verses ~= nil and mon_verse > 0 then + text = + text .. + "
Verse: " .. mon_verse + text = text .. " of " .. #verses .. "
" + end text = text .. "
" if not anythingActive() then tableback = "#440000" @@ -1590,7 +1599,8 @@ function update_monitor() text = text .. "Current
Page" - text = text .. " • " .. mon_lyric .. "" + text = + text .. " • " .. mon_lyric .. "" end if mon_nextlyric ~= "" and mon_nextlyric ~= nil then text = @@ -1603,7 +1613,8 @@ function update_monitor() text .. "Alt
Lyric" text = - text .. " • " .. mon_alt .. "" + text .. + " • " .. mon_alt .. "" end if mon_nextalt ~= "" and mon_nextalt ~= nil then text = @@ -1661,8 +1672,8 @@ function get_song_text(name) end file:close() else - return nil - end + return nil + end return song_lines end @@ -1676,81 +1687,134 @@ end -- A function named script_properties defines the properties that the user -- can change for the entire script module itself -local help = "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. - " Markup      Syntax         Markup      Syntax \n" .. - "============ ==========   ============ ==========\n" .. - " Display n Lines    #L:n      End Page after Line   Line ###\n" .. - " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. - " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. - " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. - " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. - "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. - "Comment Line    // Line       Block Comments    //[ and //] \n" .. - "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line" - +local help = + "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. + " Markup      Syntax         Markup      Syntax \n" .. + "============ ==========   ============ ==========\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line" + function script_properties() dbg_method("script_properties") - editVisSet = false + editVisSet = false script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) ------------ - obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲",change_info_visible) - local gp = obs.obs_properties_create() + obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) + ----------- + obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲", change_info_visible) + local gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) + obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) obs.obs_properties_add_text(gp, "prop_edit_song_text", "\tSong Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) - obs.obs_properties_add_button(gp, "prop_opensong_button","Edit Song with System Editor", open_song_clicked) - obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) - obs.obs_properties_add_group(script_props,"info_grp","Song Title (filename) and Lyrics Information", obs.OBS_GROUP_NORMAL,gp) ------------- - obs.obs_properties_add_button(script_props, "prepared_showing", "▲- HIDE PREPARED SONGS -▲",change_prepared_visible) - gp = obs.obs_properties_create() - local prop_dir_list = obs.obs_properties_add_list(gp,"prop_directory_list","Song Directory",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) - obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) - local gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() - local prepare_prop = obs.obs_properties_add_list(gps,"prop_prepared_list","Prepared Songs",obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prepare_prop, name, name) - end - obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) - obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) - obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List",edit_prepared_clicked) - local eps = obs.obs_properties_create() - local edit_prop = obs.obs_properties_add_editable_list(eps, "prep_list", "Prepared Songs/Text", obs.OBS_EDITABLE_LIST_TYPE_STRINGS,nil,nil ) - obs.obs_property_set_modified_callback(edit_prop, setEditVis) - obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes",save_edits_clicked) - local edit_group_prop = obs.obs_properties_add_group(gps,"edit_grp","Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", obs.OBS_GROUP_NORMAL,eps) - obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) - obs.obs_properties_add_group(script_props,"mng_grp","Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL,gp) ------------------- - obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲",change_ctrl_visible) - hotkey_props = obs.obs_properties_create() - local hktitletext = obs.obs_properties_add_text(hotkey_props,"hotkey-title", "\t", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) + obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) + obs.obs_properties_add_group( + script_props, + "info_grp", + "Song Title (filename) and Lyrics Information", + obs.OBS_GROUP_NORMAL, + gp + ) + ------------ + obs.obs_properties_add_button( + script_props, + "prepared_showing", + "▲- HIDE PREPARED SONGS -▲", + change_prepared_visible + ) + gp = obs.obs_properties_create() + local prop_dir_list = + obs.obs_properties_add_list( + gp, + "prop_directory_list", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) + local gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) + local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + local prepare_prop = + obs.obs_properties_add_list( + gps, + "prop_prepared_list", + "Prepared Songs", + obs.OBS_COMBO_TYPE_EDITABLE, + obs.OBS_COMBO_FORMAT_STRING + ) + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prepare_prop, name, name) + end + obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) + local eps = obs.obs_properties_create() + local edit_prop = + obs.obs_properties_add_editable_list( + eps, + "prep_list", + "Prepared Songs/Text", + obs.OBS_EDITABLE_LIST_TYPE_STRINGS, + nil, + nil + ) + obs.obs_property_set_modified_callback(edit_prop, setEditVis) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes", save_edits_clicked) + local edit_group_prop = + obs.obs_properties_add_group( + gps, + "edit_grp", + "Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", + obs.OBS_GROUP_NORMAL, + eps + ) + obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) + ------------------ + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) + hotkey_props = obs.obs_properties_create() + local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "\t", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button(hotkey_props,"prop_reset_button","Reset to First Prepared Song",reset_button_clicked) - ctrl_grp_prop = obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)", obs.OBS_GROUP_NORMAL,hotkey_props) - obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) ------- - obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲",change_options_visible) - gp = obs.obs_properties_create() + obs.obs_properties_add_button( + hotkey_props, + "prop_reset_button", + "Reset to First Prepared Song", + reset_button_clicked + ) + ctrl_grp_prop = + obs.obs_properties_add_group( + script_props, + "ctrl_grp", + "Lyric Control Buttons (with Assigned HotKeys)", + obs.OBS_GROUP_NORMAL, + hotkey_props + ) + obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) + ------ + obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) + gp = obs.obs_properties_create() local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "\tLines to Display", 1, 50, 1) obs.obs_property_set_long_description( lines_prop, @@ -1758,11 +1822,10 @@ function script_properties() ) local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") - local link_prop = - obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") + local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") local transition_prop = - obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") obs.obs_property_set_modified_callback(transition_prop, change_transition_property) obs.obs_property_set_long_description( transition_prop, @@ -1771,11 +1834,11 @@ function script_properties() local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) obs.obs_properties_add_int_slider(gp, "text_fade_speed", "\tFade Speed", 1, 10, 1) - obs.obs_properties_add_group(script_props,"disp_grp","Display Options", obs.OBS_GROUP_NORMAL,gp) -------------- - obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲",change_src_visible) - gp = obs.obs_properties_create() - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) + obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) + ------------- + obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) + gp = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) local source_prop = obs.obs_properties_add_list( gp, @@ -1808,35 +1871,51 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) - xgp = obs.obs_properties_create() - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") - local extra_linked_prop = obs.obs_properties_add_list(xgp,"extra_linked_list","Linked Sources ",obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) - -- initialize previously loaded extra properties from table - for _, sourceName in ipairs(extra_sources) do - obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) - end - local extra_source_prop = obs.obs_properties_add_list(xgp,"extra_source_list"," Select Source:",obs.OBS_COMBO_TYPE_LIST,obs.OBS_COMBO_FORMAT_STRING) - obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) - local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) - local extra_group_prop = obs.obs_properties_add_group(gp,"xtr_grp","Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL,xgp) - obs.obs_properties_add_group(script_props,"src_grp","Text Sources in Scenes", obs.OBS_GROUP_NORMAL,gp) - local count = obs.obs_property_list_item_count(extra_linked_prop) - if count > 0 then - obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") - else - obs.obs_property_set_visible(extra_group_prop, false) - end + obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + xgp = obs.obs_properties_create() + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") + local extra_linked_prop = + obs.obs_properties_add_list( + xgp, + "extra_linked_list", + "Linked Sources ", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + -- initialize previously loaded extra properties from table + for _, sourceName in ipairs(extra_sources) do + obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) + end + local extra_source_prop = + obs.obs_properties_add_list( + xgp, + "extra_source_list", + " Select Source:", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) + local clearcall_prop = + obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) + local extra_group_prop = + obs.obs_properties_add_group(gp, "xtr_grp", "Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL, xgp) + obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) + local count = obs.obs_property_list_item_count(extra_linked_prop) + if count > 0 then + obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") + else + obs.obs_property_set_visible(extra_group_prop, false) + end local sources = obs.obs_enum_sources() - obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") + obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") if sources ~= nil then local n = {} for _, source in ipairs(sources) do - local name = obs.obs_source_get_name(source) - if isValid(source) then - obs.obs_property_list_add_string(extra_source_prop,name,name) -- add source to extra list - end + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list + end source_id = obs.obs_source_get_unversioned_id(source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then n[#n + 1] = name @@ -1856,33 +1935,32 @@ function script_properties() end obs.source_list_release(sources) - ------------------ - obs.obs_property_set_enabled(hktitletext,false) - obs.obs_property_set_visible(edit_group_prop, false) - obs.obs_property_set_visible(meta_group_prop, false) + ----------------- + obs.obs_property_set_enabled(hktitletext, false) + obs.obs_property_set_visible(edit_group_prop, false) + obs.obs_property_set_visible(meta_group_prop, false) return script_props end -- script_update is called when settings are changed function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") - display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") source_name = obs.obs_data_get_string(settings, "prop_source_list") alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") static_source_name = obs.obs_data_get_string(settings, "prop_static_list") title_source_name = obs.obs_data_get_string(settings, "prop_title_list") ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") - link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") + link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") end -- A function named script_defaults will be called to set the default settings function script_defaults(settings) dbg_method("script_defaults") obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") + obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -1893,42 +1971,43 @@ function script_defaults(settings) end end - --verify source has an opacity setting function isValid(source) - if source ~= nil then - local flags = obs.obs_source_get_output_flags(source) - print(obs.obs_source_get_name(source) .. " - " .. flags) - local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO,obs.OBS_SOURCE_CUSTOM_DRAW) - if bit.band(flags, targetFlag) == targetFlag then - return true - end - end - return false + if source ~= nil then + local flags = obs.obs_source_get_output_flags(source) + print(obs.obs_source_get_name(source) .. " - " .. flags) + local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) + if bit.band(flags, targetFlag) == targetFlag then + return true + end + end + return false end - --- adds an extra linked source. +-- adds an extra linked source. -- Source must be text source, or have 'Color Correction' Filter applied function link_source_selected(props, prop, settings) dbg_method("link_source_selected") local extra_source = obs.obs_data_get_string(settings, "extra_source_list") - if extra_source ~= nil and extra_source ~= "" then - local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") - obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) - obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) - obs.obs_data_set_string(script_sets, "extra_source_list", "") - obs.obs_property_set_description(extra_linked_list, "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")") - end - return true + if extra_source ~= nil and extra_source ~= "" then + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) + obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) + obs.obs_data_set_string(script_sets, "extra_source_list", "") + obs.obs_property_set_description( + extra_linked_list, + "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" + ) + end + return true end -- removes linked sources function do_linked_clicked(props, p) dbg_method("do_link_clicked") - obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), true) - obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), false) - obs.obs_properties_apply_settings(props, script_sets) + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), true) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), false) + obs.obs_properties_apply_settings(props, script_sets) return true end @@ -1938,228 +2017,253 @@ function clear_linked_clicked(props, p) dbg_method("clear_linked_clicked") local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") obs.obs_property_list_clear(extra_linked_list) - obs.obs_property_set_visible(obs.obs_properties_get(props,"xtr_grp"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props,"do_link_button"), true) - obs.obs_property_set_description(extra_linked_list, "Linked Sources ") + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) + obs.obs_property_set_description(extra_linked_list, "Linked Sources ") return true end - -- A function named script_description returns the description shown to -- the user function script_description() - return description - end + return description +end function vMode(vis) - return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" + return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" end - + function expand_all_groups(props, prop, settings) - expandcollapse = not expandcollapse - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"info_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"mng_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"disp_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"src_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props,"ctrl_grp"), expandcollapse) - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - return true + expandcollapse = not expandcollapse + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "info_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "mng_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "disp_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "src_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "ctrl_grp"), expandcollapse) + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + return true end - - - function all_vis_equal(props) - if (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"prep_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) or not - (obs.obs_property_visible(obs.obs_properties_get(script_props,"info_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"mng_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"disp_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"src_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props,"ctrl_grp"))) then - expandcollapse = not expandcollapse - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - end + if + (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "prep_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) or + not (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "mng_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) + then + expandcollapse = not expandcollapse + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description( + obs.obs_properties_get(props, "expand_all_button"), + mode1 .. "ALL GROUPS" .. mode2 + ) + end end function change_info_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"info_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "info_showing"), mode1 .. "SONG INFORMATION" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "info_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + all_vis_equal(props) return true end function change_prepared_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"mng_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "prepared_showing"), mode1 .. "PREPARED SONGS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "mng_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + all_vis_equal(props) return true end function change_options_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"disp_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "options_showing"), mode1 .. "DISPLAY OPTIONS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "disp_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + all_vis_equal(props) return true end function change_src_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"src_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "src_showing"), mode1 .. "SOURCE TEXT SELECTIONS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "src_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + all_vis_equal(props) return true end function change_ctrl_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props,"ctrl_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp,vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - all_vis_equal(props) + local pp = obs.obs_properties_get(script_props, "ctrl_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + all_vis_equal(props) return true end function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") - dbg_bool("Fade: ",text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) + dbg_bool("Fade: ", text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) return true end - + function show_help_button(props, prop, settings) -dbg_method("show help") - local hb = obs.obs_properties_get(props, "show_help_button") - showhelp = not showhelp - if showhelp then - obs.obs_property_set_description(hb, help) - else - obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") - end - return true + dbg_method("show help") + local hb = obs.obs_properties_get(props, "show_help_button") + showhelp = not showhelp + if showhelp then + obs.obs_property_set_description(hb, help) + else + obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") + end + return true end function setEditVis(props, prop, settings) -- hides edit group on initial showing - dbg_method("setEditVis") - if not editVisSet then - local pp = obs.obs_properties_get(script_props,"edit_grp") - obs.obs_property_set_visible(pp, false) - pp = obs.obs_properties_get(props,"meta") - obs.obs_property_set_visible(pp, false) - editVisSet = true - end + dbg_method("setEditVis") + if not editVisSet then + local pp = obs.obs_properties_get(script_props, "edit_grp") + obs.obs_property_set_visible(pp, false) + pp = obs.obs_properties_get(props, "meta") + obs.obs_property_set_visible(pp, false) + editVisSet = true + end end function filter_songs_clicked(props, p) - local pp = obs.obs_properties_get(props,"meta") - if not obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "filter_songs_button") - obs.obs_property_set_description(mpb, "Clear Filters") -- change button function - meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") - refresh_directory() - else - obs.obs_property_set_visible(pp, false) - meta_tags = "" -- clear meta tags - refresh_directory() - local mpb = obs.obs_properties_get(props, "filter_songs_button") -- - obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function - end - return true + local pp = obs.obs_properties_get(props, "meta") + if not obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "filter_songs_button") + obs.obs_property_set_description(mpb, "Clear Filters") -- change button function + meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + refresh_directory() + else + obs.obs_property_set_visible(pp, false) + meta_tags = "" -- clear meta tags + refresh_directory() + local mpb = obs.obs_properties_get(props, "filter_songs_button") -- + obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function + end + return true end function edit_prepared_clicked(props, p) - local pp = obs.obs_properties_get(props,"edit_grp") - if obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared List") - return true - end + local pp = obs.obs_properties_get(props, "edit_grp") + if obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared List") + return true + end local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") local count = obs.obs_property_list_item_count(prop_prep_list) - local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2 do - obs.obs_data_array_erase(songNames,0) - end - end + if count2 > 0 then + for i = 0, count2 do + obs.obs_data_array_erase(songNames, 0) + end + end - for i = 0, count-1 do + for i = 0, count - 1 do local song = obs.obs_property_list_item_string(prop_prep_list, i) - local array_obj = obs.obs_data_create() - obs.obs_data_set_string(array_obj, "value", song) - obs.obs_data_array_push_back(songNames,array_obj) - obs.obs_data_release(array_obj) - end - obs.obs_data_set_array(script_sets, "prep_list", songNames) - obs.obs_data_array_release(songNames) - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Cancel Prepared Edits") + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song) + obs.obs_data_array_push_back(songNames, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(script_sets, "prep_list", songNames) + obs.obs_data_array_release(songNames) + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Cancel Prepared Edits") return true end -- removes prepared songs function save_edits_clicked(props, p) - load_source_song_directory(false) - prepared_songs = {} - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_clear(prop_prep_list) - local songNames = obs.obs_data_get_array(script_sets, "prep_list") + load_source_song_directory(false) + prepared_songs = {} + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2-1 do - local item = obs.obs_data_array_item(songNames, i); - local itemName = obs.obs_data_get_string(item, "value"); - if get_index_in_list(song_directory, itemName) ~= nil then - prepared_songs[#prepared_songs+1] = itemName - obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) - end - obs.obs_data_release(item) - end - end - obs.obs_data_array_release(songNames) - save_prepared() - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) - prepared_index = 1 - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - prepared_index = 0 - end - pp = obs.obs_properties_get(script_props,"edit_grp") - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared Songs List") - obs.obs_properties_apply_settings(props, script_sets) + if count2 > 0 then + for i = 0, count2 - 1 do + local item = obs.obs_data_array_item(songNames, i) + local itemName = obs.obs_data_get_string(item, "value") + if get_index_in_list(song_directory, itemName) ~= nil then + prepared_songs[#prepared_songs + 1] = itemName + obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(songNames) + save_prepared() + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + prepared_index = 1 + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + prepared_index = 0 + end + pp = obs.obs_properties_get(script_props, "edit_grp") + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + obs.obs_properties_apply_settings(props, script_sets) return true end @@ -2173,10 +2277,9 @@ function change_transition_property(props, prop, settings) return true end - -- A function named script_save will be called when the script is saved -function script_save(settings) - dbg_method("script_save") +function script_save(settings) + dbg_method("script_save") save_prepared() local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) @@ -2205,24 +2308,23 @@ function script_save(settings) hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) - --- - --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded - --- - local extra_sources_array = obs.obs_data_array_create() + --- + --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded + --- + local extra_sources_array = obs.obs_data_array_create() local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") local count = obs.obs_property_list_item_count(extra_linked_list) - for i = 0, count-1 do - local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local array_obj = obs.obs_data_create() - obs.obs_data_set_string(array_obj, "value", source_name) - obs.obs_data_array_push_back(extra_sources_array,array_obj) - obs.obs_data_release(array_obj) - end - obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) - obs.obs_data_array_release(extra_sources_array) + for i = 0, count - 1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", source_name) + obs.obs_data_array_push_back(extra_sources_array, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) + obs.obs_data_array_release(extra_sources_array) end - -- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS -- sets callback to obs_frontend Event Callback -- @@ -2230,71 +2332,71 @@ function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - hotkey_n_key = get_hotkeys(hotkey_save_array,"Next Lyric", " ......................") + hotkey_n_key = get_hotkeys(hotkey_save_array, "Next Lyric", " ......................") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") - hotkey_p_key = get_hotkeys(hotkey_save_array,"Previous Lyric", " ..................") + hotkey_p_key = get_hotkeys(hotkey_save_array, "Previous Lyric", " ..................") obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") - hotkey_c_key = get_hotkeys(hotkey_save_array,"Show/Hide Lyrics", " ..............") + hotkey_c_key = get_hotkeys(hotkey_save_array, "Show/Hide Lyrics", " ..............") obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") - hotkey_n_p_key = get_hotkeys(hotkey_save_array,"Next Prepared", " ................") + hotkey_n_p_key = get_hotkeys(hotkey_save_array, "Next Prepared", " ................") obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") - hotkey_p_p_key = get_hotkeys(hotkey_save_array,"Previous Prepared", "............") + hotkey_p_p_key = get_hotkeys(hotkey_save_array, "Previous Prepared", "............") obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") - hotkey_home_key = get_hotkeys(hotkey_save_array,"Reset to Song Start", " ..........") + hotkey_home_key = get_hotkeys(hotkey_save_array, "Reset to Song Start", " ..........") obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) - hotkey_reset_id = obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_reset_id = + obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") - hotkey_reset_key = get_hotkeys(hotkey_save_array,"Reset to 1st Prepared", " .......") + hotkey_reset_key = get_hotkeys(hotkey_save_array, "Reset to 1st Prepared", " .......") obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) script_sets = settings source_name = obs.obs_data_get_string(settings, "prop_source_list") - extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - - -- load previously defined extra sources from settings array into table - -- script_properties function will take them from the table and restore them as UI properties - -- - local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - local count = obs.obs_data_array_count(extra_sources_array) - if count > 0 then - for i = 0, count do - local item = obs.obs_data_array_item(extra_sources_array, i); - local sourceName = obs.obs_data_get_string(item, "value"); - if sourceName ~= "" then - extra_sources[#extra_sources + 1] = sourceName - end - obs.obs_data_release(item) - end - end - obs.obs_data_array_release(extra_sources_array) - - - -- load prepared songs from stored file - -- + extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + + -- load previously defined extra sources from settings array into table + -- script_properties function will take them from the table and restore them as UI properties + -- + local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + local count = obs.obs_data_array_count(extra_sources_array) + if count > 0 then + for i = 0, count do + local item = obs.obs_data_array_item(extra_sources_array, i) + local sourceName = obs.obs_data_get_string(item, "value") + if sourceName ~= "" then + extra_sources[#extra_sources + 1] = sourceName + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(extra_sources_array) + + -- load prepared songs from stored file + -- if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -2307,14 +2409,14 @@ function script_load(settings) end file:close() end - name_hotkeys() - + name_hotkeys() + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end --- ------ ---------- Source Showing or Source Active Helper Functions +--------- Source Showing or Source Active Helper Functions --------- Return true if sourcename given is showing anywhere or on in the Active scene ------ --- @@ -2339,16 +2441,16 @@ function isActive(sourceName) end function anythingShowing() - return isShowing(source_name) or isShowing(alternate_source_name) - or isShowing(title_source_name) or isShowing(static_source_name) + return isShowing(source_name) or isShowing(alternate_source_name) or isShowing(title_source_name) or + isShowing(static_source_name) end function sourceShowing() - return isShowing(source_name) + return isShowing(source_name) end function alternateShowing() - return isShowing(alternate_source_name) + return isShowing(alternate_source_name) end function titleShowing() @@ -2360,24 +2462,24 @@ function staticShowing() end function anythingActive() - return isActive(source_name) or isActive(alternate_source_name) - or isActive(title_source_name) or isActive(static_source_name) + return isActive(source_name) or isActive(alternate_source_name) or isActive(title_source_name) or + isActive(static_source_name) end function sourceActive() - return isActive(source_name) + return isActive(source_name) end function alternateActive() - return isActive(alternate_source_name) + return isActive(alternate_source_name) end function titleActive() - return isActive(title_source_name) + return isActive(title_source_name) end function staticActive() - return isActive(static_source_name) + return isActive(static_source_name) end --- @@ -2388,7 +2490,7 @@ end ------ --- ----------------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------------- -- get_hotkeys(loaded hotkey array, desired prefix text, leader text (between prefix and hotkey label) -- Returns translated hotkey text label with prefix and leader -- e.g. if HotKeyArray contains an assigned hotkey Shift and F1 key combo, then @@ -2396,62 +2498,94 @@ end ---------------------------------------------------------------------------------------------------------- function get_hotkeys(hotkey_array, prefix, leader) - local Translate = {["NUMLOCK"] = "NumLock", ["NUMSLASH"] = "Num/", ["NUMASTERISK"] = "Num*", - ["NUMMINUS"] = "Num-", ["NUMPLUS"] = "Num+", - ["NUMPERIOD"] = "NumDel", ["INSERT"] = "Insert", ["PAGEDOWN"] = "Page-Down", - ["PAGEUP"] = "Page-Up", ["HOME"] = "Home", ["END"] = "End",["RETURN"] = "Return", - ["UP"] = "Up", ["DOWN"] = "Down", ["RIGHT"] = "Right", ["LEFT"] = "Left", - ["SCROLLLOCK"] = "Scroll-Lock", ["BACKSPACE"] = "Backspace", ["ESCAPE"] = "Esc", - ["MENU"] = "Menu", ["META"] = "Meta", ["PRINT"] = "Prt", ["TAB"] = "Tab", - ["DELETE"] = "Del", ["CAPSLOCK"] = "Caps-Lock", ["NUMEQUAL"] = "Num=", ["PAUSE"] = "Pause", - ["VK_VOLUME_MUTE"] = "Vol Mute", ["VK_VOLUME_DOWN"] = "Vol Dwn", ["VK_VOLUME_UP"] = "Vol Up", - ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", ["VK_MEDIA_STOP"] = "Media Stop", - ["VK_MEDIA_PREV_TRACK"] = "Media Prev", ["VK_MEDIA_NEXT_TRACK"] = "Media Next"} - - item = obs.obs_data_array_item(hotkey_array, 0) - local key = string.sub(obs.obs_data_get_string(item,"key"),9) - if Translate[key] ~= nil then - key = Translate[key] - elseif string.sub(key,1,3) == "NUM" then - key = "Num " .. string.sub(key,4) - elseif string.sub(key,1,5) == "MOUSE" then - key = "Mouse " .. string.sub(key,6) - end - - obs.obs_data_release(item) + local Translate = { + ["NUMLOCK"] = "NumLock", + ["NUMSLASH"] = "Num/", + ["NUMASTERISK"] = "Num*", + ["NUMMINUS"] = "Num-", + ["NUMPLUS"] = "Num+", + ["NUMPERIOD"] = "NumDel", + ["INSERT"] = "Insert", + ["PAGEDOWN"] = "Page-Down", + ["PAGEUP"] = "Page-Up", + ["HOME"] = "Home", + ["END"] = "End", + ["RETURN"] = "Return", + ["UP"] = "Up", + ["DOWN"] = "Down", + ["RIGHT"] = "Right", + ["LEFT"] = "Left", + ["SCROLLLOCK"] = "Scroll-Lock", + ["BACKSPACE"] = "Backspace", + ["ESCAPE"] = "Esc", + ["MENU"] = "Menu", + ["META"] = "Meta", + ["PRINT"] = "Prt", + ["TAB"] = "Tab", + ["DELETE"] = "Del", + ["CAPSLOCK"] = "Caps-Lock", + ["NUMEQUAL"] = "Num=", + ["PAUSE"] = "Pause", + ["VK_VOLUME_MUTE"] = "Vol Mute", + ["VK_VOLUME_DOWN"] = "Vol Dwn", + ["VK_VOLUME_UP"] = "Vol Up", + ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", + ["VK_MEDIA_STOP"] = "Media Stop", + ["VK_MEDIA_PREV_TRACK"] = "Media Prev", + ["VK_MEDIA_NEXT_TRACK"] = "Media Next" + } + + item = obs.obs_data_array_item(hotkey_array, 0) + local key = string.sub(obs.obs_data_get_string(item, "key"), 9) + if Translate[key] ~= nil then + key = Translate[key] + elseif string.sub(key, 1, 3) == "NUM" then + key = "Num " .. string.sub(key, 4) + elseif string.sub(key, 1, 5) == "MOUSE" then + key = "Mouse " .. string.sub(key, 6) + end + + obs.obs_data_release(item) local val = prefix - if key ~= nil and key ~= "" then - val = val .. " " .. leader .. " " - if obs.obs_data_get_bool(item,"control") then val = val.."Ctrl + " end - if obs.obs_data_get_bool(item,"alt") then val = val.."Alt + " end - if obs.obs_data_get_bool(item,"shift") then val = val.."Shift + " end - if obs.obs_data_get_bool(item,"command") then val = val.."Cmd + " end - val = val .. key - end - return val + if key ~= nil and key ~= "" then + val = val .. " " .. leader .. " " + if obs.obs_data_get_bool(item, "control") then + val = val .. "Ctrl + " + end + if obs.obs_data_get_bool(item, "alt") then + val = val .. "Alt + " + end + if obs.obs_data_get_bool(item, "shift") then + val = val .. "Shift + " + end + if obs.obs_data_get_bool(item, "command") then + val = val .. "Cmd + " + end + val = val .. key + end + return val end -- name_hotkeys function renames the seven hotkeys to include their defined key text -- function name_hotkeys() - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) end - -------- ---------------- ------------------------ SOURCE FUNCTIONS ---------------- -------- --- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change --- +-- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change +-- function rename_source() -- pause_timer = true local sources = obs.obs_enum_sources() @@ -2485,7 +2619,7 @@ function rename_source() if loadLyric_items[index] == nil then loadLyric_items[index] = 1 -- First time to find this source so mark with 1 else - loadLyric_items[index] = loadLyric_items[index]+1 -- Found this source again so increment + loadLyric_items[index] = loadLyric_items[index] + 1 -- Found this source again so increment end obs.obs_data_release(settings) -- release memory end @@ -2510,7 +2644,9 @@ function rename_source() -- Mark Duplicates if index ~= nil then if loadLyric_items[index] > 1 then - name = '' .. name .. " " .. loadLyric_items[index] .. "" + name = + '' .. + name .. " " .. loadLyric_items[index] .. "" end if (c_name ~= name) then obs.obs_source_set_name(source, name) @@ -2527,78 +2663,80 @@ function rename_source() end -- Names the initial "Prepare Lyric" source (prior to being renamed to "Load Lyrics for: {song name} --- +-- source_def.get_name = function() return "Prepare Lyric" end -- Called when OBS is saving data. This will be called on each copy of Load Lyric source -- Used to initiate rename_source() function when the source dialog closes --- saved flag prevents it from being called by every source each time. +-- saved flag prevents it from being called by every source each time. -- source_def.save = function(data, settings) - if saved then return end -- we only need it once, not for every load lyric source copy - dbg_method("Source_save") - saved = true + if saved then + return + end -- we only need it once, not for every load lyric source copy + dbg_method("Source_save") + saved = true using_source = true rename_source() -- Rename and Mark sources instantly on update (WZ) end -- Called when a change is made in the source dialog (Currently Not Used) --- +-- source_def.update = function(data, settings) -dbg_method("update") + dbg_method("update") end -- Called when the source dialog is loaded (Currently not Used) -- source_def.load = function(data) -dbg_method("load") + dbg_method("load") end -- Called when the refresh button is pressed in the source dialog -- It reloads the song directory and applies any meta-tag filters if entered --- +-- function source_refresh_button_clicked(props, p) - dbg_method("source_refresh_button") - source_filter = true - dbg_inner("tags: " .. source_meta_tags) + dbg_method("source_refresh_button") + source_filter = true + dbg_inner("tags: " .. source_meta_tags) load_source_song_directory(true) table.sort(song_directory) - local prop_dir_list = obs.obs_properties_get(props,"songs") - obs.obs_property_list_clear(prop_dir_list) -- clear directories + local prop_dir_list = obs.obs_properties_get(props, "songs") + obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do - dbg_inner("SLD: " .. name) + dbg_inner("SLD: " .. name) obs.obs_property_list_add_string(prop_dir_list, name, name) - end + end return true end --- Keeps variable source-meta-tags up-to-date +-- Keeps variable source-meta-tags up-to-date -- Note: This could be done only when refreshing the directory (see source_refresh_button_clicked) --- +-- function update_source_metatags(props, p, settings) - source_meta_tags = obs.obs_data_get_string(settings,"metatags") - return true + source_meta_tags = obs.obs_data_get_string(settings, "metatags") + return true end --- Called when a user makes a song selection in the source dialog --- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens +-- Called when a user makes a song selection in the source dialog +-- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens -- Saved flag is cleared to mark changes have occured for save event --- +-- function source_selection_made(props, prop, settings) -dbg_method("source_selection") - local name = obs.obs_data_get_string(settings,"songs") - saved = false -- mark properties changed - using_source = true - prepare_selected(name) + dbg_method("source_selection") + local name = obs.obs_data_get_string(settings, "songs") + saved = false -- mark properties changed + using_source = true + prepare_selected(name) return true end -- Standard OBS get Properties function for OBS source dialog --- +-- source_def.get_properties = function(data) - source_filter = true + source_filter = true load_source_song_directory(true) local source_props = obs.obs_properties_create() local source_dir_list = @@ -2609,41 +2747,40 @@ source_def.get_properties = function(data) obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) + obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) table.sort(song_directory) for _, name in ipairs(song_directory) do obs.obs_property_list_add_string(source_dir_list, name, name) end - gps = obs.obs_properties_create() - source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) - obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) - obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() + gps = obs.obs_properties_create() + source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) + obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) + obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode - obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) - dbg_inner("props") + obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) + dbg_inner("props") return source_props - end --- Called when the source is created --- saves pointer to settings in global sourc_sets for convienence +-- Called when the source is created +-- saves pointer to settings in global sourc_sets for convienence -- Sets callbacks for active, showing, deactive, and updated callbacks --- +-- source_def.create = function(settings, source) -dbg_method("create") + dbg_method("create") data = {} - source_sets = settings + source_sets = settings obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback return data end --- Sets default settings for Activate Source in Preview +-- Sets default settings for Activate Source in Preview -- source_def.get_defaults = function(settings) obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) @@ -2651,7 +2788,7 @@ end -- On Event Functions -- These manage keeping the HTML monitor page updated when changes happen like scene changes that remove --- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are +-- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are -- either created or deleted. Rename changes color and marks with *, sources that are reference copies of the same source -- as accidentally changing the settings like the loaded song in one will change it in the reference copies. -- @@ -2672,35 +2809,35 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) - dbg_method("on_event: " .. event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page - dbg_bool("Active:",source_active) - obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS - end - if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes - dbg_inner("Scene Change") - obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change - end -end - --- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. --- + dbg_method("on_event: " .. event) + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page + dbg_bool("Active:", source_active) + obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS + end + if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes + dbg_inner("Scene Change") + obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change + end +end + +-- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. +-- function load_source_song(source, preview) dbgsp("load_source_song") local settings = obs.obs_source_get_settings(source) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") - using_source = true - load_source = source - all_sources_fade = true -- fade title and source the first time - set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in - if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles - prepare_selected(song) - end - transition_lyric_text() - if obs.obs_data_get_bool(settings, "source_home_on_active") then - home_prepared(true) - end + using_source = true + load_source = source + all_sources_fade = true -- fade title and source the first time + set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in + if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles + prepare_selected(song) + end + transition_lyric_text() + if obs.obs_data_get_bool(settings, "source_home_on_active") then + home_prepared(true) + end end obs.obs_data_release(settings) end @@ -2734,7 +2871,7 @@ end -- Call back when load source (not text source) goes to the Active -- loads the selected song and sets the current scene name for the HTML monitor --- +-- function source_showing(cd) dbg_custom("source_showing") local source = obs.calldata_source(cd, "source") @@ -2744,12 +2881,12 @@ function source_showing(cd) load_source_song(source, true) end --- dbg functions --- +-- dbg functions +-- function dbg_traceback() - if DEBUG then - print("Trace: " .. debug.traceback()) - end + if DEBUG then + print("Trace: " .. debug.traceback()) + end end function dbg(message) @@ -2771,9 +2908,9 @@ function dbg_method(message) end function dbgsp(message) -if DEBUG then - dbg("====SPECIAL=====================>> " .. message) -end + if DEBUG then + dbg("====SPECIAL=====================>> " .. message) + end end function dbg_custom(message) if DEBUG_CUSTOM then @@ -2783,18 +2920,19 @@ end function dbg_bool(name, value) if DEBUG_BOOL then - local message = "BOOL: " .. name + local message = "BOOL: " .. name if value then message = message .. " = true" else message = message .. " = false" end - dbg(message) + dbg(message) end end obs.obs_register_source(source_def) -description = [[ +description = + [[

OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian

-]] \ No newline at end of file +]] From b163adee3369bd762d4f653a948cc833ad97eaf4 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Sat, 16 Oct 2021 12:49:56 -0600 Subject: [PATCH 054/105] Added background and gradient colors to on/off, fade Added background and gradient colors to on/off, fade. Need to get settings prior to fade down or off and restore them rather than set to 100% as the ON limit. --- lyrics+.lua | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 523d8c4..ce274f0 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -615,6 +615,8 @@ function apply_source_opacity() local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new text outline opacity + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new text outlin local title_source = obs.obs_get_source_by_name(title_source_name) if title_source ~= nil then obs.obs_source_update(title_source, settings) @@ -625,6 +627,8 @@ function apply_source_opacity() local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new text outline opacity + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new text outlin local static_source = obs.obs_get_source_by_name(static_source_name) if static_source ~= nil then obs.obs_source_update(static_source, settings) @@ -644,8 +648,10 @@ function apply_source_opacity() source_id = obs.obs_source_get_unversioned_id(extra_source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new text outline opacity + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new text outline opacity obs.obs_source_update(extra_source, settings) -- merge new opacity values obs.obs_data_release(settings) else -- check for filter named "Color Correction" @@ -1711,7 +1717,7 @@ function script_properties() local gp = obs.obs_properties_create() obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) - obs.obs_properties_add_text(gp, "prop_edit_song_text", "\tSong Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) @@ -1790,7 +1796,7 @@ function script_properties() ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) hotkey_props = obs.obs_properties_create() - local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "\t", obs.OBS_TEXT_DEFAULT) + local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) @@ -1815,7 +1821,7 @@ function script_properties() ------ obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) gp = obs.obs_properties_create() - local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "\tLines to Display", 1, 50, 1) + local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "Lines to Display", 1, 50, 1) obs.obs_property_set_long_description( lines_prop, "Sets default lines per page of lyric, overwritten by Markup: #L:n" @@ -1833,7 +1839,7 @@ function script_properties() ) local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - obs.obs_properties_add_int_slider(gp, "text_fade_speed", "\tFade Speed", 1, 10, 1) + obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) ------------- obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) From 8c03d0ae4944d248f61813ec37a19f1db0dc7d51 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Sat, 16 Oct 2021 17:41:41 -0600 Subject: [PATCH 055/105] Update lyrics+.lua Just some optimized code and a small fix to add backgrounds/Gradients for fading JUST TEXT. --- lyrics+.lua | 67 ++++++++++++++++------------------------------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index ce274f0..1b5df30 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -49,6 +49,7 @@ prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_s song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) prepared_songs = {} -- holds pre-prepared list of songs to use extra_sources = {} -- holder for extra sources settings +max_opacity = {} -- record maximum opacity settings for sources link_text = false -- true if Title and Static should fade with text only during hide/show link_extras = false -- extras fade with text always when true, only during hide/show when false @@ -586,55 +587,28 @@ end ------------------------ PROGRAM FUNCTIONS ---------------- -------- - -function apply_source_opacity() - -- dbg_method("apply_source_visiblity") - +function setSourceOpacity(sourceName) + print("****** Opacity: " .. text_opacity) local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - local source = obs.obs_get_source_by_name(source_name) + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity + local source = obs.obs_get_source_by_name(sourceName) if source ~= nil then obs.obs_source_update(source, settings) end obs.obs_source_release(source) obs.obs_data_release(settings) +end - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - if alt_source ~= nil then - obs.obs_source_update(alt_source, settings) - end - obs.obs_source_release(alt_source) - obs.obs_data_release(settings) - dbg_bool("All Sources Fade:", all_sources_fade) - dbg_bool("Link Text:", link_text) - if all_sources_fade then - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new text outline opacity - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new text outlin - local title_source = obs.obs_get_source_by_name(title_source_name) - if title_source ~= nil then - obs.obs_source_update(title_source, settings) - end - obs.obs_source_release(title_source) - obs.obs_data_release(settings) - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new text outline opacity - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new text outlin - local static_source = obs.obs_get_source_by_name(static_source_name) - if static_source ~= nil then - obs.obs_source_update(static_source, settings) - end - obs.obs_source_release(static_source) - obs.obs_data_release(settings) +function apply_source_opacity() + setSourceOpacity(source_name) + setSourceOpacity(alternate_source_name) + if all_sources_fade then + setSourceOpacity(title_source_name) + setSourceOpacity(static_source_name) end if link_extras or all_sources_fade then local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") @@ -642,18 +616,16 @@ function apply_source_opacity() if count > 0 then for i = 0, count - 1 do local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - print(source_name) local extra_source = obs.obs_get_source_by_name(source_name) if extra_source ~= nil then source_id = obs.obs_source_get_unversioned_id(extra_source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new text outline opacity - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new text outline opacity - obs.obs_source_update(extra_source, settings) -- merge new opacity values - obs.obs_data_release(settings) + local settings = obs.obs_data_create() + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- set new background opacity + obs.obs_source_update(source, settings) else -- check for filter named "Color Correction" local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") if color_filter ~= nil then -- update filters opacity @@ -682,6 +654,7 @@ function apply_source_opacity() end end + function set_text_visibility(end_status) dbg_method("set_text_visibility") -- if already at desired visibility, then exit From 85a4e7d1dac828f083046d55176fa6f0c7eeb0e7 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Sat, 16 Oct 2021 18:03:47 -0600 Subject: [PATCH 056/105] Update lyrics+.lua Turned off Debugging Messages --- lyrics+.lua | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 1b5df30..3244072 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -122,8 +122,8 @@ source_saved = false -- ick... A saved toggle to keep from repeating the save editVisSet = false -- simple debugging/print mechanism -DEBUG = true -- on switch for entire debugging mechanism -DEBUG_METHODS = true -- print method names +--DEBUG = true -- on switch for entire debugging mechanism +--DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false @@ -588,7 +588,6 @@ end ---------------- -------- function setSourceOpacity(sourceName) - print("****** Opacity: " .. text_opacity) local settings = obs.obs_data_create() obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -635,7 +634,6 @@ function apply_source_opacity() obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) else -- try to just change visibility in the scene - print("No Filter") local sceneSource = obs.obs_frontend_get_current_scene() local sceneObj = obs.obs_scene_from_source(sceneSource) local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) @@ -1954,7 +1952,6 @@ end function isValid(source) if source ~= nil then local flags = obs.obs_source_get_output_flags(source) - print(obs.obs_source_get_name(source) .. " - " .. flags) local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) if bit.band(flags, targetFlag) == targetFlag then return true @@ -2913,5 +2910,5 @@ obs.obs_register_source(source_def) description = [[ -

OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian

+
OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian
]] From d5bbdf935275d3ab5fe30dbcad72c1b07d2bd248 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Sun, 17 Oct 2021 14:05:28 -0600 Subject: [PATCH 057/105] addressed Opacity Presets and Hot-Keys not showing. Honoring Preset Opacity Levels, Fixed assigned Hot-Keys not showing in Properties UI --- LyricsTrial.lua | 2946 +++++++++++++++++++++++++++++++++++++++++++++++ lyrics+.lua | 134 ++- 2 files changed, 3029 insertions(+), 51 deletions(-) create mode 100644 LyricsTrial.lua diff --git a/LyricsTrial.lua b/LyricsTrial.lua new file mode 100644 index 0000000..0364129 --- /dev/null +++ b/LyricsTrial.lua @@ -0,0 +1,2946 @@ +--- Copyright 2020 amirchev/wzaggle + +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at + +-- http://www.apache.org/licenses/LICENSE-2.0 + +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +obs = obslua +bit = require("bit") + +-- source definitions +source_data = {} +source_def = {} +source_def.id = "Prepare_Lyrics" +source_def.type = OBS_SOURCE_TYPE_INPUT +source_def.output_flags = bit.bor(obs.OBS_SOURCE_CUSTOM_DRAW) + +-- text sources +source_name = "" +alternate_source_name = "" +static_source_name = "" +static_text = "" +title_source_name = "" + +-- settings +windows_os = false +first_open = true + +display_lines = 0 +ensure_lines = true + +-- lyrics/alternate lyrics by page +lyrics = {} +alternate = {} + +-- verse indicies if marked +verses = {} + +page_index = 0 -- current page of lyrics being displayed +prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected + +song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) +prepared_songs = {} -- holds pre-prepared list of songs to use +extra_sources = {} -- holder for extra sources settings +max_opacity = {} -- record maximum opacity settings for sources + +link_text = false -- true if Title and Static should fade with text only during hide/show +link_extras = false -- extras fade with text always when true, only during hide/show when false +all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide +source_song_title = "" -- The song title from a source loaded song +using_source = false -- true when a lyric load song is being used instead of a pre-prepared song +source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) + +load_scene = "" -- name of scene loading a lyric with a source +last_prepared_song = "" -- name of the last prepared song (prevents duplicate loading of already loaded song) + +-- hotkeys +hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_p_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_c_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_n_p_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID +hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID + +hotkey_n_key = "" +hotkey_p_key = "" +hotkey_c_key = "" +hotkey_n_p_key = "" +hotkey_p_p_key = "" +hotkey_home_key = "" +hotkey_reset_key = "" + +-- script placeholders +script_sets = nil +script_props = nil +source_sets = nil +source_props = nil +hotkey_props = nil + +--monitor variables +mon_song = "" +mon_lyric = "" +mon_verse = 0 +mon_nextlyric = "" +mon_alt = "" +mon_nextalt = "" +mon_nextsong = "" +meta_tags = "" +source_meta_tags = "" + +-- text status & fade +TEXT_VISIBLE = 0 -- text is visible +TEXT_HIDDEN = 1 -- text is hidden +TEXT_SHOWING = 3 -- going from hidden -> visible +TEXT_HIDING = 4 -- going from visible -> hidden +TEXT_TRANSITION_OUT = 5 -- fade out transition to next lyric +TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change +TEXT_HIDE = 7 -- turn off the text and ignore fade if selected +TEXT_SHOW = 8 -- turn on the text and ignore fade if selected + +text_status = TEXT_VISIBLE +text_opacity = 100 +text_fade_speed = 1 +text_fade_enabled = false +load_source = nil +expandcollapse = true +showhelp = false + +transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) +transition_completed = false + +source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now + +editVisSet = false + +-- simple debugging/print mechanism +--DEBUG = true -- on switch for entire debugging mechanism +--DEBUG_METHODS = true -- print method names +--DEBUG_INNER = true -- print inner method breakpoints +--DEBUG_CUSTOM = true -- print custom debugging messages +--DEBUG_BOOL = true -- print message with bool state true/false + +-------- +---------------- +------------------------ CALLBACKS +---------------- +-------- + +function next_lyric(pressed) + if not pressed then + return + end + dbg_method("next_lyric") + -- check if transition enabled + if transition_enabled and not transition_completed then + obs.obs_frontend_preview_program_trigger_transition() + transition_completed = true + return + end + dbg_inner("next page") + if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing + if page_index < #lyrics then + page_index = page_index + 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + next_prepared(true) + end + end +end + +function prev_lyric(pressed) + if not pressed then + return + end + dbg_method("prev_lyric") + if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing + if page_index > 1 then + page_index = page_index - 1 + dbg_inner("page_index: " .. page_index) + transition_lyric_text(false) + else + prev_prepared(true) + end + end +end + +function prev_prepared(pressed) + if not pressed then + return + end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + prepare_selected(prepared_songs[prepared_index]) + return + end + if prepared_index > 1 then + using_source = false + prepare_selected(prepared_songs[prepared_index - 1]) + return + end + if not source_active or using_source then + using_source = false + prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared + else + using_source = true + prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source + load_source_song(load_source, false) + end +end + +function next_prepared(pressed) + if not pressed then + return + end + if #prepared_songs == 0 then + return + end + if using_source then + using_source = false + dbg_custom("do current prepared") + prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song + return + end + if prepared_index < #prepared_songs then + using_source = false + dbg_custom("do next prepared") + prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared + return + end + if not source_active or using_source then + using_source = false + dbg_custom("do first prepared") + prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available + else + using_source = true + dbg_custom("do source prepared") + prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source + load_source_song(load_source, false) + end +end + +function toggle_lyrics_visibility(pressed) + dbg_method("toggle_lyrics_visibility") + if not pressed then + return + end + if link_text then + all_sources_fade = true + end + if text_status ~= TEXT_HIDDEN then + read_source_opacity() -- record maximum opacities for TEXT_VISIBLE condition. + dbg_inner("hiding") + set_text_visibility(TEXT_HIDDEN) + else + dbg_inner("showing") + set_text_visibility(TEXT_VISIBLE) + end +end + +function get_load_lyric_song() + local scene = obs.obs_frontend_get_current_scene() + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + local song = nil + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id + if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + song = obs.obs_data_get_string(settings, "song") -- Get index for this source (set earlier) + obs.obs_data_release(settings) -- release memory + end + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + return song +end + +function home_prepared(pressed) + if not pressed then + return false + end + dbg_method("home_prepared") + using_source = false + page_index = 0 + + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + end + obs.obs_properties_apply_settings(props, script_sets) + prepared_index = 1 + prepare_selected(prepared_songs[prepared_index]) + return true +end + +function home_song(pressed) + if not pressed then + return false + end + dbg_method("home_song") + page_index = 1 + transition_lyric_text(false) + return true +end + +function get_current_scene_name() + dbg_method("get_current_scene_name") + local scene = obs.obs_frontend_get_current_scene() + local current_scene = obs.obs_source_get_name(scene) + obs.obs_source_release(scene) + if current_scene ~= nil then + return current_scene + else + return "-" + end +end + +function next_button_clicked(props, p) + next_lyric(true) + return true +end + +function prev_button_clicked(props, p) + prev_lyric(true) + return true +end + +function toggle_button_clicked(props, p) + toggle_lyrics_visibility(true) + return true +end + +function home_button_clicked(props, p) + home_song(true) + return true +end + +function reset_button_clicked(props, p) + home_prepared(true) + return true +end +function prev_prepared_clicked(props, p) + prev_prepared(true) + return true +end + +function next_prepared_clicked(props, p) + next_prepared(true) + return true +end + +function save_song_clicked(props, p) + local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") + local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") + -- if this is a new song, add it to the directory + if save_song(name, text) then + local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") + obs.obs_property_list_add_string(prop_dir_list, name, name) + obs.obs_data_set_string(script_sets, "prop_directory_list", name) + obs.obs_properties_apply_settings(props, script_sets) + elseif prepared_songs[prepared_index] == name then + -- if this song is being displayed, then prepare it anew + prepare_song_by_name(name) + transition_lyric_text(false) + end + return true +end + +function delete_song_clicked(props, p) + dbg_method("delete_song_clicked") + -- call delete song function + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + delete_song(name) + -- update + local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") + for i = 0, obs.obs_property_list_item_count(prop_dir_list) do + if obs.obs_property_list_item_string(prop_dir_list, i) == name then + obs.obs_property_list_item_remove(prop_dir_list, i) + if i > 1 then + i = i - 1 + end + if #song_directory > 0 then + obs.obs_data_set_string(script_sets, "prop_directory_list", song_directory[i]) + else + obs.obs_data_set_string(script_sets, "prop_directory_list", "") + obs.obs_data_set_string(script_sets, "prop_edit_song_title", "") + obs.obs_data_set_string(script_sets, "prop_edit_song_text", "") + end + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + if get_index_in_list(prepared_songs, name) ~= nil then + if obs.obs_property_list_item_string(prop_prep_list, i) == name then + obs.obs_property_list_item_remove(prop_prep_list, i) + if i > 1 then + i = i - 1 + end + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[i]) + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + end + end + end + obs.obs_properties_apply_settings(props, script_sets) + return true + end + end + return true +end + +-- prepare song button clicked +function prepare_song_clicked(props, p) + dbg_method("prepare_song_clicked") + if #prepared_songs == 0 then + set_text_visibility(TEXT_HIDDEN) + end + prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) + + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) + + obs.obs_properties_apply_settings(props, script_sets) + save_prepared() + return true +end + +function refresh_button_clicked(props, p) + local source_prop = obs.obs_properties_get(props, "prop_source_list") + local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") + local static_source_prop = obs.obs_properties_get(props, "prop_static_list") + local title_source_prop = obs.obs_properties_get(props, "prop_title_list") + local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") + + obs.obs_property_list_clear(source_prop) -- clear current properties list + obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list + obs.obs_property_list_clear(static_source_prop) -- clear current properties list + obs.obs_property_list_clear(title_source_prop) -- clear current properties list + obs.obs_property_list_clear(extra_source_prop) -- clear extra sources list + + obs.obs_property_list_add_string(extra_source_prop, "", "") + + local sources = obs.obs_enum_sources() + if sources ~= nil then + local n = {} + for _, source in ipairs(sources) do + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list + end + source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + n[#n + 1] = name + end + end + table.sort(n) + obs.obs_property_list_add_string(source_prop, "", "") + obs.obs_property_list_add_string(title_source_prop, "", "") + obs.obs_property_list_add_string(alternate_source_prop, "", "") + obs.obs_property_list_add_string(static_source_prop, "", "") + for _, name in ipairs(n) do + obs.obs_property_list_add_string(source_prop, name, name) + obs.obs_property_list_add_string(title_source_prop, name, name) + obs.obs_property_list_add_string(alternate_source_prop, name, name) + obs.obs_property_list_add_string(static_source_prop, name, name) + end + end + obs.source_list_release(sources) + refresh_directory() + + return true +end + +function refresh_directory_button_clicked(props, p) + dbg_method("refresh directory") + refresh_directory() + return true +end + +function refresh_directory() + local prop_dir_list = obs.obs_properties_get(script_props, "prop_directory_list") + local source_prop = obs.obs_properties_get(props, "prop_source_list") + source_filter = false + load_source_song_directory(true) + table.sort(song_directory) + obs.obs_property_list_clear(prop_dir_list) -- clear directories + for _, name in ipairs(song_directory) do + dbg_inner(name) + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_properties_apply_settings(script_props, script_sets) +end + +-- Called with ANY change to the prepared song list +function prepare_selection_made(props, prop, settings) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prep_grp"), + " Prepared Songs/Text (" .. #prepared_songs .. ")" + ) + dbg_method("prepare_selection_made") + local name = obs.obs_data_get_string(settings, "prop_prepared_list") + using_source = false + prepare_selected(name) + return true +end + +-- removes prepared songs +function clear_prepared_clicked(props, p) + dbg_method("clear_prepared_clicked") + prepared_songs = {} -- required for monitor page + page_index = 0 -- required for monitor page + prepared_index = 0 -- required for monitor page + update_source_text() -- required for monitor page + -- clear the list + local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prep_prop) + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + obs.obs_properties_apply_settings(props, script_sets) + save_prepared() + return true +end + +function prepare_selected(name) + dbg_method("prepare_selected") + -- try to prepare song + if prepare_song_by_name(name) then + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + else + source_song_title = name + all_sources_fade = true + end + + transition_lyric_text(using_source) + else + -- hide everything if unable to prepare song + -- TODO: clear lyrics entirely after text is hidden + set_text_visibility(TEXT_HIDDEN) + end + + --update_source_text() + return true +end + +-- called when selection is made from directory list +function preview_selection_made(props, prop, settings) + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + + if get_index_in_list(song_directory, name) == nil then + return false + end -- do nothing if invalid name + + obs.obs_data_set_string(settings, "prop_edit_song_title", name) + local song_lines = get_song_text(name) + local combined_text = "" + for i, line in ipairs(song_lines) do + if (i < #song_lines) then + combined_text = combined_text .. line .. "\n" + else + combined_text = combined_text .. line + end + end + obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) + return true +end + +function open_song_clicked(props, p) + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + if windows_os then + os.execute('explorer "' .. path .. '"') + else + os.execute('xdg-open "' .. path .. '"') + end + return true +end + +function open_button_clicked(props, p) + local path = get_songs_folder_path() + if windows_os then + os.execute('explorer "' .. path .. '"') + else + os.execute('xdg-open "' .. path .. '"') + end +end + +-------- +---------------- +------------------------ PROGRAM FUNCTIONS +---------------- +-------- +function setSourceOpacity(sourceName) + if sourceName ~= nil and sourceName ~= "" then + local settings = obs.obs_data_create() + adj_text_opacity = text_opacity /100 + obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity + obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity + local source = obs.obs_get_source_by_name(sourceName) + if source ~= nil then + obs.obs_source_update(source, settings) + end + obs.obs_source_release(source) + obs.obs_data_release(settings) + end +end + + +function apply_source_opacity() + setSourceOpacity(source_name) + setSourceOpacity(alternate_source_name) + if all_sources_fade then + setSourceOpacity(title_source_name) + setSourceOpacity(static_source_name) + end + if link_extras or all_sources_fade then + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count - 1 do + local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local extra_source = obs.obs_get_source_by_name(sourceName) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + setSourceOpacity(sourceName) + else -- check for filter named "Color Correction" + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_data_create() + obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) + obs.obs_source_update(color_filter, filter_settings) + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + else -- try to just change visibility in the scene + local sceneSource = obs.obs_frontend_get_current_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) + obs.obs_source_release(scene) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end + end +end + +function getSourceOpacity(sourceName) + if sourceName ~= nil and sourceName ~= "" then + local source = obs.obs_get_source_by_name(sourceName) + local settings = obs.obs_source_get_settings(source) + max_opacity[sourceName]={} + max_opacity[sourceName]["opacity"] = obs.obs_data_get_int(settings, "opacity") -- text opacity + max_opacity[sourceName]["outline"] = obs.obs_data_get_int(settings, "outline_opacity") -- outline opacity + max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity + max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity + obs.obs_source_release(source) + obs.obs_data_release(settings) + end +end + + +function read_source_opacity() + getSourceOpacity(source_name) + getSourceOpacity(alternate_source_name) + getSourceOpacity(title_source_name) + getSourceOpacity(static_source_name) + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count - 1 do + local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local extra_source = obs.obs_get_source_by_name(sourceName) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + getSourceOpacity(sourceName) + else -- check for filter named "Color Correction" + + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_source_get_settings(color_filter) + max_opacity[sourceName]={} + max_opacity[sourceName]["CC-opacity"] = obs.obs_data_get_double(filter_settings, "opacity") + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end +end + +function set_text_visibility(end_status) + dbg_method("set_text_visibility") + -- if already at desired visibility, then exit + if text_status == end_status then + return + end + if end_status == TEXT_HIDE then + text_opacity = 0 + text_status = end_status + apply_source_opacity() + return + elseif end_status == TEXT_SHOW then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + apply_source_opacity() + return + end + if text_fade_enabled then + -- if fade enabled, begin fade in or out + if end_status == TEXT_HIDDEN then + text_status = TEXT_HIDING + elseif end_status == TEXT_VISIBLE then + text_status = TEXT_SHOWING + end + --all_sources_fade = true + start_fade_timer() + else -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + end + apply_source_opacity() + --update_source_text() + all_sources_fade = false + return + end +end + +-- transition to the next lyrics, use fade if enabled +-- if lyrics are hidden, force_show set to true will make them visible +function transition_lyric_text(force_show) + dbg_method("transition_lyric_text") + dbg_bool("force show", force_show) + -- update the lyrics display immediately on 2 conditions + -- a) the text is hidden or hiding, and we will not force it to show + -- b) text fade is not enabled + -- otherwise, start text transition out and update the lyrics once + -- fade out transition is complete + if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then + update_source_text() + -- if text is done hiding, we can cancel the all_sources_fade + if text_status == TEXT_HIDDEN then + all_sources_fade = false + end + dbg_inner("hidden") + elseif not text_fade_enabled then + dbg_custom("Instant On") + -- if text fade is not enabled, then we can cancel the all_sources_fade + all_sources_fade = false + set_text_visibility(TEXT_VISIBLE) -- does update_source_text() + update_source_text() + dbg_inner("no text fade") + else -- initiate fade out/in + dbg_custom("Transition Timer") + text_status = TEXT_TRANSITION_OUT + start_fade_timer() + end + dbg_bool("using_source", using_source) +end + +-- updates the selected lyrics +function update_source_text() + dbg_method("update_source_text") + dbg_custom("Page Index: " .. page_index) + local text = "" + local alttext = "" + local next_lyric = "" + local next_alternate = "" + local static = static_text + local mstatic = static -- save static for use with monitor + local title = "" + + if alt_title ~= "" then + title = alt_title + else + if not using_source then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Update from prepared: " .. prepared_index) + title = prepared_songs[prepared_index] + end + else + dbg_custom("Updatefrom source: " .. source_song_title) + title = source_song_title + end + end + + local source = obs.obs_get_source_by_name(source_name) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) + + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then + if #lyrics > 0 then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] + end + end + if #alternate > 0 then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] + end + end + + if link_text then + if string.len(text) == 0 and string.len(alttext) == 0 then + --static = "" + --title = "" + end + end + end + -- update source texts + if source ~= nil then + dbg_inner("Title Load") + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + next_lyric = lyrics[page_index + 1] + if (next_lyric == nil) then + next_lyric = "" + end + end + if alt_source ~= nil then + local settings = obs.obs_data_create() -- setup TEXT settings with opacity values + obs.obs_data_set_string(settings, "text", alttext) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" + end + end + if stat_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) + end + if title_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) + end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) + obs.obs_source_release(title_source) + + local next_prepared = "" + if using_source then + next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song + elseif prepared_index < #prepared_songs then + next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song + else + if source_active then + next_prepared = source_song_title -- plan to go back to source loaded song + else + next_prepared = prepared_songs[1] -- plan to loop around to first prepared song + end + end + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i] + 1 then + mon_verse = i + end + end -- v = current verse number for this page + end + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared + + update_monitor() +end + +function start_fade_timer() + dbgsp("started fade timer") + obs.timer_add(fade_callback, 50) +end + +function fade_callback() + -- if not in a transitory state, exit callback + if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then + obs.remove_current_callback() + all_sources_fade = false + end + -- the amount we want to change opacity by + local opacity_delta = 1 + text_fade_speed + -- change opacity in the direction of transitory state + if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then + local new_opacity = text_opacity - opacity_delta + if new_opacity > 0 then + text_opacity = new_opacity + else + -- completed fade out, determine next move + text_opacity = 0 + if text_status == TEXT_TRANSITION_OUT then + -- update to new lyric between fades + update_source_text() + -- begin transition back in + text_status = TEXT_TRANSITION_IN + else + text_status = TEXT_HIDDEN + end + end + elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then + local new_opacity = text_opacity + opacity_delta + if new_opacity < 100 then + text_opacity = new_opacity + else + -- completed fade in + text_opacity = 100 + text_status = TEXT_VISIBLE + end + end + -- apply the new opacity + apply_source_opacity() +end + +function prepare_song_by_index(index) + dbg_method("prepare_song_by_index") + if index <= #prepared_songs then + prepare_song_by_name(prepared_songs[index]) + end +end + +-- prepares lyrics of the song +function prepare_song_by_name(name) + dbg_method("prepare_song_by_name") + if name == nil then + return false + end + last_prepared_song = name + -- if using transition on lyric change, first transition + -- would be reset with new song prepared + transition_completed = false + -- load song lines + local song_lines = get_song_text(name) + if song_lines == nil then + return false + end + local cur_line = 1 + local cur_aline = 1 + local recordRefrain = false + local playRefrain = false + local use_alternate = false + local use_static = false + local showText = true + local commentBlock = false + local singleAlternate = false + local refrain = {} + local arefrain = {} + lyrics = {} + verses = {} + alternate = {} + static_text = "" + alt_title = "" + local adjusted_display_lines = display_lines + local refrain_display_lines = display_lines + local alternate_display_lines = display_lines + local displaySize = display_lines + for _, line in ipairs(song_lines) do + local new_lines = 1 + local single_line = false + local comment_index = line:find("//%[") -- Look for comment block Set + if comment_index ~= nil then + commentBlock = true + line = line:sub(comment_index + 3) + end + comment_index = line:find("//]") -- Look for comment block Clear + if comment_index ~= nil then + commentBlock = false + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + if not commentBlock then + local comment_index = line:find("%s*//") + if comment_index ~= nil then + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + local alternate_index = line:find("#A%[") + if alternate_index ~= nil then + use_alternate = true + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + alternate_index = line:find("#A]") + if alternate_index ~= nil then + use_alternate = false + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + local static_index = line:find("#S%[") + if static_index ~= nil then + use_static = true + line = line:sub(1, static_index - 1) + new_lines = 0 + end + static_index = line:find("#S]") + if static_index ~= nil then + use_static = false + line = line:sub(1, static_index - 1) + new_lines = 0 + end + + local newcount_index = line:find("#L:") + if newcount_index ~= nil then + local iS, iE = line:find("%d+", newcount_index + 3) + local newLines = tonumber(line:sub(iS, iE)) + if use_alternate then + alternate_display_lines = newLines + elseif recordRefrain then + refrain_display_lines = newLines + else + adjusted_display_lines = newLines + refrain_display_lines = newLines + alternate_display_lines = newLines + end + line = line:sub(1, newcount_index - 1) + new_lines = 0 -- ignore line + end + local static_index = line:find("#S:") + if static_index ~= nil then + line = line:sub(static_index + 3) + static_text = line + new_lines = 0 + end + local title_index = line:find("#T:") + if title_index ~= nil then + local title_indexEnd = line:find("%s+", title_index + 1) + line = line:sub(title_indexEnd + 1) + alt_title = line + new_lines = 0 + end + local alt_index = line:find("#A:") + if alt_index ~= nil then + local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) + new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) + local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) + line = line:sub(alt_indexEnd + 1) + singleAlternate = true + end + if line:find("###") ~= nil then -- Look for single line + line = line:gsub("%s*###%s*", "") + single_line = true + end + local newcount_index = line:find("#D:") + if newcount_index ~= nil then + local newcount_indexStart, newcount_indexEnd = line:find("%d+", newcount_index + 3) + new_lines = tonumber(line:sub(newcount_indexStart, newcount_indexEnd)) + _, newcount_indexEnd = line:find("%s+", newcount_indexEnd + 1) + line = line:sub(newcount_indexEnd + 1) + end + local refrain_index = line:find("#R%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = false + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#R]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + + refrain_index = line:find("##R") + if refrain_index == nil then + refrain_index = line:find("##r") + end + if refrain_index ~= nil then + playRefrain = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + else + playRefrain = false + end + newcount_index = line:find("#P:") + if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) + line = line:sub(1, newcount_index - 1) + end + newcount_index = line:find("#B:") + if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) + line = line:sub(1, newcount_index - 1) + end + local phantom_index = line:find("##P") + if phantom_index ~= nil then + line = line:sub(1, phantom_index - 1) + end + phantom_index = line:find("##B") + if phantom_index ~= nil then + line = line:gsub("%s*##B%s*", "") .. "\n" + end + local verse_index = line:find("##V") + if verse_index ~= nil then + line = line:sub(1, verse_index - 1) + new_lines = 0 + verses[#verses + 1] = #lyrics + dbg_inner("Verse: " .. #lyrics) + end + if line ~= nil then + if use_static then + if static_text == "" then + static_text = line + else + static_text = static_text .. "\n" .. line + end + else + if use_alternate or singleAlternate then + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = alternate_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + arefrain[#refrain + 1] = line + else + arefrain[#refrain] = arefrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_aline == 1) then + alternate[#alternate + 1] = line + else + alternate[#alternate] = alternate[#alternate] .. "\n" .. line + end + end + cur_aline = cur_aline + 1 + if single_line or singleAlternate or cur_aline > displaySize then + if ensure_lines then + for i = cur_aline, displaySize, 1 do + cur_aline = i + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + if recordRefrain then + arefrain[#refrain] = arefrain[#refrain] .. "\n" + end + end + end + cur_aline = 1 + end + new_lines = new_lines - 1 + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(arefrain) do + alternate[#alternate + 1] = refrain_line + end + end + singleAlternate = false + else + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = adjusted_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + refrain[#refrain + 1] = line + else + refrain[#refrain] = refrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_line == 1) then + lyrics[#lyrics + 1] = line + else + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line + end + end + cur_line = cur_line + 1 + if single_line or cur_line > displaySize then + if ensure_lines then + for i = cur_line, displaySize, 1 do + cur_line = i + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end + end + cur_line = 1 + end + new_lines = new_lines - 1 + end + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(refrain) do + lyrics[#lyrics + 1] = refrain_line + end + end + end + end + end + end + if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then + for i = cur_line, displaySize, 1 do + cur_line = i + if use_alternate then + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + else + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end + end + lyrics[#lyrics + 1] = "" + -- pause_timer = false + return true +end + +-- finds the index of a song in the directory +-- if item is not in list, then return nil +function get_index_in_list(list, q_item) + for index, item in ipairs(list) do + if item == q_item then + return index + end + end + return nil +end + +-------- +---------------- +------------------------ FILE FUNCTIONS +---------------- +-------- + +-- delete previewed song +function delete_song(name) + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + os.remove(path) + table.remove(song_directory, get_index_in_list(song_directory, name)) + source_filter = false + load_source_song_directory(false) +end + +-- loads the song directory +function load_source_song_directory(use_filter) + dbg_method("load_source_song_directory") + local keytext = meta_tags + if source_filter then + keytext = source_meta_tags + end + dbg_inner(keytext) + local keys = ParseCSVLine(keytext) + + song_directory = {} + local filenames = {} + local tags = {} + local dir = obs.os_opendir(get_songs_folder_path()) + -- get_songs_folder_path()) + local entry + local songExt + local songTitle + local goodEntry = true + + repeat + entry = obs.os_readdir(dir) + if + entry and not entry.directory and + (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") + then + songExt = obs.os_get_path_extension(entry.d_name) + songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) + tags = readTags(songTitle) + goodEntry = true + if use_filter and #keys > 0 then -- need to check files + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + goodEntry = false -- start assuming file will not be shown + if #tags == 0 then -- check no tagged option + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + else -- have keys and tags so compare them + for k = 1, #keys do + for t = 1, #tags do + if tags[t] == keys[k] then + goodEntry = true -- found match so show file + break + end + end + if goodEntry then -- stop outer key loop on match + break + end + end + end + end + if goodEntry then -- add file if valid match + if songExt == ".enc" then + song_directory[#song_directory + 1] = dec(songTitle) + else + song_directory[#song_directory + 1] = songTitle + end + end + end + until not entry + obs.os_closedir(dir) +end +-- +-- reads the first line of each lyric file, looks for the //meta comment and returns any CSV tags that exist +-- +function readTags(name) + local meta = "" + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + meta = line + break + end + file:close() + end + local meta_index = meta:find("//meta ") -- Look for meta block Set + if meta_index ~= nil then + meta = meta:sub(meta_index + 7) + return ParseCSVLine(meta) + end + return {} +end + +function ParseCSVLine(line) + local res = {} + local pos = 1 + sep = "," + while true do + local c = string.sub(line, pos, pos) + if (c == "") then + break + end + if (c == '"') then + local txt = "" + repeat + local startp, endp = string.find(line, '^%b""', pos) + txt = txt .. string.sub(line, startp + 1, endp - 1) + pos = endp + 1 + c = string.sub(line, pos, pos) + if (c == '"') then + txt = txt .. '"' + end + until (c ~= '"') + txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. txt) + table.insert(res, txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp, endp = string.find(line, sep, pos) + if (startp) then + local t = string.sub(line, pos, startp - 1) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + pos = endp + 1 + else + local t = string.sub(line, pos) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + break + end + end + end + return res +end + +local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet + +-- encode title/filename if it contains invalid filename characters +-- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup +-- +function enc(data) + return ((data:gsub( + ".", + function(x) + local r, b = "", x:byte() + for i = 8, 1, -1 do + r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ) .. "0000"):gsub( + "%d%d%d?%d?%d?%d?", + function(x) + if (#x < 6) then + return "" + end + local c = 0 + for i = 1, 6 do + c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0) + end + return b:sub(c + 1, c + 1) + end + ) .. ({"", "==", "="})[#data % 3 + 1]) +end +-- +-- decode an encoded title/filename +-- +function dec(data) + data = string.gsub(data, "[^" .. b .. "=]", "") + return (data:gsub( + ".", + function(x) + if (x == "=") then + return "" + end + local r, f = "", (b:find(x) - 1) + for i = 6, 1, -1 do + r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ):gsub( + "%d%d%d?%d?%d?%d?%d?%d?", + function(x) + if (#x ~= 8) then + return "" + end + local c = 0 + for i = 1, 8 do + c = c + (x:sub(i, i) == "1" and 2 ^ (8 - i) or 0) + end + return string.char(c) + end + )) +end + +function testValid(filename) + if string.find(filename, "[\128-\255]") ~= nil then + return false + end + if string.find(filename, '[\\\\/:*?"<>|]') ~= nil then + return false + end + return true +end + +-- saves previewed song, return true if new song +function save_song(name, text) + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "w") + if file ~= nil then + for line in text:gmatch("([^\n]+)") do + local trimmed = line:match("%s*(%S-.*%S+)%s*") + if trimmed ~= nil then + file:write(trimmed, "\n") + end + end + file:close() + if get_index_in_list(song_directory, name) == nil then + song_directory[#song_directory + 1] = name + return true + end + end + return false +end + +-- saves preprepared songs +function save_prepared() + dbg_method("save_prepared") + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") + for i, name in ipairs(prepared_songs) do + -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs + file:write(name, "\n") + -- end + end + file:close() + return true +end + +function update_monitor() + dbg_method("update_monitor") + local tableback = "black" + local text = "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = + text .. + "
" + text = + text .. + "
" + if using_source then + text = text .. "From Source: " .. load_scene .. "
" + else + text = text .. "Prepared Song: " .. prepared_index + text = + text .. + " of " .. #prepared_songs .. "
" + end + text = + text .. + "
Lyric Page: " .. + page_index + text = text .. " of " .. #lyrics .. "
" + if #verses ~= nil and mon_verse > 0 then + text = + text .. + "
Verse: " .. mon_verse + text = text .. " of " .. #verses .. "
" + end + text = text .. "
" + if not anythingActive() then + tableback = "#440000" + end + local visbgTitle = tableback + local visbgText = tableback + if text_status == TEXT_HIDDEN or text_status == TEXT_HIDING then + visbgText = "maroon" + if link_text then + visbgTitle = "maroon" + end + end + + text = + text .. + "
" + if mon_song ~= "" and mon_song ~= nil then + text = + text .. + "" + text = + text .. + "" + end + if mon_lyric ~= "" and mon_lyric ~= nil then + text = + text .. + "" + text = + text .. "" + end + if mon_nextlyric ~= "" and mon_nextlyric ~= nil then + text = + text .. + "" + text = text .. "" + end + if mon_alt ~= "" and mon_alt ~= nil then + text = + text .. + "" + text = + text .. + "" + end + if mon_nextalt ~= "" and mon_nextalt ~= nil then + text = + text .. + "" + text = text .. "" + end + if mon_nextsong ~= "" and mon_nextsong ~= nil then + text = + text .. + "" + text = text .. "" + end + text = text .. "
Song
Title
" .. mon_song .. "
Current
Page
• " .. mon_lyric .. "
Next
Page
• " .. mon_nextlyric .. "
Alt
Lyric
• " .. mon_alt .. "
Next
Alt
• " .. mon_nextalt .. "
Next
Song:
" .. mon_nextsong .. "
" + local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") + dbg_inner("write monitor file") + file:write(text) + file:close() + return true +end + +-- returns path of the given song name +function get_song_file_path(name, suffix) + if name == nil then + return nil + end + return get_songs_folder_path() .. "\\" .. name .. suffix +end + +-- returns path of the lyrics songs folder +function get_songs_folder_path() + local sep = package.config:sub(1, 1) + local path = "" + if windows_os then + path = os.getenv("USERPROFILE") + else + path = os.getenv("HOME") + end + return path .. sep .. ".config" .. sep .. ".obs_lyrics" +end + +-- gets the text of a song +function get_song_text(name) + local song_lines = {} + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + song_lines[#song_lines + 1] = line + end + file:close() + else + return nil + end + + return song_lines +end + +-- ------ +---------------- +------------------------ OBS DEFAULT FUNCTIONS +-- -------------- +-------- + +-- A function named script_properties defines the properties that the user +-- can change for the entire script module itself + +local help = + "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. + " Markup      Syntax         Markup      Syntax \n" .. + "============ ==========   ============ ==========\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line" + +function script_properties() + dbg_method("script_properties") + editVisSet = false + script_props = obs.obs_properties_create() + obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) + ----------- + obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲", change_info_visible) + local gp = obs.obs_properties_create() + obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) + obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) + obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) + obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) + obs.obs_properties_add_group( + script_props, + "info_grp", + "Song Title (filename) and Lyrics Information", + obs.OBS_GROUP_NORMAL, + gp + ) + ------------ + obs.obs_properties_add_button( + script_props, + "prepared_showing", + "▲- HIDE PREPARED SONGS -▲", + change_prepared_visible + ) + gp = obs.obs_properties_create() + local prop_dir_list = + obs.obs_properties_add_list( + gp, + "prop_directory_list", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) + local gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) + local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + local prepare_prop = + obs.obs_properties_add_list( + gps, + "prop_prepared_list", + "Prepared Songs", + obs.OBS_COMBO_TYPE_EDITABLE, + obs.OBS_COMBO_FORMAT_STRING + ) + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prepare_prop, name, name) + end + obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) + local eps = obs.obs_properties_create() + local edit_prop = + obs.obs_properties_add_editable_list( + eps, + "prep_list", + "Prepared Songs/Text", + obs.OBS_EDITABLE_LIST_TYPE_STRINGS, + nil, + nil + ) + obs.obs_property_set_modified_callback(edit_prop, setEditVis) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes", save_edits_clicked) + local edit_group_prop = + obs.obs_properties_add_group( + gps, + "edit_grp", + "Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", + obs.OBS_GROUP_NORMAL, + eps + ) + obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) + ------------------ + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) + hotkey_props = obs.obs_properties_create() + local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked) + obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)",obs.OBS_GROUP_NORMAL,hotkey_props) + name_hotkeys() + ------ + obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) + gp = obs.obs_properties_create() + local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "Lines to Display", 1, 50, 1) + obs.obs_property_set_long_description( + lines_prop, + "Sets default lines per page of lyric, overwritten by Markup: #L:n" + ) + local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") + obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") + local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") + obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") + local transition_prop = + obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_property_set_modified_callback(transition_prop, change_transition_property) + obs.obs_property_set_long_description( + transition_prop, + "Use with Studio Mode, duplicate sources, and OBS source transitions" + ) + local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) + obs.obs_property_set_modified_callback(fade_prop, change_fade_property) + obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) + ------------- + obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) + gp = obs.obs_properties_create() + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) + local source_prop = + obs.obs_properties_add_list( + gp, + "prop_source_list", + "Text Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + local title_source_prop = + obs.obs_properties_add_list( + gp, + "prop_title_list", + "Title Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + local alternate_source_prop = + obs.obs_properties_add_list( + gp, + "prop_alternate_list", + "Alternate Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + local static_source_prop = + obs.obs_properties_add_list( + gp, + "prop_static_list", + "Static Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + xgp = obs.obs_properties_create() + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") + local extra_linked_prop = + obs.obs_properties_add_list( + xgp, + "extra_linked_list", + "Linked Sources ", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + -- initialize previously loaded extra properties from table + for _, sourceName in ipairs(extra_sources) do + obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) + end + local extra_source_prop = + obs.obs_properties_add_list( + xgp, + "extra_source_list", + " Select Source:", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) + local clearcall_prop = + obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) + local extra_group_prop = + obs.obs_properties_add_group(gp, "xtr_grp", "Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL, xgp) + obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) + local count = obs.obs_property_list_item_count(extra_linked_prop) + if count > 0 then + obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") + else + obs.obs_property_set_visible(extra_group_prop, false) + end + + local sources = obs.obs_enum_sources() + obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") + if sources ~= nil then + local n = {} + for _, source in ipairs(sources) do + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list + end + source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + n[#n + 1] = name + end + end + table.sort(n) + obs.obs_property_list_add_string(source_prop, "", "") + obs.obs_property_list_add_string(title_source_prop, "", "") + obs.obs_property_list_add_string(alternate_source_prop, "", "") + obs.obs_property_list_add_string(static_source_prop, "", "") + for _, name in ipairs(n) do + obs.obs_property_list_add_string(source_prop, name, name) + obs.obs_property_list_add_string(title_source_prop, name, name) + obs.obs_property_list_add_string(alternate_source_prop, name, name) + obs.obs_property_list_add_string(static_source_prop, name, name) + end + end + obs.source_list_release(sources) + + ----------------- + obs.obs_property_set_enabled(hktitletext, false) + obs.obs_property_set_visible(edit_group_prop, false) + obs.obs_property_set_visible(meta_group_prop, false) + return script_props +end + +-- script_update is called when settings are changed +function script_update(settings) + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") + text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") + display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + source_name = obs.obs_data_get_string(settings, "prop_source_list") + alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") + static_source_name = obs.obs_data_get_string(settings, "prop_static_list") + title_source_name = obs.obs_data_get_string(settings, "prop_title_list") + ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") + link_text = obs.obs_data_get_bool(settings, "do_link_text") + link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") + read_source_opacity() -- update opacities if sources might have changed +end + +-- A function named script_defaults will be called to set the default settings +function script_defaults(settings) + dbg_method("script_defaults") + obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) + obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + if windows_os then + os.execute('mkdir "' .. get_songs_folder_path() .. '"') + else + os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') + end +end + +--verify source has an opacity setting +function isValid(source) + if source ~= nil then + local flags = obs.obs_source_get_output_flags(source) + local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) + if bit.band(flags, targetFlag) == targetFlag then + return true + end + end + return false +end + +-- adds an extra linked source. +-- Source must be text source, or have 'Color Correction' Filter applied +function link_source_selected(props, prop, settings) + dbg_method("link_source_selected") + local extra_source = obs.obs_data_get_string(settings, "extra_source_list") + if extra_source ~= nil and extra_source ~= "" then + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) + obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) + obs.obs_data_set_string(script_sets, "extra_source_list", "") + obs.obs_property_set_description( + extra_linked_list, + "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" + ) + end + return true +end + +-- removes linked sources +function do_linked_clicked(props, p) + dbg_method("do_link_clicked") + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), true) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), false) + obs.obs_properties_apply_settings(props, script_sets) + + return true +end + +-- removes linked sources +function clear_linked_clicked(props, p) + dbg_method("clear_linked_clicked") + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_clear(extra_linked_list) + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) + obs.obs_property_set_description(extra_linked_list, "Linked Sources ") + + return true +end + +-- A function named script_description returns the description shown to +-- the user + +function script_description() + return description +end + +function vMode(vis) + return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" +end + +function expand_all_groups(props, prop, settings) + expandcollapse = not expandcollapse + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "info_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "mng_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "disp_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "src_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "ctrl_grp"), expandcollapse) + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + return true +end + +function all_vis_equal(props) + if + (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "prep_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) or + not (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "mng_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) + then + expandcollapse = not expandcollapse + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description( + obs.obs_properties_get(props, "expand_all_button"), + mode1 .. "ALL GROUPS" .. mode2 + ) + end +end + +function change_info_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "info_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + all_vis_equal(props) + return true +end + +function change_prepared_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "mng_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + all_vis_equal(props) + return true +end + +function change_options_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "disp_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + all_vis_equal(props) + return true +end + +function change_src_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "src_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + all_vis_equal(props) + return true +end + +function change_ctrl_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "ctrl_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + all_vis_equal(props) + return true +end + +function change_fade_property(props, prop, settings) + local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") + dbg_bool("Fade: ", text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) + local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") + obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) + return true +end + +function show_help_button(props, prop, settings) + dbg_method("show help") + local hb = obs.obs_properties_get(props, "show_help_button") + showhelp = not showhelp + if showhelp then + obs.obs_property_set_description(hb, help) + else + obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") + end + return true +end + +function setEditVis(props, prop, settings) -- hides edit group on initial showing + dbg_method("setEditVis") + if not editVisSet then + local pp = obs.obs_properties_get(script_props, "edit_grp") + obs.obs_property_set_visible(pp, false) + pp = obs.obs_properties_get(props, "meta") + obs.obs_property_set_visible(pp, false) + editVisSet = true + end +end + +function filter_songs_clicked(props, p) + local pp = obs.obs_properties_get(props, "meta") + if not obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "filter_songs_button") + obs.obs_property_set_description(mpb, "Clear Filters") -- change button function + meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + refresh_directory() + else + obs.obs_property_set_visible(pp, false) + meta_tags = "" -- clear meta tags + refresh_directory() + local mpb = obs.obs_properties_get(props, "filter_songs_button") -- + obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function + end + return true +end + +function edit_prepared_clicked(props, p) + local pp = obs.obs_properties_get(props, "edit_grp") + if obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared List") + return true + end + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + local count = obs.obs_property_list_item_count(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + if count2 > 0 then + for i = 0, count2 do + obs.obs_data_array_erase(songNames, 0) + end + end + + for i = 0, count - 1 do + local song = obs.obs_property_list_item_string(prop_prep_list, i) + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song) + obs.obs_data_array_push_back(songNames, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(script_sets, "prep_list", songNames) + obs.obs_data_array_release(songNames) + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Cancel Prepared Edits") + return true +end + +-- removes prepared songs +function save_edits_clicked(props, p) + load_source_song_directory(false) + prepared_songs = {} + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + if count2 > 0 then + for i = 0, count2 - 1 do + local item = obs.obs_data_array_item(songNames, i) + local itemName = obs.obs_data_get_string(item, "value") + if get_index_in_list(song_directory, itemName) ~= nil then + prepared_songs[#prepared_songs + 1] = itemName + obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(songNames) + save_prepared() + if #prepared_songs > 0 then + obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) + prepared_index = 1 + else + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + prepared_index = 0 + end + pp = obs.obs_properties_get(script_props, "edit_grp") + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + obs.obs_properties_apply_settings(props, script_sets) + return true +end + +function change_transition_property(props, prop, settings) + local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") + local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") + local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") + obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) + obs.obs_property_set_enabled(fade_speed_prop, not transition_set) + transition_enabled = transition_set + return true +end + +-- A function named script_save will be called when the script is saved +function script_save(settings) + dbg_method("script_save") + save_prepared() + local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) + obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) + obs.obs_data_set_array(settings, "lyric_prev_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) + obs.obs_data_set_array(settings, "lyric_clear_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) + obs.obs_data_set_array(settings, "next_prepared_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) + obs.obs_data_set_array(settings, "previous_prepared_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) + obs.obs_data_set_array(settings, "home_song_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) + obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + --- + --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded + --- + local extra_sources_array = obs.obs_data_array_create() + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + for i = 0, count - 1 do + local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", source_name) + obs.obs_data_array_push_back(extra_sources_array, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) + obs.obs_data_array_release(extra_sources_array) +end + +-- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS +-- sets callback to obs_frontend Event Callback +-- +function script_load(settings) + dbg_method("script_load") + hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") + hotkey_n_key = get_hotkeys(hotkey_save_array, "Next Lyric", ".......................") + obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") + hotkey_p_key = get_hotkeys(hotkey_save_array, "Previous Lyric", " ..................") + obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) + hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") + hotkey_c_key = get_hotkeys(hotkey_save_array, "Show/Hide Lyrics", " ..............") + obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") + hotkey_n_p_key = get_hotkeys(hotkey_save_array, "Next Prepared", " ................") + obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") + hotkey_p_p_key = get_hotkeys(hotkey_save_array, "Previous Prepared", "............") + obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) + hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") + hotkey_home_key = get_hotkeys(hotkey_save_array, "Reset to Song Start", " ..........") + obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + hotkey_reset_id = + obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) + hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") + hotkey_reset_key = get_hotkeys(hotkey_save_array, "Reset to 1st Prepared", " .......") + obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) + obs.obs_data_array_release(hotkey_save_array) + + script_sets = settings + source_name = obs.obs_data_get_string(settings, "prop_source_list") + + extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + + -- load previously defined extra sources from settings array into table + -- script_properties function will take them from the table and restore them as UI properties + -- + local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") + local count = obs.obs_data_array_count(extra_sources_array) + if count > 0 then + for i = 0, count do + local item = obs.obs_data_array_item(extra_sources_array, i) + local sourceName = obs.obs_data_get_string(item, "value") + if sourceName ~= "" then + extra_sources[#extra_sources + 1] = sourceName + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(extra_sources_array) + + -- load prepared songs from stored file + -- + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + load_source_song_directory(false) + -- load prepared songs from previous + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") + if file ~= nil then + for line in file:lines() do + prepared_songs[#prepared_songs + 1] = line + end + file:close() + end + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture +end + +--- +------ +--------- Source Showing or Source Active Helper Functions +--------- Return true if sourcename given is showing anywhere or on in the Active scene +------ +--- +function isShowing(sourceName) + local source = obs.obs_get_source_by_name(sourceName) + local showing = false + if source ~= nil then + showing = obs.obs_source_showing(source) + end + obs.obs_source_release(source) + return showing +end + +function isActive(sourceName) + local source = obs.obs_get_source_by_name(sourceName) + local active = false + if source ~= nil then + active = obs.obs_source_active(source) + end + obs.obs_source_release(source) + return active +end + +function anythingShowing() + return isShowing(source_name) or isShowing(alternate_source_name) or isShowing(title_source_name) or + isShowing(static_source_name) +end + +function sourceShowing() + return isShowing(source_name) +end + +function alternateShowing() + return isShowing(alternate_source_name) +end + +function titleShowing() + return isShowing(title_source_name) +end + +function staticShowing() + return isShowing(static_source_name) +end + +function anythingActive() + return isActive(source_name) or isActive(alternate_source_name) or isActive(title_source_name) or + isActive(static_source_name) +end + +function sourceActive() + return isActive(source_name) +end + +function alternateActive() + return isActive(alternate_source_name) +end + +function titleActive() + return isActive(title_source_name) +end + +function staticActive() + return isActive(static_source_name) +end + +--- +------ +--------- Initialization Functions +--------- Manages defined Hotkey Save, Load, Translate and Button rename +--------- Loads inital song directory and any previously prepared lyrics +------ +--- + +---------------------------------------------------------------------------------------------------------- +-- get_hotkeys(loaded hotkey array, desired prefix text, leader text (between prefix and hotkey label) +-- Returns translated hotkey text label with prefix and leader +-- e.g. if HotKeyArray contains an assigned hotkey Shift and F1 key combo, then +-- get_hotkeys(HotKeyArray," ....... ", "HotKey") returns "Hotkey ....... Shift + F1" +---------------------------------------------------------------------------------------------------------- + +function get_hotkeys(hotkey_array, prefix, leader) + local Translate = { + ["NUMLOCK"] = "NumLock", + ["NUMSLASH"] = "Num/", + ["NUMASTERISK"] = "Num*", + ["NUMMINUS"] = "Num-", + ["NUMPLUS"] = "Num+", + ["NUMPERIOD"] = "NumDel", + ["INSERT"] = "Insert", + ["PAGEDOWN"] = "Page-Down", + ["PAGEUP"] = "Page-Up", + ["HOME"] = "Home", + ["END"] = "End", + ["RETURN"] = "Return", + ["UP"] = "Up", + ["DOWN"] = "Down", + ["RIGHT"] = "Right", + ["LEFT"] = "Left", + ["SCROLLLOCK"] = "Scroll-Lock", + ["BACKSPACE"] = "Backspace", + ["ESCAPE"] = "Esc", + ["MENU"] = "Menu", + ["META"] = "Meta", + ["PRINT"] = "Prt", + ["TAB"] = "Tab", + ["DELETE"] = "Del", + ["CAPSLOCK"] = "Caps-Lock", + ["NUMEQUAL"] = "Num=", + ["PAUSE"] = "Pause", + ["VK_VOLUME_MUTE"] = "Vol Mute", + ["VK_VOLUME_DOWN"] = "Vol Dwn", + ["VK_VOLUME_UP"] = "Vol Up", + ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", + ["VK_MEDIA_STOP"] = "Media Stop", + ["VK_MEDIA_PREV_TRACK"] = "Media Prev", + ["VK_MEDIA_NEXT_TRACK"] = "Media Next" + } + + item = obs.obs_data_array_item(hotkey_array, 0) + local key = string.sub(obs.obs_data_get_string(item, "key"), 9) + if Translate[key] ~= nil then + key = Translate[key] + elseif string.sub(key, 1, 3) == "NUM" then + key = "Num " .. string.sub(key, 4) + elseif string.sub(key, 1, 5) == "MOUSE" then + key = "Mouse " .. string.sub(key, 6) + end + + obs.obs_data_release(item) + local val = prefix + if key ~= nil and key ~= "" then + val = val .. " " .. leader .. " " + if obs.obs_data_get_bool(item, "control") then + val = val .. "Ctrl + " + end + if obs.obs_data_get_bool(item, "alt") then + val = val .. "Alt + " + end + if obs.obs_data_get_bool(item, "shift") then + val = val .. "Shift + " + end + if obs.obs_data_get_bool(item, "command") then + val = val .. "Cmd + " + end + val = val .. key + end + return val +end + +-- name_hotkeys function renames the seven hotkeys to include their defined key text +-- +function name_hotkeys() + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) + obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) +end + +-------- +---------------- +------------------------ SOURCE FUNCTIONS +---------------- +-------- + +-- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change +-- +function rename_source() + -- pause_timer = true + local sources = obs.obs_enum_sources() + if (sources ~= nil) then + -- count and index sources + local t = 1 + for _, source in ipairs(sources) do + local source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "Prepare_Lyrics" then + local settings = obs.obs_source_get_settings(source) + obs.obs_data_set_string(settings, "index", t) -- add index to source data + t = t + 1 + obs.obs_data_release(settings) -- release memory + end + end + -- Find and mark Duplicates in loadLyric_items table + local loadLyric_items = {} -- Start Table for all load Sources + local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items + if scenes ~= nil then + for _, scenesource in ipairs(scenes) do -- Loop through all scenes + local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer + local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name + local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene + if scene_items ~= nil then + for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items + local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer + local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id + if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item + local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source + local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) + if loadLyric_items[index] == nil then + loadLyric_items[index] = 1 -- First time to find this source so mark with 1 + else + loadLyric_items[index] = loadLyric_items[index] + 1 -- Found this source again so increment + end + obs.obs_data_release(settings) -- release memory + end + end + end + obs.sceneitem_list_release(scene_items) -- Free scene list + end + obs.source_list_release(scenes) -- Free source list + end + + -- Name Source with Song Title + local i = 1 + for _, source in ipairs(sources) do + local source_id = obs.obs_source_get_unversioned_id(source) -- Get source + if source_id == "Prepare_Lyrics" then -- Skip if not a Load Lyric source + local c_name = obs.obs_source_get_name(source) -- Get current Source Name + local settings = obs.obs_source_get_settings(source) -- Get settings for this source + local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load + local index = obs.obs_data_get_string(settings, "index") -- get index + if (song ~= nil) then + local name = "Load lyrics for: " .. song .. "" -- use index for compare + -- Mark Duplicates + if index ~= nil then + if loadLyric_items[index] > 1 then + name = + '' .. + name .. " " .. loadLyric_items[index] .. "" + end + if (c_name ~= name) then + obs.obs_source_set_name(source, name) + end + end + i = i + 1 + end + obs.obs_data_release(settings) + end + end + end + obs.source_list_release(sources) + -- pause_timer = false +end + +-- Names the initial "Prepare Lyric" source (prior to being renamed to "Load Lyrics for: {song name} +-- +source_def.get_name = function() + return "Prepare Lyric" +end + +-- Called when OBS is saving data. This will be called on each copy of Load Lyric source +-- Used to initiate rename_source() function when the source dialog closes +-- saved flag prevents it from being called by every source each time. +-- +source_def.save = function(data, settings) + if saved then + return + end -- we only need it once, not for every load lyric source copy + dbg_method("Source_save") + saved = true + using_source = true + rename_source() -- Rename and Mark sources instantly on update (WZ) +end + +-- Called when a change is made in the source dialog (Currently Not Used) +-- +source_def.update = function(data, settings) + dbg_method("update") +end + +-- Called when the source dialog is loaded (Currently not Used) +-- +source_def.load = function(data) + dbg_method("load") +end + +-- Called when the refresh button is pressed in the source dialog +-- It reloads the song directory and applies any meta-tag filters if entered +-- +function source_refresh_button_clicked(props, p) + dbg_method("source_refresh_button") + source_filter = true + dbg_inner("tags: " .. source_meta_tags) + load_source_song_directory(true) + table.sort(song_directory) + local prop_dir_list = obs.obs_properties_get(props, "songs") + obs.obs_property_list_clear(prop_dir_list) -- clear directories + for _, name in ipairs(song_directory) do + dbg_inner("SLD: " .. name) + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + return true +end + +-- Keeps variable source-meta-tags up-to-date +-- Note: This could be done only when refreshing the directory (see source_refresh_button_clicked) +-- +function update_source_metatags(props, p, settings) + source_meta_tags = obs.obs_data_get_string(settings, "metatags") + return true +end + +-- Called when a user makes a song selection in the source dialog +-- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens +-- Saved flag is cleared to mark changes have occured for save event +-- +function source_selection_made(props, prop, settings) + dbg_method("source_selection") + local name = obs.obs_data_get_string(settings, "songs") + saved = false -- mark properties changed + using_source = true + prepare_selected(name) + return true +end + +-- Standard OBS get Properties function for OBS source dialog +-- +source_def.get_properties = function(data) + source_filter = true + load_source_song_directory(true) + local source_props = obs.obs_properties_create() + local source_dir_list = + obs.obs_properties_add_list( + source_props, + "songs", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(source_dir_list, name, name) + end + gps = obs.obs_properties_create() + source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) + obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) + obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode + obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode + obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) + dbg_inner("props") + return source_props +end + +-- Called when the source is created +-- saves pointer to settings in global sourc_sets for convienence +-- Sets callbacks for active, showing, deactive, and updated callbacks +-- +source_def.create = function(settings, source) + dbg_method("create") + data = {} + source_sets = settings + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback + obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback + return data +end + +-- Sets default settings for Activate Source in Preview +-- +source_def.get_defaults = function(settings) + obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) +end + +-- On Event Functions +-- These manage keeping the HTML monitor page updated when changes happen like scene changes that remove +-- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are +-- either created or deleted. Rename changes color and marks with *, sources that are reference copies of the same source +-- as accidentally changing the settings like the loaded song in one will change it in the reference copies. +-- + +-- Called via the timed callback, removes the callback and updates the HTML monitor page +-- +function update_source_callback() + obs.remove_current_callback() + update_monitor() +end + +-- called via the timed callback, removes the callback and renames all the load sources +-- +function rename_callback() + obs.remove_current_callback() + rename_source() +end + +-- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events +function on_event(event) + print(event) + if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page + dbg_bool("Active:", source_active) + obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS + end + if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes + dbg_inner("Scene Change") + obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change + end +end + +-- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. +-- +function load_source_song(source, preview) + dbgsp("load_source_song") + local settings = obs.obs_source_get_settings(source) + if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then + local song = obs.obs_data_get_string(settings, "songs") + using_source = true + load_source = source + all_sources_fade = true -- fade title and source the first time + set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in + if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles + prepare_selected(song) + end + transition_lyric_text() + if obs.obs_data_get_bool(settings, "source_home_on_active") then + home_prepared(true) + end + end + obs.obs_data_release(settings) +end + +-- Call back when load source (not text source) goes to the Active Scene +-- loads the selected song and sets the current scene name for the HTML monitor +-- +function source_isactive(cd) + dbg_custom("source_active") + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + dbg_inner("source active") + load_scene = get_current_scene_name() + load_source_song(source, false) + source_active = true -- using source lyric +end + +-- Call back when load source leaves the current Active Scene +-- just resets the source_active flag +-- +function source_inactive(cd) + dbg_inner("source inactive") + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + source_active = false -- indicates source loading lyric is active (but using prepared lyrics is still possible) +end + +-- Call back when load source (not text source) goes to the Active +-- loads the selected song and sets the current scene name for the HTML monitor +-- +function source_showing(cd) + dbg_custom("source_showing") + local source = obs.calldata_source(cd, "source") + if source == nil then + return + end + load_source_song(source, true) +end + +-- dbg functions +-- +function dbg_traceback() + if DEBUG then + print("Trace: " .. debug.traceback()) + end +end + +function dbg(message) + if DEBUG then + print(message) + end +end + +function dbg_inner(message) + if DEBUG_INNER then + dbg("INNR: " .. message) + end +end + +function dbg_method(message) + if DEBUG_METHODS then + dbg("-- MTHD: " .. message) + end +end + +function dbgsp(message) + if DEBUG then + dbg("====SPECIAL=====================>> " .. message) + end +end +function dbg_custom(message) + if DEBUG_CUSTOM then + dbg("CUST: " .. message) + end +end + +function dbg_bool(name, value) + if DEBUG_BOOL then + local message = "BOOL: " .. name + if value then + message = message .. " = true" + else + message = message .. " = false" + end + dbg(message) + end +end + +obs.obs_register_source(source_def) + +description = + [[ +
OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian
+]] diff --git a/lyrics+.lua b/lyrics+.lua index 3244072..0364129 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -240,6 +240,7 @@ function toggle_lyrics_visibility(pressed) all_sources_fade = true end if text_status ~= TEXT_HIDDEN then + read_source_opacity() -- record maximum opacities for TEXT_VISIBLE condition. dbg_inner("hiding") set_text_visibility(TEXT_HIDDEN) else @@ -588,17 +589,20 @@ end ---------------- -------- function setSourceOpacity(sourceName) - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity - local source = obs.obs_get_source_by_name(sourceName) - if source ~= nil then - obs.obs_source_update(source, settings) - end - obs.obs_source_release(source) - obs.obs_data_release(settings) + if sourceName ~= nil and sourceName ~= "" then + local settings = obs.obs_data_create() + adj_text_opacity = text_opacity /100 + obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity + obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity + local source = obs.obs_get_source_by_name(sourceName) + if source ~= nil then + obs.obs_source_update(source, settings) + end + obs.obs_source_release(source) + obs.obs_data_release(settings) + end end @@ -614,22 +618,17 @@ function apply_source_opacity() local count = obs.obs_property_list_item_count(extra_linked_list) if count > 0 then for i = 0, count - 1 do - local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local extra_source = obs.obs_get_source_by_name(source_name) + local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local extra_source = obs.obs_get_source_by_name(sourceName) if extra_source ~= nil then source_id = obs.obs_source_get_unversioned_id(extra_source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - local settings = obs.obs_data_create() - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- set new background opacity - obs.obs_source_update(source, settings) + setSourceOpacity(sourceName) else -- check for filter named "Color Correction" local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_source_get_settings(color_filter) - obs.obs_data_set_double(filter_settings, "opacity", text_opacity / 100) + local filter_settings = obs.obs_data_create() + obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) obs.obs_source_update(color_filter, filter_settings) obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) @@ -652,7 +651,53 @@ function apply_source_opacity() end end - +function getSourceOpacity(sourceName) + if sourceName ~= nil and sourceName ~= "" then + local source = obs.obs_get_source_by_name(sourceName) + local settings = obs.obs_source_get_settings(source) + max_opacity[sourceName]={} + max_opacity[sourceName]["opacity"] = obs.obs_data_get_int(settings, "opacity") -- text opacity + max_opacity[sourceName]["outline"] = obs.obs_data_get_int(settings, "outline_opacity") -- outline opacity + max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity + max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity + obs.obs_source_release(source) + obs.obs_data_release(settings) + end +end + + +function read_source_opacity() + getSourceOpacity(source_name) + getSourceOpacity(alternate_source_name) + getSourceOpacity(title_source_name) + getSourceOpacity(static_source_name) + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count - 1 do + local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local extra_source = obs.obs_get_source_by_name(sourceName) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + getSourceOpacity(sourceName) + else -- check for filter named "Color Correction" + + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_source_get_settings(color_filter) + max_opacity[sourceName]={} + max_opacity[sourceName]["CC-opacity"] = obs.obs_data_get_double(filter_settings, "opacity") + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end +end + function set_text_visibility(end_status) dbg_method("set_text_visibility") -- if already at desired visibility, then exit @@ -1667,16 +1712,16 @@ end local help = "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. " Markup      Syntax         Markup      Syntax \n" .. - "============ ==========   ============ ==========\n" .. - " Display n Lines    #L:n      End Page after Line   Line ###\n" .. - " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. - " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. - " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. - " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. - "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. - "Comment Line    // Line       Block Comments    //[ and //] \n" .. - "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line" + "============ ==========   ============ ==========\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line" function script_properties() dbg_method("script_properties") @@ -1774,21 +1819,9 @@ function script_properties() obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button( - hotkey_props, - "prop_reset_button", - "Reset to First Prepared Song", - reset_button_clicked - ) - ctrl_grp_prop = - obs.obs_properties_add_group( - script_props, - "ctrl_grp", - "Lyric Control Buttons (with Assigned HotKeys)", - obs.OBS_GROUP_NORMAL, - hotkey_props - ) - obs.obs_property_set_modified_callback(ctrl_grp_prop, name_hotkeys) + obs.obs_properties_add_button(hotkey_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked) + obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)",obs.OBS_GROUP_NORMAL,hotkey_props) + name_hotkeys() ------ obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) gp = obs.obs_properties_create() @@ -1931,6 +1964,7 @@ function script_update(settings) ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") + read_source_opacity() -- update opacities if sources might have changed end -- A function named script_defaults will be called to set the default settings @@ -2308,7 +2342,7 @@ function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - hotkey_n_key = get_hotkeys(hotkey_save_array, "Next Lyric", " ......................") + hotkey_n_key = get_hotkeys(hotkey_save_array, "Next Lyric", ".......................") obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) @@ -2385,8 +2419,6 @@ function script_load(settings) end file:close() end - name_hotkeys() - obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end @@ -2785,7 +2817,7 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) - dbg_method("on_event: " .. event) + print(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page dbg_bool("Active:", source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS From 4ae8357a2f2cd3d00bfd57bf207c8aa787ad76f7 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Sun, 17 Oct 2021 22:40:55 -0600 Subject: [PATCH 058/105] Some fixes for fading non-100% opacities. It is possible to get a text object "lost" by having it fade to 0% with HIDE then closing the script without putting it back, when not forcing 0-100% fades. This is because the script needs to try to "read" the current opacity level and then re-use that as the max. So a new button allows manually setting these maximums when all sources are as they should be. Doing this automatically is nearly impossible. Probably setting source item callbacks and trying to management them. I have scripts that do this but they are a pain to keep up with. Also required is the option to NOT fade backgrounds if you want to keep them transparent for text items for example, or just want them left alone. I employed progressive disclosure whenever possible to try to limit complexity of the UI until required. --- LyricsTrial.lua | 51 ++++++++++---- lyrics+.lua | 184 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 176 insertions(+), 59 deletions(-) diff --git a/LyricsTrial.lua b/LyricsTrial.lua index 0364129..91f0eac 100644 --- a/LyricsTrial.lua +++ b/LyricsTrial.lua @@ -113,6 +113,7 @@ text_fade_enabled = false load_source = nil expandcollapse = true showhelp = false +use100percent = false transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false @@ -122,8 +123,8 @@ source_saved = false -- ick... A saved toggle to keep from repeating the save editVisSet = false -- simple debugging/print mechanism ---DEBUG = true -- on switch for entire debugging mechanism ---DEBUG_METHODS = true -- print method names +DEBUG = true -- on switch for entire debugging mechanism +DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false @@ -240,7 +241,6 @@ function toggle_lyrics_visibility(pressed) all_sources_fade = true end if text_status ~= TEXT_HIDDEN then - read_source_opacity() -- record maximum opacities for TEXT_VISIBLE condition. dbg_inner("hiding") set_text_visibility(TEXT_HIDDEN) else @@ -591,11 +591,19 @@ end function setSourceOpacity(sourceName) if sourceName ~= nil and sourceName ~= "" then local settings = obs.obs_data_create() - adj_text_opacity = text_opacity /100 - obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity - obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity + if not use100percent then + adj_text_opacity = text_opacity /100 + obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity + obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity + else + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity + end + local source = obs.obs_get_source_by_name(sourceName) if source ~= nil then obs.obs_source_update(source, settings) @@ -627,8 +635,12 @@ function apply_source_opacity() else -- check for filter named "Color Correction" local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_data_create() - obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) + local filter_settings = obs.obs_data_create() + if not use100percent then + obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) + else + obs.obs_data_set_double(filter_settings, "opacity", text_opacity/100) + end obs.obs_source_update(color_filter, filter_settings) obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) @@ -667,6 +679,7 @@ end function read_source_opacity() + dbg_method("read_source_opacity") getSourceOpacity(source_name) getSourceOpacity(alternate_source_name) getSourceOpacity(title_source_name) @@ -1834,6 +1847,7 @@ function script_properties() obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") + local transition_prop = obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") obs.obs_property_set_modified_callback(transition_prop, change_transition_property) @@ -1844,6 +1858,7 @@ function script_properties() local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + obs.obs_properties_add_bool(gp,"use100percent", "Always use 0-100% opacity for fades") obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) ------------- obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) @@ -1881,7 +1896,8 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + obs.obs_properties_add_button(gp, "Opacity_refresh", "Mark Source Opacities if not 100%", read_source_opacity()) + obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) xgp = obs.obs_properties_create() obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") local extra_linked_prop = @@ -1949,6 +1965,7 @@ function script_properties() obs.obs_property_set_enabled(hktitletext, false) obs.obs_property_set_visible(edit_group_prop, false) obs.obs_property_set_visible(meta_group_prop, false) + read_source_opacity() return script_props end @@ -1964,7 +1981,7 @@ function script_update(settings) ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") - read_source_opacity() -- update opacities if sources might have changed + use100percent = obs.obs_data_get_bool(settings, "use100percent") end -- A function named script_defaults will be called to set the default settings @@ -2419,9 +2436,18 @@ function script_load(settings) end file:close() end + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end +function script_unload() +all_sources_fade = true +text_opacity = 100 +apply_source_opacity() + +end + + --- ------ --------- Source Showing or Source Active Helper Functions @@ -2817,7 +2843,6 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) - print(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page dbg_bool("Active:", source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS diff --git a/lyrics+.lua b/lyrics+.lua index 0364129..c80b87c 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -113,6 +113,12 @@ text_fade_enabled = false load_source = nil expandcollapse = true showhelp = false +use100percent = false +fade_text_back = false +fade_title_back = false +fade_alternate_back = false +fade_static_back = false +fade_extra_back = false transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false @@ -122,8 +128,8 @@ source_saved = false -- ick... A saved toggle to keep from repeating the save editVisSet = false -- simple debugging/print mechanism ---DEBUG = true -- on switch for entire debugging mechanism ---DEBUG_METHODS = true -- print method names +DEBUG = true -- on switch for entire debugging mechanism +DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false @@ -240,7 +246,6 @@ function toggle_lyrics_visibility(pressed) all_sources_fade = true end if text_status ~= TEXT_HIDDEN then - read_source_opacity() -- record maximum opacities for TEXT_VISIBLE condition. dbg_inner("hiding") set_text_visibility(TEXT_HIDDEN) else @@ -488,8 +493,8 @@ end -- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) obs.obs_property_set_description( - obs.obs_properties_get(props, "prep_grp"), - " Prepared Songs/Text (" .. #prepared_songs .. ")" + obs.obs_properties_get(props, "prop_prepared_list"), + "Prepared (" .. #prepared_songs .. ")" ) dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") @@ -588,30 +593,53 @@ end ------------------------ PROGRAM FUNCTIONS ---------------- -------- -function setSourceOpacity(sourceName) +function setSourceOpacity(sourceName, fadeBackground) if sourceName ~= nil and sourceName ~= "" then - local settings = obs.obs_data_create() - adj_text_opacity = text_opacity /100 - obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity - obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity - local source = obs.obs_get_source_by_name(sourceName) - if source ~= nil then - obs.obs_source_update(source, settings) + if text_fade_enabled then + local settings = obs.obs_data_create() + if use100percent then -- try to honor preset maximum opacities + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity + if fadeBackground then + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity + end + else + adj_text_opacity = text_opacity /100 + obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity + if fadeBackground then + obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity + end + end + local source = obs.obs_get_source_by_name(sourceName) + if source ~= nil then + obs.obs_source_update(source, settings) + end + obs.obs_source_release(source) + obs.obs_data_release(settings) + else + local sceneSource = obs.obs_frontend_get_current_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source(sceneObj, sourceName) + obs.obs_source_release(sceneSource) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end end - obs.obs_source_release(source) - obs.obs_data_release(settings) end end function apply_source_opacity() - setSourceOpacity(source_name) - setSourceOpacity(alternate_source_name) + setSourceOpacity(source_name, fade_text_back) + setSourceOpacity(alternate_source_name, fade_alternate_back) if all_sources_fade then - setSourceOpacity(title_source_name) - setSourceOpacity(static_source_name) + setSourceOpacity(title_source_name, fade_title_back) + setSourceOpacity(static_source_name, fade_static_back) end if link_extras or all_sources_fade then local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") @@ -623,20 +651,24 @@ function apply_source_opacity() if extra_source ~= nil then source_id = obs.obs_source_get_unversioned_id(extra_source) if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - setSourceOpacity(sourceName) + setSourceOpacity(sourceName, fade_extra_back) else -- check for filter named "Color Correction" local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") - if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_data_create() - obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) + if color_filter ~= nil and text_fade_enabled then -- update filters opacity + local filter_settings = obs.obs_data_create() + if use100percent then + obs.obs_data_set_double(filter_settings, "opacity", text_opacity/100) + else + obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) + end obs.obs_source_update(color_filter, filter_settings) obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) else -- try to just change visibility in the scene - local sceneSource = obs.obs_frontend_get_current_scene() + local sceneSource = obs.obs_frontend_get_current_preview_scene() local sceneObj = obs.obs_scene_from_source(sceneSource) - local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) - obs.obs_source_release(scene) + local sceneItem = obs.obs_scene_find_source(sceneObj, sourceName) + obs.obs_source_release(sceneSource) if text_opacity > 50 then obs.obs_sceneitem_set_visible(sceneItem, true) else @@ -665,8 +697,15 @@ function getSourceOpacity(sourceName) end end +-- removes prepared songs +function read_source_opacity_clicked(props, p) + dbg_method("read_opacities_clicked") + read_source_opacity() + return true +end function read_source_opacity() + dbg_method("read_source_opacity") getSourceOpacity(source_name) getSourceOpacity(alternate_source_name) getSourceOpacity(title_source_name) @@ -1777,7 +1816,7 @@ function script_properties() obs.obs_properties_add_list( gps, "prop_prepared_list", - "Prepared Songs", + "Prepared ", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) @@ -1785,6 +1824,10 @@ function script_properties() obs.obs_property_list_add_string(prepare_prop, name, name) end obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) + local count = obs.obs_property_list_item_count(prepare_prop) + if count > 0 then + obs.obs_property_set_description( prepare_prop, "Prepared (" .. count .. ")") + end obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) local eps = obs.obs_properties_create() @@ -1807,7 +1850,7 @@ function script_properties() obs.OBS_GROUP_NORMAL, eps ) - obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) @@ -1834,8 +1877,9 @@ function script_properties() obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") + local transition_prop = - obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") obs.obs_property_set_modified_callback(transition_prop, change_transition_property) obs.obs_property_set_long_description( transition_prop, @@ -1843,12 +1887,14 @@ function script_properties() ) local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + local fp1 = obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + local fp2 = obs.obs_properties_add_bool(gp,"use100percent", "Use 0-100% opacity for fades") + obs.obs_property_set_modified_callback(fp2, change_100percent_property) + local oprefprop = obs.obs_properties_add_button(gp, "refreshOP", "Mark Max Opacity for Source Fades", read_source_opacity_clicked) obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) ------------- obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) gp = obs.obs_properties_create() - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) local source_prop = obs.obs_properties_add_list( gp, @@ -1857,6 +1903,7 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) + local flbprop = obs.obs_properties_add_bool(gp, "fade_text_back", "Fade Text Background") local title_source_prop = obs.obs_properties_add_list( gp, @@ -1865,6 +1912,7 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) + local ftbprop = obs.obs_properties_add_bool(gp, "fade_title_back", "Fade Title Background") local alternate_source_prop = obs.obs_properties_add_list( gp, @@ -1873,6 +1921,7 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) + local fabprop = obs.obs_properties_add_bool(gp, "fade_alternate_back", "Fade Alternate Background") local static_source_prop = obs.obs_properties_add_list( gp, @@ -1881,14 +1930,16 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + local fsbprop = obs.obs_properties_add_bool(gp, "fade_static_back", "Fade Static Background") + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) + + local dlprop = obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) xgp = obs.obs_properties_create() - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") local extra_linked_prop = obs.obs_properties_add_list( xgp, "extra_linked_list", - "Linked Sources ", + "Linked Sources", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1905,6 +1956,8 @@ function script_properties() obs.OBS_COMBO_FORMAT_STRING ) obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") + local febprop = obs.obs_properties_add_bool(xgp, "fade_extra_back", "Fade Background for Text Sources") local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) local extra_group_prop = @@ -1912,9 +1965,13 @@ function script_properties() obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) local count = obs.obs_property_list_item_count(extra_linked_prop) if count > 0 then - obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") + do_linked_clicked(script_props,dlprop) + obs.obs_property_set_description( + extra_linked_prop, + "Linked Sources (" .. count .. ")" + ) else - obs.obs_property_set_visible(extra_group_prop, false) + clear_linked_clicked(script_props, clearcall_prop) end local sources = obs.obs_enum_sources() @@ -1949,6 +2006,16 @@ function script_properties() obs.obs_property_set_enabled(hktitletext, false) obs.obs_property_set_visible(edit_group_prop, false) obs.obs_property_set_visible(meta_group_prop, false) + obs.obs_property_set_visible(fp1, text_fade_enabled) + obs.obs_property_set_visible(fp2, text_fade_enabled) + obs.obs_property_set_visible(flbprop, text_fade_enabled) + obs.obs_property_set_visible(ftbprop, text_fade_enabled) + obs.obs_property_set_visible(fabprop, text_fade_enabled) + obs.obs_property_set_visible(fsbprop, text_fade_enabled) + obs.obs_property_set_visible(febprop, text_fade_enabled) + obs.obs_property_set_visible(oprefprop, (text_fade_enabled and (not use100percent))) + + read_source_opacity() return script_props end @@ -1964,7 +2031,12 @@ function script_update(settings) ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") - read_source_opacity() -- update opacities if sources might have changed + use100percent = obs.obs_data_get_bool(settings, "use100percent") + fade_text_back = obs.obs_data_get_bool(settings, "fade_text_back") + fade_title_back = obs.obs_data_get_bool(settings, "fade_title_back") + fade_alternate_back = obs.obs_data_get_bool(settings, "fade_alternate_back") + fade_static_back = obs.obs_data_get_bool(settings, "fade_static_back") + fade_extra_back = obs.obs_data_get_bool(settings, "fade_extra_back") end -- A function named script_defaults will be called to set the default settings @@ -2026,10 +2098,11 @@ end function clear_linked_clicked(props, p) dbg_method("clear_linked_clicked") local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_clear(extra_linked_list) obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) - obs.obs_property_set_description(extra_linked_list, "Linked Sources ") + obs.obs_property_set_description(extra_linked_list, "Linked Sources") return true end @@ -2160,13 +2233,25 @@ end function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") - dbg_bool("Fade: ", text_fade_set) obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "use100percent"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), text_fade_enabled and not use100percent) local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) return true end +function change_100percent_property(props, prop, settings) + use100percent = obs.obs_data_get_bool(settings, "use100percent") + obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), not use100percent) + return true +end + function show_help_button(props, prop, settings) dbg_method("show help") local hb = obs.obs_properties_get(props, "show_help_button") @@ -2278,12 +2363,11 @@ function save_edits_clicked(props, p) end function change_transition_property(props, prop, settings) - local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") + transition_enabled = obs.obs_data_get_bool(settings, "transition_enabled") local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") - obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) - obs.obs_property_set_enabled(fade_speed_prop, not transition_set) - transition_enabled = transition_set + obs.obs_property_set_enabled(text_fade_set_prop, not transition_enabled) + obs.obs_property_set_enabled(fade_speed_prop, not transition_enabled) return true end @@ -2419,9 +2503,18 @@ function script_load(settings) end file:close() end + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end +function script_unload() +all_sources_fade = true +text_opacity = 100 +apply_source_opacity() + +end + + --- ------ --------- Source Showing or Source Active Helper Functions @@ -2817,7 +2910,6 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) - print(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page dbg_bool("Active:", source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS From 4d2d616832ce84110dc63da5aae37cc4fb2793eb Mon Sep 17 00:00:00 2001 From: amirchev Date: Mon, 18 Oct 2021 14:03:04 -0700 Subject: [PATCH 059/105] added checks for nil to prepared_index --- lyrics.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 523d8c4..248f3e4 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -844,7 +844,7 @@ function update_source_text() local next_prepared = "" if using_source then next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song - elseif prepared_index < #prepared_songs then + elseif prepared_index ~= nil and prepared_index < #prepared_songs then next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song else if source_active then @@ -1554,7 +1554,11 @@ function update_monitor() if using_source then text = text .. "From Source: " .. load_scene .. "
" else - text = text .. "Prepared Song: " .. prepared_index + local indexText = "N/A" + if prepared_index ~= nil then + indexText = prepared_index + end + text = text .. "Prepared Song: " .. indexText text = text .. " of " .. #prepared_songs .. "
" From bc38bcd17418d49d22bf5e2f8beb20c00536cf56 Mon Sep 17 00:00:00 2001 From: amirchev Date: Mon, 18 Oct 2021 14:13:10 -0700 Subject: [PATCH 060/105] disable debugging --- lyrics.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 248f3e4..1b04126 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -121,11 +121,11 @@ source_saved = false -- ick... A saved toggle to keep from repeating the save editVisSet = false -- simple debugging/print mechanism -DEBUG = true -- on switch for entire debugging mechanism +DEBUG = false -- on switch for entire debugging mechanism DEBUG_METHODS = true -- print method names ---DEBUG_INNER = true -- print inner method breakpoints ---DEBUG_CUSTOM = true -- print custom debugging messages ---DEBUG_BOOL = true -- print message with bool state true/false +DEBUG_INNER = true -- print inner method breakpoints +DEBUG_CUSTOM = true -- print custom debugging messages +DEBUG_BOOL = true -- print message with bool state true/false -------- ---------------- @@ -638,7 +638,7 @@ function apply_source_opacity() if count > 0 then for i = 0, count - 1 do local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - print(source_name) + dbg_inner(source_name) local extra_source = obs.obs_get_source_by_name(source_name) if extra_source ~= nil then source_id = obs.obs_source_get_unversioned_id(extra_source) @@ -657,7 +657,7 @@ function apply_source_opacity() obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) else -- try to just change visibility in the scene - print("No Filter") + dbg_inner("No Filter") local sceneSource = obs.obs_frontend_get_current_scene() local sceneObj = obs.obs_scene_from_source(sceneSource) local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) @@ -1979,7 +1979,7 @@ end function isValid(source) if source ~= nil then local flags = obs.obs_source_get_output_flags(source) - print(obs.obs_source_get_name(source) .. " - " .. flags) + dbg_inner(obs.obs_source_get_name(source) .. " - " .. flags) local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) if bit.band(flags, targetFlag) == targetFlag then return true From aa45cbd414d625acd9c62afe8851e2c01b04088b Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 19 Oct 2021 21:44:53 -0600 Subject: [PATCH 061/105] Saving Prepared List to External File is optional Added bool option setting to below the Save in the Optional Edit Prepared section. It is related to saving prepared songs and it is in a optional section that won't typically take up space. --- LyricsTrial.lua | 39 ++++++++++++--- lyrics+.lua | 129 ++++++++++++++++++++++++++++++++++++------------ lyrics.lua | 102 +++++++++++++++++++++++++++----------- 3 files changed, 203 insertions(+), 67 deletions(-) diff --git a/LyricsTrial.lua b/LyricsTrial.lua index 91f0eac..064f820 100644 --- a/LyricsTrial.lua +++ b/LyricsTrial.lua @@ -123,8 +123,8 @@ source_saved = false -- ick... A saved toggle to keep from repeating the save editVisSet = false -- simple debugging/print mechanism -DEBUG = true -- on switch for entire debugging mechanism -DEBUG_METHODS = true -- print method names +--DEBUG = true -- on switch for entire debugging mechanism +--DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false @@ -645,7 +645,7 @@ function apply_source_opacity() obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) else -- try to just change visibility in the scene - local sceneSource = obs.obs_frontend_get_current_scene() + local sceneSource = obs.obs_frontend_get_current_preview_scene() local sceneObj = obs.obs_scene_from_source(sceneSource) local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) obs.obs_source_release(scene) @@ -1825,7 +1825,7 @@ function script_properties() ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) hotkey_props = obs.obs_properties_create() - local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "", obs.OBS_TEXT_DEFAULT) + local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "+", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) @@ -1834,6 +1834,7 @@ function script_properties() obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) obs.obs_properties_add_button(hotkey_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked) obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)",obs.OBS_GROUP_NORMAL,hotkey_props) + obs.obs_property_set_modified_callback(hktitletext, nameKeysCallback) name_hotkeys() ------ obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) @@ -1981,7 +1982,12 @@ function script_update(settings) ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") + fade_text = obs.obs_data_get_bool(settings, "use100percent") use100percent = obs.obs_data_get_bool(settings, "use100percent") + use100percent = obs.obs_data_get_bool(settings, "use100percent") + use100percent = obs.obs_data_get_bool(settings, "use100percent") + use100percent = obs.obs_data_get_bool(settings, "use100percent") + use100percent = obs.obs_data_get_bool(settings, "use100percent") end -- A function named script_defaults will be called to set the default settings @@ -2046,7 +2052,7 @@ function clear_linked_clicked(props, p) obs.obs_property_list_clear(extra_linked_list) obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) - obs.obs_property_set_description(extra_linked_list, "Linked Sources ") + obs.obs_property_set_description(extra_linked_list, "Linked Sources") return true end @@ -2113,6 +2119,24 @@ function all_vis_equal(props) end end +function updateProperties() + local p = obs.obs_properties_get(props, "hotkey-title") + local v = obs.obs_property_get_description(p) + if v == '+' then + v = '-' + else + v = '+' + end + print(v) + obs.obs_property_set_description(p,v) +end + +function name_keys_callback(props, prop, settings) + print("Name") + name_hotkeys() + return true +end + function change_info_visible(props, prop, settings) local pp = obs.obs_properties_get(script_props, "info_grp") local vis = not obs.obs_property_visible(pp) @@ -2350,6 +2374,8 @@ function script_save(settings) end obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) obs.obs_data_array_release(extra_sources_array) + +) end -- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS @@ -2436,7 +2462,7 @@ function script_load(settings) end file:close() end - + obs.timer_add(updateProperties, 1000) obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end @@ -2603,6 +2629,7 @@ end -- name_hotkeys function renames the seven hotkeys to include their defined key text -- function name_hotkeys() +dbg_method("Name Hotkeys") obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) diff --git a/lyrics+.lua b/lyrics+.lua index c80b87c..ca8e907 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -408,6 +408,8 @@ function delete_song_clicked(props, p) end -- prepare song button clicked +-- Adds the currently selected song from the directory to the prepared list +-- function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") if #prepared_songs == 0 then @@ -418,9 +420,12 @@ function prepare_song_clicked(props, p) obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - + if #prepared_songs > 0 then + obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") + else + obs.obs_property_set_description(prop_prep_list, "Prepared") + end obs.obs_properties_apply_settings(props, script_sets) - save_prepared() return true end @@ -492,14 +497,11 @@ end -- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) - obs.obs_property_set_description( - obs.obs_properties_get(props, "prop_prepared_list"), - "Prepared (" .. #prepared_songs .. ")" - ) dbg_method("prepare_selection_made") local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false prepare_selected(name) + return true end @@ -509,16 +511,20 @@ function clear_prepared_clicked(props, p) prepared_songs = {} -- required for monitor page page_index = 0 -- required for monitor page prepared_index = 0 -- required for monitor page - update_source_text() -- required for monitor page + save_prepared() + update_monitor() -- required for monitor page -- clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - obs.obs_properties_apply_settings(props, script_sets) - save_prepared() + + obs.obs_property_set_description(obs.obs_properties_get(props, "prop_prepared_list"), "Prepared") + obs.obs_property_list_add_string(obs.obs_properties_get(props, "prop_prepared_list"), "", "") + --s.obs_properties_apply_settings(props, script_sets) return true end +-- +-- This function prepares the currently selected song function prepare_selected(name) dbg_method("prepare_selected") -- try to prepare song @@ -1820,6 +1826,7 @@ function script_properties() obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end @@ -1850,6 +1857,9 @@ function script_properties() obs.OBS_GROUP_NORMAL, eps ) + local saveExtProp = obs.obs_properties_add_bool(eps, "saveExternal", "Use external Prepared.dat file ") + obs.obs_property_set_modified_callback(saveExtProp, reLoadPrepared) + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) ------------------ @@ -1863,7 +1873,7 @@ function script_properties() obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) obs.obs_properties_add_button(hotkey_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked) - obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)",obs.OBS_GROUP_NORMAL,hotkey_props) + obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons ",obs.OBS_GROUP_NORMAL,hotkey_props) name_hotkeys() ------ obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) @@ -2371,6 +2381,70 @@ function change_transition_property(props, prop, settings) return true end + +-- reloads prepared songs if source , settings or file, is changed +function reLoadPrepared(props, prop, settings) +dbg_method("reLoad Prepared") + local newSaveExternal = obs.obs_data_get_bool(settings, "saveExternal") + if saveExternal ~= newSaveExternal then + save_prepared(settings) + saveExternal = obs.obs_data_get_bool(settings, "saveExternal") + load_prepared(settings) + end + return true +end + +function load_prepared(settings) +dbg_method("Load Prepared") + if saveExternal then -- loads prepared songs from prepared.dat file + -- load prepared songs from stored file + -- + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") + if file ~= nil then + for line in file:lines() do + prepared_songs[#prepared_songs + 1] = line + end + file:close() + end + else -- loads prepared songs from settings + local prepared_songs_array = obs.obs_data_get_array(settings, "prepared_songs_list") + local count = obs.obs_data_array_count(prepared_songs_array) + if count > 0 then + for i = 0, count do + local item = obs.obs_data_array_item(prepared_songs_array, i) + local songName = obs.obs_data_get_string(item, "value") + if songName ~= "" then + prepared_songs[#prepared_songs + 1] = songName + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(prepared_songs_array) + end +end + +function save_prepared(settings) + if saveExternal then -- saves preprepared songs in prepared.dat file + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") + for i, name in ipairs(prepared_songs) do + -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs + file:write(name, "\n") + -- end + end + file:close() + else -- saves prepared songs in settings array + local prepared_songs_array = obs.obs_data_array_create() + for i, song_name in ipairs(prepared_songs) do + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song_name) + obs.obs_data_array_push_back(prepared_songs_array, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(settings, "prepared_songs_list", prepared_songs_array) + obs.obs_data_array_release(prepared_songs_array) + end +end + -- A function named script_save will be called when the script is saved function script_save(settings) dbg_method("script_save") @@ -2417,6 +2491,8 @@ function script_save(settings) end obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) obs.obs_data_array_release(extra_sources_array) + + save_prepared(settings) end -- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS @@ -2488,30 +2564,21 @@ function script_load(settings) end end obs.obs_data_array_release(extra_sources_array) - - -- load prepared songs from stored file - -- - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - load_source_song_directory(false) - -- load prepared songs from previous - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") - if file ~= nil then - for line in file:lines() do - prepared_songs[#prepared_songs + 1] = line - end - file:close() - end + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + load_source_song_directory(false) + + load_prepared(settings) + obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end -function script_unload() -all_sources_fade = true -text_opacity = 100 -apply_source_opacity() - +function script_unload() -- not sure this is working as expected + all_sources_fade = true + text_opacity = 100 + apply_source_opacity() end diff --git a/lyrics.lua b/lyrics.lua index 523d8c4..3b591dd 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -413,7 +413,7 @@ function prepare_song_clicked(props, p) obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) obs.obs_properties_apply_settings(props, script_sets) - save_prepared() + return true end @@ -508,7 +508,6 @@ function clear_prepared_clicked(props, p) obs.obs_property_list_clear(prep_prop) obs.obs_data_set_string(script_sets, "prop_prepared_list", "") obs.obs_properties_apply_settings(props, script_sets) - save_prepared() return true end @@ -1519,18 +1518,6 @@ function save_song(name, text) return false end --- saves preprepared songs -function save_prepared() - dbg_method("save_prepared") - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") - for i, name in ipairs(prepared_songs) do - -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs - file:write(name, "\n") - -- end - end - file:close() - return true -end function update_monitor() dbg_method("update_monitor") @@ -1785,6 +1772,8 @@ function script_properties() obs.OBS_GROUP_NORMAL, eps ) + local saveExtProp = obs.obs_properties_add_bool(eps, "saveExternal", "Use external Prepared.dat file ") + obs.obs_property_set_modified_callback(saveExtProp, reLoadPrepared) obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) ------------------ @@ -1954,6 +1943,7 @@ function script_update(settings) ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") link_text = obs.obs_data_get_bool(settings, "do_link_text") link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") + saveExternal = obs.obs_data_get_bool(settings, "saveExternal") end -- A function named script_defaults will be called to set the default settings @@ -2277,6 +2267,69 @@ function change_transition_property(props, prop, settings) return true end +-- reloads prepared songs if source , settings or file, is changed +function reLoadPrepared(props, prop, settings) + saveExternal = obs.obs_data_get_bool(settings, "saveExternal") + load_prepared(settings) + return true +end + +function load_prepared(settings) + if saveExternal then -- loads prepared songs from prepared.dat file + -- load prepared songs from stored file + -- + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + load_source_song_directory(false) + -- load prepared songs from previous + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") + if file ~= nil then + for line in file:lines() do + prepared_songs[#prepared_songs + 1] = line + end + file:close() + end + else + local prepared_songs_array = obs.obs_data_get_array(settings, "prepared_songs_list") + local count = obs.obs_data_array_count(prepared_songs_array) + if count > 0 then + for i = 0, count do + local item = obs.obs_data_array_item(prepared_songs_array, i) + local songName = obs.obs_data_get_string(item, "value") + if songName ~= "" then + prepared_songs[#prepared_songs + 1] = songName + end + obs.obs_data_release(item) + end + end + obs.obs_data_array_release(prepared_songs_array) + end +end + +function save_prepared(settings) + if saveExternal then -- saves preprepared songs in prepared.dat file + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") + for i, name in ipairs(prepared_songs) do + -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs + file:write(name, "\n") + -- end + end + file:close() + else -- saves prepared songs in settings array + local prepared_songs_array = obs.obs_data_array_create() + local prepared_songs_list = obs.obs_properties_get(script_props, "prop_prepared_list") + for i, song_name in ipairs(prepared_songs) do + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song_name) + obs.obs_data_array_push_back(prepared_songs_array, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(settings, "Prepared_songs_list", prepared_songs_array) + obs.obs_data_array_release(prepared_songs_array) + end +end + -- A function named script_save will be called when the script is saved function script_save(settings) dbg_method("script_save") @@ -2323,6 +2376,9 @@ function script_save(settings) end obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) obs.obs_data_array_release(extra_sources_array) + + save_prepared(settings) + end -- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS @@ -2376,8 +2432,6 @@ function script_load(settings) script_sets = settings source_name = obs.obs_data_get_string(settings, "prop_source_list") - extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - -- load previously defined extra sources from settings array into table -- script_properties function will take them from the table and restore them as UI properties -- @@ -2395,20 +2449,8 @@ function script_load(settings) end obs.obs_data_array_release(extra_sources_array) - -- load prepared songs from stored file - -- - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - load_source_song_directory(false) - -- load prepared songs from previous - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") - if file ~= nil then - for line in file:lines() do - prepared_songs[#prepared_songs + 1] = line - end - file:close() - end + load_prepared(settings) + name_hotkeys() obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture From 38974550e1476ffdbd791dfddbe4d612d5497311 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 19 Oct 2021 21:49:46 -0600 Subject: [PATCH 062/105] Update lyrics+.lua Disabled Debugging --- lyrics+.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index ca8e907..850da43 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -128,8 +128,8 @@ source_saved = false -- ick... A saved toggle to keep from repeating the save editVisSet = false -- simple debugging/print mechanism -DEBUG = true -- on switch for entire debugging mechanism -DEBUG_METHODS = true -- print method names +--DEBUG = true -- on switch for entire debugging mechanism +--DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false From 1c938f5e4df4c7e06132999d155d9bcf0582cca1 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 19 Oct 2021 22:46:35 -0600 Subject: [PATCH 063/105] More Progressive disclosure Added global on/off for background fades --- lyrics+.lua | 73 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 850da43..d51083e 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -119,6 +119,7 @@ fade_title_back = false fade_alternate_back = false fade_static_back = false fade_extra_back = false +allow_back_fade = false transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) transition_completed = false @@ -1776,9 +1777,9 @@ function script_properties() ----------- obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲", change_info_visible) local gp = obs.obs_properties_create() - obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) - obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) @@ -1802,7 +1803,7 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_directory_list", - "Song Directory", + "Song Directory", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1822,7 +1823,7 @@ function script_properties() obs.obs_properties_add_list( gps, "prop_prepared_list", - "Prepared ", + "Prepared ", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) @@ -1833,7 +1834,7 @@ function script_properties() obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) local count = obs.obs_property_list_item_count(prepare_prop) if count > 0 then - obs.obs_property_set_description( prepare_prop, "Prepared (" .. count .. ")") + obs.obs_property_set_description( prepare_prop, "Prepared (" .. count .. ")") end obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) @@ -1860,7 +1861,7 @@ function script_properties() local saveExtProp = obs.obs_properties_add_bool(eps, "saveExternal", "Use external Prepared.dat file ") obs.obs_property_set_modified_callback(saveExtProp, reLoadPrepared) - obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) @@ -1878,7 +1879,7 @@ function script_properties() ------ obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) gp = obs.obs_properties_create() - local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "Lines to Display", 1, 50, 1) + local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "Lines to Display", 1, 50, 1) obs.obs_property_set_long_description( lines_prop, "Sets default lines per page of lyric, overwritten by Markup: #L:n" @@ -1893,13 +1894,15 @@ function script_properties() obs.obs_property_set_modified_callback(transition_prop, change_transition_property) obs.obs_property_set_long_description( transition_prop, - "Use with Studio Mode, duplicate sources, and OBS source transitions" + "Use with Studio Mode, duplicate sources, and OBS source transitions (beta)" ) - local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) + local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable Fade Transitions") -- Fade Enable (WZ) obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - local fp1 = obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + local fp1 = obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) local fp2 = obs.obs_properties_add_bool(gp,"use100percent", "Use 0-100% opacity for fades") + local fp3 = obs.obs_properties_add_bool(gp,"allowBackFade", "Enable Background Fading") obs.obs_property_set_modified_callback(fp2, change_100percent_property) + obs.obs_property_set_modified_callback(fp3, change_back_fade_property) local oprefprop = obs.obs_properties_add_button(gp, "refreshOP", "Mark Max Opacity for Source Fades", read_source_opacity_clicked) obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) ------------- @@ -1909,7 +1912,7 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_source_list", - "Text Source", + "Text Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1918,7 +1921,7 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_title_list", - "Title Source", + "Title Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1927,7 +1930,7 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_alternate_list", - "Alternate Source", + "Alternate Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1936,7 +1939,7 @@ function script_properties() obs.obs_properties_add_list( gp, "prop_static_list", - "Static Source", + "Static Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -2018,6 +2021,7 @@ function script_properties() obs.obs_property_set_visible(meta_group_prop, false) obs.obs_property_set_visible(fp1, text_fade_enabled) obs.obs_property_set_visible(fp2, text_fade_enabled) + obs.obs_property_set_visible(fp3, text_fade_enabled) obs.obs_property_set_visible(flbprop, text_fade_enabled) obs.obs_property_set_visible(ftbprop, text_fade_enabled) obs.obs_property_set_visible(fabprop, text_fade_enabled) @@ -2042,11 +2046,13 @@ function script_update(settings) link_text = obs.obs_data_get_bool(settings, "do_link_text") link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") use100percent = obs.obs_data_get_bool(settings, "use100percent") - fade_text_back = obs.obs_data_get_bool(settings, "fade_text_back") - fade_title_back = obs.obs_data_get_bool(settings, "fade_title_back") - fade_alternate_back = obs.obs_data_get_bool(settings, "fade_alternate_back") - fade_static_back = obs.obs_data_get_bool(settings, "fade_static_back") - fade_extra_back = obs.obs_data_get_bool(settings, "fade_extra_back") + allow_back_fade = obs.obs_data_get_bool(settings, "allowBackFade") + fade_text_back = obs.obs_data_get_bool(settings, "fade_text_back") and allow_back_fade + fade_title_back = obs.obs_data_get_bool(settings, "fade_title_back") and allow_back_fade + fade_alternate_back = obs.obs_data_get_bool(settings, "fade_alternate_back") and allow_back_fade + fade_static_back = obs.obs_data_get_bool(settings, "fade_static_back") and allow_back_fade + fade_extra_back = obs.obs_data_get_bool(settings, "fade_extra_back") and allow_back_fade + end -- A function named script_defaults will be called to set the default settings @@ -2245,11 +2251,12 @@ function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) obs.obs_property_set_visible(obs.obs_properties_get(props, "use100percent"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "allowBackFade"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_set and allow_back_fade) obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), text_fade_enabled and not use100percent) local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) @@ -2262,6 +2269,24 @@ function change_100percent_property(props, prop, settings) return true end +function change_back_fade_property(props, prop, settings) + allow_back_fade = obs.obs_data_get_bool(settings, "allowBackFade") + if allow_back_fade then + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_enabled) + else + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), false) + end + return true +end + function show_help_button(props, prop, settings) dbg_method("show help") local hb = obs.obs_properties_get(props, "show_help_button") From e0b847ea61778410e15c032c8b68c9071a69601f Mon Sep 17 00:00:00 2001 From: amirchev Date: Thu, 21 Oct 2021 12:47:42 -0700 Subject: [PATCH 064/105] documentation --- lyrics.lua | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/lyrics.lua b/lyrics.lua index 124409d..42a6e01 100644 --- a/lyrics.lua +++ b/lyrics.lua @@ -359,6 +359,8 @@ function save_song_clicked(props, p) return true end +-- callback for the delete song button +-- deletes the selected song and updates the UI function delete_song_clicked(props, p) dbg_method("delete_song_clicked") -- call delete song function @@ -511,6 +513,7 @@ function clear_prepared_clicked(props, p) return true end +-- prepares the song with the title {name} function prepare_selected(name) dbg_method("prepare_selected") -- try to prepare song @@ -556,6 +559,8 @@ function preview_selection_made(props, prop, settings) return true end +-- callback for when open song in editor button is clicked +-- opens the song in the native text editor function open_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_directory_list") if testValid(name) then @@ -571,6 +576,8 @@ function open_song_clicked(props, p) return true end +-- callback for when open songs folder button is clicked +-- opens the folder containing files of all the saved songs function open_button_clicked(props, p) local path = get_songs_folder_path() if windows_os then @@ -580,12 +587,7 @@ function open_button_clicked(props, p) end end --------- ----------------- ------------------------- PROGRAM FUNCTIONS ----------------- --------- - +-- applies current source opacity to the necessary sources function apply_source_opacity() -- dbg_method("apply_source_visiblity") @@ -675,6 +677,8 @@ function apply_source_opacity() end end +-- changes the visibility of the text; called EVERY time text is to be +-- hidden or made visible; not called during transition function set_text_visibility(end_status) dbg_method("set_text_visibility") -- if already at desired visibility, then exit @@ -719,7 +723,7 @@ function set_text_visibility(end_status) end -- transition to the next lyrics, use fade if enabled --- if lyrics are hidden, force_show set to true will make them visible +-- if lyrics are hidden, force_show set to trued will make them visible function transition_lyric_text(force_show) dbg_method("transition_lyric_text") dbg_bool("force show", force_show) @@ -870,11 +874,13 @@ function update_source_text() update_monitor() end +-- starts the fade timer function start_fade_timer() dbgsp("started fade timer") obs.timer_add(fade_callback, 50) end +-- function is called by the fade timer to increment/decrement opacity value manually function fade_callback() -- if not in a transitory state, exit callback if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then @@ -1483,6 +1489,7 @@ function dec(data) )) end +-- checks to see if {filename} is a valid name for a file function testValid(filename) if string.find(filename, "[\128-\255]") ~= nil then return false @@ -1518,7 +1525,7 @@ function save_song(name, text) return false end - +-- updates the HTML monitor file with current status information function update_monitor() dbg_method("update_monitor") local tableback = "black" @@ -1675,9 +1682,6 @@ end -- -------------- -------- --- A function named script_properties defines the properties that the user --- can change for the entire script module itself - local help = "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. " Markup      Syntax         Markup      Syntax \n" .. @@ -1691,7 +1695,9 @@ local help = "Comment Line    // Line       Block Comments    //[ and //] \n" .. "Mark Verses     ##V        Override Title     #T: text\n\n" .. "Optional comma delimited meta tags follow '//meta ' on 1st line" - + +-- A function named script_properties defines the properties that the user +-- can change for the entire script module itself function script_properties() dbg_method("script_properties") editVisSet = false @@ -2278,6 +2284,7 @@ function reLoadPrepared(props, prop, settings) return true end +-- loads prepared songs from external file or internal settings array function load_prepared(settings) if saveExternal then -- loads prepared songs from prepared.dat file -- load prepared songs from stored file @@ -2311,6 +2318,9 @@ function load_prepared(settings) end end +-- saves prepared files for use next time OBS is opened +-- can save into an external file called "Prepared.dat" in the songs folder +-- or into internal settings array function save_prepared(settings) if saveExternal then -- saves preprepared songs in prepared.dat file local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") @@ -2631,7 +2641,6 @@ end -------- -- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change --- function rename_source() -- pause_timer = true local sources = obs.obs_enum_sources() @@ -2709,7 +2718,6 @@ function rename_source() end -- Names the initial "Prepare Lyric" source (prior to being renamed to "Load Lyrics for: {song name} --- source_def.get_name = function() return "Prepare Lyric" end From f864aa75068ae7f6b3256d292ce242c24318d534 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 21 Oct 2021 15:12:33 -0600 Subject: [PATCH 065/105] Update README.md --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 40307bf..74e5333 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ Try it: ``` ### Define refrain and show it right away (`#R[` and `#R]`) Use this notation to define a refrain that will be displayed right away as well. +### Define refrain but DON'T show it right away (`#r[` and `#r]`) +Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. +### Play refrain (`##R`) +Use this annotation to show where a refrain should be inserted. See above. Try it: ``` #R[ optional comment @@ -106,11 +110,6 @@ it will also continue with three lines per verse. Now hit the refrain again! ##R ``` -### Play refrain (`##R`) -Use this annotation to show where a refrain should be inserted. See above. -### Define refrain but DON'T show it right away (`#r[` and `#r]`) -Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. - ### Static Text (`#S[` and `#S]`) Use this anotation to define a block of text lines shown in the selected Static Source that remain constant during the scene (no paging). Try it: @@ -126,6 +125,12 @@ Try it: ``` #S: The song Amazing Grace was written by John Newton who was a former Slave Trader ``` +### Override Title/filename (`#T: new title`) +Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename +Try it: +``` +#T: How Great Thou Art (주하나님지으신모든세계) +``` ### Alternate Text Block (`#A[` and `#A]`) Use this annotation to mark additional verses or text to show and page in the selected Alternate Source. Note: The page length will be governed by text in the main block if it exists and its Text Source exists in the scene. From 15bf249163d14a386d89b865d0c3a9090f353828 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:54:34 -0600 Subject: [PATCH 066/105] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 74e5333..ae71728 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,21 @@ Manage and display lyrics to any text source in your OBS scene. 6. Advance lyrics as needed using the buttons or appropriate hotkeys. You can also advance to the next prepared song using hotkeys. 7. When you're finished with the current song, hide the lyrics and select the next song from the "Prepared Songs" list. -There is a much more in-depth guide [here](https://obsproject.com/forum/resources/display-lyrics-as-subtitles.1005/). - ## Things to know - To display a specific song when a scene is activated, add a "Source" to the scene by clicking the + sign in the scene, adding a "Prepare Lyric" source, and selecting the song to open. - Use "Home" hotkey to return to the beginning of your prepared songs, perhaps after practicing the songs. - Continue clicking `Advance lyrics` after the end of a song to begin the next prepared song. - Ensure a constant number of lines displayed using the checkbox, e.g., if the song ends and only one line is left, lyrics will be padded with blank lines to ensure you hava a minimum number of lines. - A Monitor.htm file is created with current/next song, lyrics and alternate lyrics that can be docked in OBS with custom browser docks. Use Open Songs Folder button, open Monitor.htm with browser, copy url and paste it into an OBS custom browser dock. +- Prepared songs are stored in the Settings for the scene collection unless the option to use an external Prepared.dat file is selected in the Edit Prepared Songs subgroup. ## Notation +### Mark songs with with 'meta' tags for filtering on future selection (`//meta *tag1*, *tag2*, ... , *tag n*`) +Using //meta tags on the __1st line__ of lyrics allows song files to be labeled as belonging to different genre. Example genre are Hymn, Contemporary, Gospel, Country, Blues, Spritual, Rock, Chant, Reggae, Metal, or HipHop. However, any tag can be used to organize and cross organize Lyric/Text files into categories. Other meta groups could be Call/Response or Scripture. Meta tags must match exactly, so the tag __*hymn*__ is different from the tag __*Hymn*__ +Try it: +``` +//meta Hymn, Blues, Spiritual +``` ### Single blank line/padding (`##P` or `##B`) Use on any line that you want to keep as an empty line (for line padding, etc.) Try it: From 231276c2296224b812490d49df923e1cf1e95dd7 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:55:38 -0600 Subject: [PATCH 067/105] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae71728..c9d3f87 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Manage and display lyrics to any text source in your OBS scene. - Prepared songs are stored in the Settings for the scene collection unless the option to use an external Prepared.dat file is selected in the Edit Prepared Songs subgroup. ## Notation -### Mark songs with with 'meta' tags for filtering on future selection (`//meta *tag1*, *tag2*, ... , *tag n*`) -Using //meta tags on the __1st line__ of lyrics allows song files to be labeled as belonging to different genre. Example genre are Hymn, Contemporary, Gospel, Country, Blues, Spritual, Rock, Chant, Reggae, Metal, or HipHop. However, any tag can be used to organize and cross organize Lyric/Text files into categories. Other meta groups could be Call/Response or Scripture. Meta tags must match exactly, so the tag __*hymn*__ is different from the tag __*Hymn*__ +### Mark songs with with 'meta' tags for filtering on future selection (`//meta tag1, tag2, ... , tag n`) +Using //meta tags on the __1st line__ of lyrics allows song files to be labeled as belonging to different genre. Example genre are Hymn, Contemporary, Gospel, Country, Blues, Spritual, Rock, Chant, Reggae, Metal, or HipHop. However, any tag can be used to organize and cross organize Lyric/Text files into categories. Other meta groups could be Call/Response or Scripture. Meta tags must match exactly, so the tag __*hymn*__ is different from the tag __*Hymn*__. Try it: ``` //meta Hymn, Blues, Spiritual From a749d28b9303b6d9c415cf3eb19baf6757daffb2 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Fri, 22 Oct 2021 01:26:42 -0600 Subject: [PATCH 068/105] Updated Prepared lyrics to always have a blank first line. It seems that the prepared list should have a blank first line so that a first song can be selected manually if desired. Some other changes are just cosmetic. Readme has some screen captures. --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++------ lyrics+.lua | 61 +++++++++++++++++++-------------- 2 files changed, 122 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 40307bf..b523624 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,21 @@ Manage and display lyrics to any text source in your OBS scene. 6. Advance lyrics as needed using the buttons or appropriate hotkeys. You can also advance to the next prepared song using hotkeys. 7. When you're finished with the current song, hide the lyrics and select the next song from the "Prepared Songs" list. -There is a much more in-depth guide [here](https://obsproject.com/forum/resources/display-lyrics-as-subtitles.1005/). - ## Things to know - To display a specific song when a scene is activated, add a "Source" to the scene by clicking the + sign in the scene, adding a "Prepare Lyric" source, and selecting the song to open. - Use "Home" hotkey to return to the beginning of your prepared songs, perhaps after practicing the songs. - Continue clicking `Advance lyrics` after the end of a song to begin the next prepared song. -- Ensure a constant number of lines displayed using the checkbox, e.g., if the song ends and only one line is left, lyrics will be padded with blank lines to ensure you hava a minimum number of lines. +- Ensure a constant number of lines displayed using the checkbox, e.g., if the song ends and only one line is left, lyrics will be padded with blank lines to ensure you hava a minimum number of lines. (See Display Options) - A Monitor.htm file is created with current/next song, lyrics and alternate lyrics that can be docked in OBS with custom browser docks. Use Open Songs Folder button, open Monitor.htm with browser, copy url and paste it into an OBS custom browser dock. +- Prepared songs are stored in the Settings for the scene collection unless the option to use an external Prepared.dat file is selected in the Edit Prepared Songs subgroup. ## Notation +### Mark songs with with 'meta' tags for filtering on future selection (`//meta tag1, tag2, ... , tag n`) +Using //meta tags on the __1st line__ of lyrics allows song files to be labeled as belonging to different genre. Example genre are Hymn, Contemporary, Gospel, Country, Blues, Spritual, Rock, Chant, Reggae, Metal, or HipHop. However, any tag can be used to organize and cross organize Lyric/Text files into categories. Other meta groups could be Call/Response or Scripture. Meta tags must match exactly, so the tag __*hymn*__ is different from the tag __*Hymn*__. +Try it: +``` +//meta Hymn, Blues, Spiritual +``` ### Single blank line/padding (`##P` or `##B`) Use on any line that you want to keep as an empty line (for line padding, etc.) Try it: @@ -89,6 +94,10 @@ Try it: ``` ### Define refrain and show it right away (`#R[` and `#R]`) Use this notation to define a refrain that will be displayed right away as well. +### Define refrain but DON'T show it right away (`#r[` and `#r]`) +Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. +### Play refrain (`##R`) +Use this annotation to show where a refrain should be inserted. See above. Try it: ``` #R[ optional comment @@ -106,11 +115,6 @@ it will also continue with three lines per verse. Now hit the refrain again! ##R ``` -### Play refrain (`##R`) -Use this annotation to show where a refrain should be inserted. See above. -### Define refrain but DON'T show it right away (`#r[` and `#r]`) -Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. - ### Static Text (`#S[` and `#S]`) Use this anotation to define a block of text lines shown in the selected Static Source that remain constant during the scene (no paging). Try it: @@ -126,6 +130,12 @@ Try it: ``` #S: The song Amazing Grace was written by John Newton who was a former Slave Trader ``` +### Override Title/filename (`#T: new title`) +Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename +Try it: +``` +#T: How Great Thou Art (주하나님지으신모든세계) +``` ### Alternate Text Block (`#A[` and `#A]`) Use this annotation to mark additional verses or text to show and page in the selected Alternate Source. Note: The page length will be governed by text in the main block if it exists and its Text Source exists in the scene. @@ -141,17 +151,82 @@ Que salvo a un desgraciado como yo Alguna vez estuve perdido, pero ahora me he encontrado Estuve ciego pero ahora veo #A] -``` + ``` ### Single Line Alternate Text repeated for n pages (`#A:n line`) Use this annotation to include a simple single line of Alternate Text to be used for n pages. Try it: ``` #A:2 This alaternate line shows for the next two pages of Lyrics. ``` +### Mark Verses (`##V`) + +Use this annotation to mark where new verses start. Verse number will be displayed in the monitor. +Try it: + +``` +#R[ optional comment +#L:2 +This song starts with this refrain! +It will only show these two lines!!! +#R] optional comment +#L:3 +##V +Now the verse begins, +after the refrain. +And all three lines will show! +##R +##V +Now the second verse begins, +it will also continue with three lines per verse. +Now hit the refrain again! +##R +``` + +### Song Title (filename) and Lyrics Information + +![image-20211022011756528](C:\Users\willi\AppData\Roaming\Typora\typora-user-images\image-20211022011756528.png) + +The song Title is also used as a filename to store the lyrics. If the text of the title is not a valid OS filename then the filename will be encoded to create a valid filename. Alternately providing a valid filename for this field, the actual Title can be included using the ##T markup. Song lyrics can be added in the dialog, saved, and deleted. Songs can also be opened with the default system text editor. + +### Manage Prepared Songs/Text + +![image-20211022010733511](C:\Users\willi\AppData\Roaming\Typora\typora-user-images\image-20211022010733511.png) + +Songs saved in the Song Title and Lyrics Information can be selected in the Manage Prepared section to be added to the Prepared Songs/Text list. Selecting a song from this Prepared List loads the contents of the Song/Text into the selected Text Sources. If songs are marked with //meta tags, they can be filtered by specifying one or more tags and refreshing the directory. Prepared songs can be edited as a list where they can be individually ordered or deleted. *(New songs can be typed into the edit list manually if they exist in the directory exactly as typed)* + +### Lyric Control Buttons + +Control Buttons perform the seven different functions of the Lyrics Script. Additionally, Hot Keys can be assigned within OBS to perform these same functions. + +![img](file:///C:/Users/willi/AppData/Local/Temp/SNAGHTML11109d6a.PNG) + +### Display Options + +![image-20211021232744449](C:\Users\willi\AppData\Roaming\Typora\typora-user-images\image-20211021232744449.png) + +Enabling Fade Transitions will offer additional options to cause lyrics and other sources to fade to transparent before changing to a different page and fading back to opaque. The Use 0-100% option is set by default. Unchecking this option will cause Lyrics to restore faded sources back to their "marked" original opacity levels if specific graphic effects have been applied to text. Background color fading is optional and can be further configured per text source if enabled. + +### Text Sources in Scenes + +![image-20211021234545740](C:\Users\willi\AppData\Roaming\Typora\typora-user-images\image-20211021234545740.png) + +Lyrics will modify the text content of existing text sources within OBS and a given scene. These Text, Title, Alternate and Static text sources are defined in the Text Sources in Scenes section. New text sources added to OBS while the script properties window is open, can be included by clicking the Refresh All Sources button. Additional visual sources can be added and linked to show/hide/fade with the Title and Static text sources if desired, such as with a background image for Lyrics, etc. Optionally, these sources can be faded with the Lyrics and Alternate text. + ### Lyrics Monitor Browser Dock -A Lyrics Monitor Page updated in HTML is available in the Songs Folder as Monitor.htm. Press the Open Songs Folder to find the file and open it in a browser. It is also possible to add this url as a dockable window in OBS/View/Docks/Custom Browser Docks. The page shows Prepared Song x of n, Lyric Page x of n, Scene if current lyric is loaded from a source, The Song Title, Current Lyrics Page, Next Lyrics Page, Current Alternate Lyrics Page, Next Alternate Lyrics Page, and the Next Prepared Song. -Note: Lyrics loaded by a source in a scene are always prepared to the first prepared lyric location, and existing prepared lyrics are shifted up. Scene prepared lyrics are NOT saved in the prepared lyrics list. +![image-20211021235826735](C:\Users\willi\AppData\Roaming\Typora\typora-user-images\image-20211021235826735.png) + +A Lyrics Monitor Page updated in HTML is available in the Songs Folder as Monitor.htm. Press the Open Songs Folder to find the file and open it in a browser. It is also possible to add this url as a dockable window in OBS/View/Docks/Custom Browser Docks. The page shows: + +- Prepared Song x of n (or Scene if current lyric is loaded from a source) +- Lyric Page x of n +- Current Verse if marked in Lyrics +- The Song Title, Current Lyrics Page Text, Next Lyrics Page Text +- Current Alternate Lyrics Page if marked in song/text file +- Next Alternate Lyrics Page Text if marked in song/text file +- The Next Prepared Song/Text file + + Note: Red backgrounds in the Monitor Page indicate lyrics are not currently visible, or the selected text sources do not exist in the current Active scene. ## That's it Please post any bugs or feature requests here or to the OBS forum. diff --git a/lyrics+.lua b/lyrics+.lua index d51083e..e0582a6 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -420,13 +420,14 @@ function prepare_song_clicked(props, p) local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") if #prepared_songs > 0 then - obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") + obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") else - obs.obs_property_set_description(prop_prep_list, "Prepared") + obs.obs_property_set_description(prop_prep_list, "Prepared") end obs.obs_properties_apply_settings(props, script_sets) + save_prepared(script_sets) return true end @@ -517,10 +518,15 @@ function clear_prepared_clicked(props, p) -- clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) - obs.obs_property_set_description(obs.obs_properties_get(props, "prop_prepared_list"), "Prepared") obs.obs_property_list_add_string(obs.obs_properties_get(props, "prop_prepared_list"), "", "") - --s.obs_properties_apply_settings(props, script_sets) + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + local pp = obs.obs_properties_get(props, "edit_grp") + if obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared List") + end return true end @@ -1815,7 +1821,7 @@ function script_properties() obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() @@ -1827,14 +1833,15 @@ function script_properties() obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + obs.obs_property_list_add_string(prepare_prop, "", "") for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) local count = obs.obs_property_list_item_count(prepare_prop) - if count > 0 then - obs.obs_property_set_description( prepare_prop, "Prepared (" .. count .. ")") + if count > 1 then + obs.obs_property_set_description( prepare_prop, "Prepared (" .. count-1 .. ")") end obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) @@ -1843,7 +1850,7 @@ function script_properties() obs.obs_properties_add_editable_list( eps, "prep_list", - "Prepared Songs/Text", + "Prepared Songs/Text", obs.OBS_EDITABLE_LIST_TYPE_STRINGS, nil, nil @@ -1861,7 +1868,7 @@ function script_properties() local saveExtProp = obs.obs_properties_add_bool(eps, "saveExternal", "Use external Prepared.dat file ") obs.obs_property_set_modified_callback(saveExtProp, reLoadPrepared) - obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) ------------------ obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) @@ -1896,7 +1903,8 @@ function script_properties() transition_prop, "Use with Studio Mode, duplicate sources, and OBS source transitions (beta)" ) - local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable Fade Transitions") -- Fade Enable (WZ) + + local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable Fade Transitions") obs.obs_property_set_modified_callback(fade_prop, change_fade_property) local fp1 = obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) local fp2 = obs.obs_properties_add_bool(gp,"use100percent", "Use 0-100% opacity for fades") @@ -1905,9 +1913,11 @@ function script_properties() obs.obs_property_set_modified_callback(fp3, change_back_fade_property) local oprefprop = obs.obs_properties_add_button(gp, "refreshOP", "Mark Max Opacity for Source Fades", read_source_opacity_clicked) obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) + ------------- obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) gp = obs.obs_properties_create() + local source_prop = obs.obs_properties_add_list( gp, @@ -1952,7 +1962,7 @@ function script_properties() obs.obs_properties_add_list( xgp, "extra_linked_list", - "Linked Sources", + "Linked Sources", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -1964,7 +1974,7 @@ function script_properties() obs.obs_properties_add_list( xgp, "extra_source_list", - " Select Source:", + " Select Source:", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) @@ -2094,7 +2104,7 @@ function link_source_selected(props, prop, settings) obs.obs_data_set_string(script_sets, "extra_source_list", "") obs.obs_property_set_description( extra_linked_list, - "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" + "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" ) end return true @@ -2118,7 +2128,7 @@ function clear_linked_clicked(props, p) obs.obs_property_list_clear(extra_linked_list) obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) - obs.obs_property_set_description(extra_linked_list, "Linked Sources") + obs.obs_property_set_description(extra_linked_list, "Linked Sources") return true end @@ -2345,8 +2355,7 @@ function edit_prepared_clicked(props, p) obs.obs_data_array_erase(songNames, 0) end end - - for i = 0, count - 1 do + for i = 1, count do local song = obs.obs_property_list_item_string(prop_prep_list, i) local array_obj = obs.obs_data_create() obs.obs_data_set_string(array_obj, "value", song) @@ -2367,6 +2376,7 @@ function save_edits_clicked(props, p) prepared_songs = {} local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prop_prep_list) + obs.obs_property_list_add_string(prop_prep_list, "", "") local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) if count2 > 0 then @@ -2382,17 +2392,18 @@ function save_edits_clicked(props, p) end obs.obs_data_array_release(songNames) save_prepared() - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) - prepared_index = 1 - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - prepared_index = 0 - end + obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + prepared_index = 0 pp = obs.obs_properties_get(script_props, "edit_grp") obs.obs_property_set_visible(pp, false) local mpb = obs.obs_properties_get(props, "prop_manage_button") obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + + if #prepared_songs > 0 then + obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") + else + obs.obs_property_set_description(prop_prep_list, "Prepared") + end obs.obs_properties_apply_settings(props, script_sets) return true end From e77b94045cf4ad8218783464f0272f49610f6ed4 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Fri, 22 Oct 2021 01:43:08 -0600 Subject: [PATCH 069/105] Working on Readme markdown with image captures Working on Readme markdown with image captures --- LyricsTrial.lua | 2998 ------------------------------ README.md | 2 +- images/Display Options.gif | Bin 0 -> 8310 bytes images/Lyric Control Buttons.gif | Bin 0 -> 7073 bytes images/Manage Prepared.gif | Bin 0 -> 15567 bytes images/Text Sources.gif | Bin 0 -> 8657 bytes images/Title Lyrics.gif | Bin 0 -> 8705 bytes images/monitor.gif | Bin 0 -> 13392 bytes lyrics.lua | 2984 ----------------------------- 9 files changed, 1 insertion(+), 5983 deletions(-) delete mode 100644 LyricsTrial.lua create mode 100644 images/Display Options.gif create mode 100644 images/Lyric Control Buttons.gif create mode 100644 images/Manage Prepared.gif create mode 100644 images/Text Sources.gif create mode 100644 images/Title Lyrics.gif create mode 100644 images/monitor.gif delete mode 100644 lyrics.lua diff --git a/LyricsTrial.lua b/LyricsTrial.lua deleted file mode 100644 index 064f820..0000000 --- a/LyricsTrial.lua +++ /dev/null @@ -1,2998 +0,0 @@ ---- Copyright 2020 amirchev/wzaggle - --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at - --- http://www.apache.org/licenses/LICENSE-2.0 - --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -obs = obslua -bit = require("bit") - --- source definitions -source_data = {} -source_def = {} -source_def.id = "Prepare_Lyrics" -source_def.type = OBS_SOURCE_TYPE_INPUT -source_def.output_flags = bit.bor(obs.OBS_SOURCE_CUSTOM_DRAW) - --- text sources -source_name = "" -alternate_source_name = "" -static_source_name = "" -static_text = "" -title_source_name = "" - --- settings -windows_os = false -first_open = true - -display_lines = 0 -ensure_lines = true - --- lyrics/alternate lyrics by page -lyrics = {} -alternate = {} - --- verse indicies if marked -verses = {} - -page_index = 0 -- current page of lyrics being displayed -prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected - -song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) -prepared_songs = {} -- holds pre-prepared list of songs to use -extra_sources = {} -- holder for extra sources settings -max_opacity = {} -- record maximum opacity settings for sources - -link_text = false -- true if Title and Static should fade with text only during hide/show -link_extras = false -- extras fade with text always when true, only during hide/show when false -all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide -source_song_title = "" -- The song title from a source loaded song -using_source = false -- true when a lyric load song is being used instead of a pre-prepared song -source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) - -load_scene = "" -- name of scene loading a lyric with a source -last_prepared_song = "" -- name of the last prepared song (prevents duplicate loading of already loaded song) - --- hotkeys -hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID -hotkey_p_id = obs.OBS_INVALID_HOTKEY_ID -hotkey_c_id = obs.OBS_INVALID_HOTKEY_ID -hotkey_n_p_id = obs.OBS_INVALID_HOTKEY_ID -hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID -hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID -hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID - -hotkey_n_key = "" -hotkey_p_key = "" -hotkey_c_key = "" -hotkey_n_p_key = "" -hotkey_p_p_key = "" -hotkey_home_key = "" -hotkey_reset_key = "" - --- script placeholders -script_sets = nil -script_props = nil -source_sets = nil -source_props = nil -hotkey_props = nil - ---monitor variables -mon_song = "" -mon_lyric = "" -mon_verse = 0 -mon_nextlyric = "" -mon_alt = "" -mon_nextalt = "" -mon_nextsong = "" -meta_tags = "" -source_meta_tags = "" - --- text status & fade -TEXT_VISIBLE = 0 -- text is visible -TEXT_HIDDEN = 1 -- text is hidden -TEXT_SHOWING = 3 -- going from hidden -> visible -TEXT_HIDING = 4 -- going from visible -> hidden -TEXT_TRANSITION_OUT = 5 -- fade out transition to next lyric -TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change -TEXT_HIDE = 7 -- turn off the text and ignore fade if selected -TEXT_SHOW = 8 -- turn on the text and ignore fade if selected - -text_status = TEXT_VISIBLE -text_opacity = 100 -text_fade_speed = 1 -text_fade_enabled = false -load_source = nil -expandcollapse = true -showhelp = false -use100percent = false - -transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) -transition_completed = false - -source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now - -editVisSet = false - --- simple debugging/print mechanism ---DEBUG = true -- on switch for entire debugging mechanism ---DEBUG_METHODS = true -- print method names ---DEBUG_INNER = true -- print inner method breakpoints ---DEBUG_CUSTOM = true -- print custom debugging messages ---DEBUG_BOOL = true -- print message with bool state true/false - --------- ----------------- ------------------------- CALLBACKS ----------------- --------- - -function next_lyric(pressed) - if not pressed then - return - end - dbg_method("next_lyric") - -- check if transition enabled - if transition_enabled and not transition_completed then - obs.obs_frontend_preview_program_trigger_transition() - transition_completed = true - return - end - dbg_inner("next page") - if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing - if page_index < #lyrics then - page_index = page_index + 1 - dbg_inner("page_index: " .. page_index) - transition_lyric_text(false) - else - next_prepared(true) - end - end -end - -function prev_lyric(pressed) - if not pressed then - return - end - dbg_method("prev_lyric") - if (#lyrics > 0 or #alternate > 0) and sourceShowing() then -- only change if defined and showing - if page_index > 1 then - page_index = page_index - 1 - dbg_inner("page_index: " .. page_index) - transition_lyric_text(false) - else - prev_prepared(true) - end - end -end - -function prev_prepared(pressed) - if not pressed then - return - end - if #prepared_songs == 0 then - return - end - if using_source then - using_source = false - prepare_selected(prepared_songs[prepared_index]) - return - end - if prepared_index > 1 then - using_source = false - prepare_selected(prepared_songs[prepared_index - 1]) - return - end - if not source_active or using_source then - using_source = false - prepare_selected(prepared_songs[#prepared_songs]) -- cycle through prepared - else - using_source = true - prepared_index = #prepared_songs -- wrap prepared index to end so ready if leaving load source - load_source_song(load_source, false) - end -end - -function next_prepared(pressed) - if not pressed then - return - end - if #prepared_songs == 0 then - return - end - if using_source then - using_source = false - dbg_custom("do current prepared") - prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song - return - end - if prepared_index < #prepared_songs then - using_source = false - dbg_custom("do next prepared") - prepare_selected(prepared_songs[prepared_index + 1]) -- if prepared then goto next prepared - return - end - if not source_active or using_source then - using_source = false - dbg_custom("do first prepared") - prepare_selected(prepared_songs[1]) -- at the end so go back to start if no source load available - else - using_source = true - dbg_custom("do source prepared") - prepared_index = 1 -- wrap prepared index to beginning so ready if leaving load source - load_source_song(load_source, false) - end -end - -function toggle_lyrics_visibility(pressed) - dbg_method("toggle_lyrics_visibility") - if not pressed then - return - end - if link_text then - all_sources_fade = true - end - if text_status ~= TEXT_HIDDEN then - dbg_inner("hiding") - set_text_visibility(TEXT_HIDDEN) - else - dbg_inner("showing") - set_text_visibility(TEXT_VISIBLE) - end -end - -function get_load_lyric_song() - local scene = obs.obs_frontend_get_current_scene() - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - local song = nil - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id - if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - song = obs.obs_data_get_string(settings, "song") -- Get index for this source (set earlier) - obs.obs_data_release(settings) -- release memory - end - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - return song -end - -function home_prepared(pressed) - if not pressed then - return false - end - dbg_method("home_prepared") - using_source = false - page_index = 0 - - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - end - obs.obs_properties_apply_settings(props, script_sets) - prepared_index = 1 - prepare_selected(prepared_songs[prepared_index]) - return true -end - -function home_song(pressed) - if not pressed then - return false - end - dbg_method("home_song") - page_index = 1 - transition_lyric_text(false) - return true -end - -function get_current_scene_name() - dbg_method("get_current_scene_name") - local scene = obs.obs_frontend_get_current_scene() - local current_scene = obs.obs_source_get_name(scene) - obs.obs_source_release(scene) - if current_scene ~= nil then - return current_scene - else - return "-" - end -end - -function next_button_clicked(props, p) - next_lyric(true) - return true -end - -function prev_button_clicked(props, p) - prev_lyric(true) - return true -end - -function toggle_button_clicked(props, p) - toggle_lyrics_visibility(true) - return true -end - -function home_button_clicked(props, p) - home_song(true) - return true -end - -function reset_button_clicked(props, p) - home_prepared(true) - return true -end -function prev_prepared_clicked(props, p) - prev_prepared(true) - return true -end - -function next_prepared_clicked(props, p) - next_prepared(true) - return true -end - -function save_song_clicked(props, p) - local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") - local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") - -- if this is a new song, add it to the directory - if save_song(name, text) then - local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") - obs.obs_property_list_add_string(prop_dir_list, name, name) - obs.obs_data_set_string(script_sets, "prop_directory_list", name) - obs.obs_properties_apply_settings(props, script_sets) - elseif prepared_songs[prepared_index] == name then - -- if this song is being displayed, then prepare it anew - prepare_song_by_name(name) - transition_lyric_text(false) - end - return true -end - -function delete_song_clicked(props, p) - dbg_method("delete_song_clicked") - -- call delete song function - local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - delete_song(name) - -- update - local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") - for i = 0, obs.obs_property_list_item_count(prop_dir_list) do - if obs.obs_property_list_item_string(prop_dir_list, i) == name then - obs.obs_property_list_item_remove(prop_dir_list, i) - if i > 1 then - i = i - 1 - end - if #song_directory > 0 then - obs.obs_data_set_string(script_sets, "prop_directory_list", song_directory[i]) - else - obs.obs_data_set_string(script_sets, "prop_directory_list", "") - obs.obs_data_set_string(script_sets, "prop_edit_song_title", "") - obs.obs_data_set_string(script_sets, "prop_edit_song_text", "") - end - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - if get_index_in_list(prepared_songs, name) ~= nil then - if obs.obs_property_list_item_string(prop_prep_list, i) == name then - obs.obs_property_list_item_remove(prop_prep_list, i) - if i > 1 then - i = i - 1 - end - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[i]) - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - end - end - end - obs.obs_properties_apply_settings(props, script_sets) - return true - end - end - return true -end - --- prepare song button clicked -function prepare_song_clicked(props, p) - dbg_method("prepare_song_clicked") - if #prepared_songs == 0 then - set_text_visibility(TEXT_HIDDEN) - end - prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) - - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - - obs.obs_properties_apply_settings(props, script_sets) - save_prepared() - return true -end - -function refresh_button_clicked(props, p) - local source_prop = obs.obs_properties_get(props, "prop_source_list") - local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") - local static_source_prop = obs.obs_properties_get(props, "prop_static_list") - local title_source_prop = obs.obs_properties_get(props, "prop_title_list") - local extra_source_prop = obs.obs_properties_get(props, "extra_source_list") - - obs.obs_property_list_clear(source_prop) -- clear current properties list - obs.obs_property_list_clear(alternate_source_prop) -- clear current properties list - obs.obs_property_list_clear(static_source_prop) -- clear current properties list - obs.obs_property_list_clear(title_source_prop) -- clear current properties list - obs.obs_property_list_clear(extra_source_prop) -- clear extra sources list - - obs.obs_property_list_add_string(extra_source_prop, "", "") - - local sources = obs.obs_enum_sources() - if sources ~= nil then - local n = {} - for _, source in ipairs(sources) do - local name = obs.obs_source_get_name(source) - if isValid(source) then - obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list - end - source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n + 1] = name - end - end - table.sort(n) - obs.obs_property_list_add_string(source_prop, "", "") - obs.obs_property_list_add_string(title_source_prop, "", "") - obs.obs_property_list_add_string(alternate_source_prop, "", "") - obs.obs_property_list_add_string(static_source_prop, "", "") - for _, name in ipairs(n) do - obs.obs_property_list_add_string(source_prop, name, name) - obs.obs_property_list_add_string(title_source_prop, name, name) - obs.obs_property_list_add_string(alternate_source_prop, name, name) - obs.obs_property_list_add_string(static_source_prop, name, name) - end - end - obs.source_list_release(sources) - refresh_directory() - - return true -end - -function refresh_directory_button_clicked(props, p) - dbg_method("refresh directory") - refresh_directory() - return true -end - -function refresh_directory() - local prop_dir_list = obs.obs_properties_get(script_props, "prop_directory_list") - local source_prop = obs.obs_properties_get(props, "prop_source_list") - source_filter = false - load_source_song_directory(true) - table.sort(song_directory) - obs.obs_property_list_clear(prop_dir_list) -- clear directories - for _, name in ipairs(song_directory) do - dbg_inner(name) - obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_properties_apply_settings(script_props, script_sets) -end - --- Called with ANY change to the prepared song list -function prepare_selection_made(props, prop, settings) - obs.obs_property_set_description( - obs.obs_properties_get(props, "prep_grp"), - " Prepared Songs/Text (" .. #prepared_songs .. ")" - ) - dbg_method("prepare_selection_made") - local name = obs.obs_data_get_string(settings, "prop_prepared_list") - using_source = false - prepare_selected(name) - return true -end - --- removes prepared songs -function clear_prepared_clicked(props, p) - dbg_method("clear_prepared_clicked") - prepared_songs = {} -- required for monitor page - page_index = 0 -- required for monitor page - prepared_index = 0 -- required for monitor page - update_source_text() -- required for monitor page - -- clear the list - local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_clear(prep_prop) - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - obs.obs_properties_apply_settings(props, script_sets) - save_prepared() - return true -end - -function prepare_selected(name) - dbg_method("prepare_selected") - -- try to prepare song - if prepare_song_by_name(name) then - page_index = 1 - if not using_source then - prepared_index = get_index_in_list(prepared_songs, name) - else - source_song_title = name - all_sources_fade = true - end - - transition_lyric_text(using_source) - else - -- hide everything if unable to prepare song - -- TODO: clear lyrics entirely after text is hidden - set_text_visibility(TEXT_HIDDEN) - end - - --update_source_text() - return true -end - --- called when selection is made from directory list -function preview_selection_made(props, prop, settings) - local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - - if get_index_in_list(song_directory, name) == nil then - return false - end -- do nothing if invalid name - - obs.obs_data_set_string(settings, "prop_edit_song_title", name) - local song_lines = get_song_text(name) - local combined_text = "" - for i, line in ipairs(song_lines) do - if (i < #song_lines) then - combined_text = combined_text .. line .. "\n" - else - combined_text = combined_text .. line - end - end - obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) - return true -end - -function open_song_clicked(props, p) - local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") - end - if windows_os then - os.execute('explorer "' .. path .. '"') - else - os.execute('xdg-open "' .. path .. '"') - end - return true -end - -function open_button_clicked(props, p) - local path = get_songs_folder_path() - if windows_os then - os.execute('explorer "' .. path .. '"') - else - os.execute('xdg-open "' .. path .. '"') - end -end - --------- ----------------- ------------------------- PROGRAM FUNCTIONS ----------------- --------- -function setSourceOpacity(sourceName) - if sourceName ~= nil and sourceName ~= "" then - local settings = obs.obs_data_create() - if not use100percent then - adj_text_opacity = text_opacity /100 - obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity - obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity - else - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity - end - - local source = obs.obs_get_source_by_name(sourceName) - if source ~= nil then - obs.obs_source_update(source, settings) - end - obs.obs_source_release(source) - obs.obs_data_release(settings) - end -end - - -function apply_source_opacity() - setSourceOpacity(source_name) - setSourceOpacity(alternate_source_name) - if all_sources_fade then - setSourceOpacity(title_source_name) - setSourceOpacity(static_source_name) - end - if link_extras or all_sources_fade then - local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - if count > 0 then - for i = 0, count - 1 do - local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local extra_source = obs.obs_get_source_by_name(sourceName) - if extra_source ~= nil then - source_id = obs.obs_source_get_unversioned_id(extra_source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - setSourceOpacity(sourceName) - else -- check for filter named "Color Correction" - local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") - if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_data_create() - if not use100percent then - obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) - else - obs.obs_data_set_double(filter_settings, "opacity", text_opacity/100) - end - obs.obs_source_update(color_filter, filter_settings) - obs.obs_data_release(filter_settings) - obs.obs_source_release(color_filter) - else -- try to just change visibility in the scene - local sceneSource = obs.obs_frontend_get_current_preview_scene() - local sceneObj = obs.obs_scene_from_source(sceneSource) - local sceneItem = obs.obs_scene_find_source(sceneObj, source_name) - obs.obs_source_release(scene) - if text_opacity > 50 then - obs.obs_sceneitem_set_visible(sceneItem, true) - else - obs.obs_sceneitem_set_visible(sceneItem, false) - end - end - end - end - obs.obs_source_release(extra_source) -- release source ptr - end - end - end -end - -function getSourceOpacity(sourceName) - if sourceName ~= nil and sourceName ~= "" then - local source = obs.obs_get_source_by_name(sourceName) - local settings = obs.obs_source_get_settings(source) - max_opacity[sourceName]={} - max_opacity[sourceName]["opacity"] = obs.obs_data_get_int(settings, "opacity") -- text opacity - max_opacity[sourceName]["outline"] = obs.obs_data_get_int(settings, "outline_opacity") -- outline opacity - max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity - max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity - obs.obs_source_release(source) - obs.obs_data_release(settings) - end -end - - -function read_source_opacity() - dbg_method("read_source_opacity") - getSourceOpacity(source_name) - getSourceOpacity(alternate_source_name) - getSourceOpacity(title_source_name) - getSourceOpacity(static_source_name) - local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - if count > 0 then - for i = 0, count - 1 do - local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local extra_source = obs.obs_get_source_by_name(sourceName) - if extra_source ~= nil then - source_id = obs.obs_source_get_unversioned_id(extra_source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - getSourceOpacity(sourceName) - else -- check for filter named "Color Correction" - - local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") - if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_source_get_settings(color_filter) - max_opacity[sourceName]={} - max_opacity[sourceName]["CC-opacity"] = obs.obs_data_get_double(filter_settings, "opacity") - obs.obs_data_release(filter_settings) - obs.obs_source_release(color_filter) - end - end - end - obs.obs_source_release(extra_source) -- release source ptr - end - end -end - -function set_text_visibility(end_status) - dbg_method("set_text_visibility") - -- if already at desired visibility, then exit - if text_status == end_status then - return - end - if end_status == TEXT_HIDE then - text_opacity = 0 - text_status = end_status - apply_source_opacity() - return - elseif end_status == TEXT_SHOW then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - apply_source_opacity() - return - end - if text_fade_enabled then - -- if fade enabled, begin fade in or out - if end_status == TEXT_HIDDEN then - text_status = TEXT_HIDING - elseif end_status == TEXT_VISIBLE then - text_status = TEXT_SHOWING - end - --all_sources_fade = true - start_fade_timer() - else -- change visibility immediately (fade or no fade) - if end_status == TEXT_HIDDEN then - text_opacity = 0 - text_status = end_status - elseif end_status == TEXT_VISIBLE then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - end - apply_source_opacity() - --update_source_text() - all_sources_fade = false - return - end -end - --- transition to the next lyrics, use fade if enabled --- if lyrics are hidden, force_show set to true will make them visible -function transition_lyric_text(force_show) - dbg_method("transition_lyric_text") - dbg_bool("force show", force_show) - -- update the lyrics display immediately on 2 conditions - -- a) the text is hidden or hiding, and we will not force it to show - -- b) text fade is not enabled - -- otherwise, start text transition out and update the lyrics once - -- fade out transition is complete - if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then - update_source_text() - -- if text is done hiding, we can cancel the all_sources_fade - if text_status == TEXT_HIDDEN then - all_sources_fade = false - end - dbg_inner("hidden") - elseif not text_fade_enabled then - dbg_custom("Instant On") - -- if text fade is not enabled, then we can cancel the all_sources_fade - all_sources_fade = false - set_text_visibility(TEXT_VISIBLE) -- does update_source_text() - update_source_text() - dbg_inner("no text fade") - else -- initiate fade out/in - dbg_custom("Transition Timer") - text_status = TEXT_TRANSITION_OUT - start_fade_timer() - end - dbg_bool("using_source", using_source) -end - --- updates the selected lyrics -function update_source_text() - dbg_method("update_source_text") - dbg_custom("Page Index: " .. page_index) - local text = "" - local alttext = "" - local next_lyric = "" - local next_alternate = "" - local static = static_text - local mstatic = static -- save static for use with monitor - local title = "" - - if alt_title ~= "" then - title = alt_title - else - if not using_source then - if prepared_index ~= nil and prepared_index ~= 0 then - dbg_custom("Update from prepared: " .. prepared_index) - title = prepared_songs[prepared_index] - end - else - dbg_custom("Updatefrom source: " .. source_song_title) - title = source_song_title - end - end - - local source = obs.obs_get_source_by_name(source_name) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - local stat_source = obs.obs_get_source_by_name(static_source_name) - local title_source = obs.obs_get_source_by_name(title_source_name) - - if using_source or (prepared_index ~= nil and prepared_index ~= 0) then - if #lyrics > 0 then - if lyrics[page_index] ~= nil then - text = lyrics[page_index] - end - end - if #alternate > 0 then - if alternate[page_index] ~= nil then - alttext = alternate[page_index] - end - end - - if link_text then - if string.len(text) == 0 and string.len(alttext) == 0 then - --static = "" - --title = "" - end - end - end - -- update source texts - if source ~= nil then - dbg_inner("Title Load") - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", text) - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) - next_lyric = lyrics[page_index + 1] - if (next_lyric == nil) then - next_lyric = "" - end - end - if alt_source ~= nil then - local settings = obs.obs_data_create() -- setup TEXT settings with opacity values - obs.obs_data_set_string(settings, "text", alttext) - obs.obs_source_update(alt_source, settings) - obs.obs_data_release(settings) - next_alternate = alternate[page_index + 1] - if (next_alternate == nil) then - next_alternate = "" - end - end - if stat_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", static) - obs.obs_source_update(stat_source, settings) - obs.obs_data_release(settings) - end - if title_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", title) - obs.obs_source_update(title_source, settings) - obs.obs_data_release(settings) - end - -- release source references - obs.obs_source_release(source) - obs.obs_source_release(alt_source) - obs.obs_source_release(stat_source) - obs.obs_source_release(title_source) - - local next_prepared = "" - if using_source then - next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song - elseif prepared_index < #prepared_songs then - next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song - else - if source_active then - next_prepared = source_song_title -- plan to go back to source loaded song - else - next_prepared = prepared_songs[1] -- plan to loop around to first prepared song - end - end - mon_verse = 0 - if #verses ~= nil then --find valid page Index - for i = 1, #verses do - if page_index >= verses[i] + 1 then - mon_verse = i - end - end -- v = current verse number for this page - end - mon_song = title - mon_lyric = text:gsub("\n", "
• ") - mon_nextlyric = next_lyric:gsub("\n", "
• ") - mon_alt = alttext:gsub("\n", "
• ") - mon_nextalt = next_alternate:gsub("\n", "
• ") - mon_nextsong = next_prepared - - update_monitor() -end - -function start_fade_timer() - dbgsp("started fade timer") - obs.timer_add(fade_callback, 50) -end - -function fade_callback() - -- if not in a transitory state, exit callback - if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then - obs.remove_current_callback() - all_sources_fade = false - end - -- the amount we want to change opacity by - local opacity_delta = 1 + text_fade_speed - -- change opacity in the direction of transitory state - if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then - local new_opacity = text_opacity - opacity_delta - if new_opacity > 0 then - text_opacity = new_opacity - else - -- completed fade out, determine next move - text_opacity = 0 - if text_status == TEXT_TRANSITION_OUT then - -- update to new lyric between fades - update_source_text() - -- begin transition back in - text_status = TEXT_TRANSITION_IN - else - text_status = TEXT_HIDDEN - end - end - elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then - local new_opacity = text_opacity + opacity_delta - if new_opacity < 100 then - text_opacity = new_opacity - else - -- completed fade in - text_opacity = 100 - text_status = TEXT_VISIBLE - end - end - -- apply the new opacity - apply_source_opacity() -end - -function prepare_song_by_index(index) - dbg_method("prepare_song_by_index") - if index <= #prepared_songs then - prepare_song_by_name(prepared_songs[index]) - end -end - --- prepares lyrics of the song -function prepare_song_by_name(name) - dbg_method("prepare_song_by_name") - if name == nil then - return false - end - last_prepared_song = name - -- if using transition on lyric change, first transition - -- would be reset with new song prepared - transition_completed = false - -- load song lines - local song_lines = get_song_text(name) - if song_lines == nil then - return false - end - local cur_line = 1 - local cur_aline = 1 - local recordRefrain = false - local playRefrain = false - local use_alternate = false - local use_static = false - local showText = true - local commentBlock = false - local singleAlternate = false - local refrain = {} - local arefrain = {} - lyrics = {} - verses = {} - alternate = {} - static_text = "" - alt_title = "" - local adjusted_display_lines = display_lines - local refrain_display_lines = display_lines - local alternate_display_lines = display_lines - local displaySize = display_lines - for _, line in ipairs(song_lines) do - local new_lines = 1 - local single_line = false - local comment_index = line:find("//%[") -- Look for comment block Set - if comment_index ~= nil then - commentBlock = true - line = line:sub(comment_index + 3) - end - comment_index = line:find("//]") -- Look for comment block Clear - if comment_index ~= nil then - commentBlock = false - line = line:sub(1, comment_index - 1) - new_lines = 0 - end - if not commentBlock then - local comment_index = line:find("%s*//") - if comment_index ~= nil then - line = line:sub(1, comment_index - 1) - new_lines = 0 - end - local alternate_index = line:find("#A%[") - if alternate_index ~= nil then - use_alternate = true - line = line:sub(1, alternate_index - 1) - new_lines = 0 - end - alternate_index = line:find("#A]") - if alternate_index ~= nil then - use_alternate = false - line = line:sub(1, alternate_index - 1) - new_lines = 0 - end - local static_index = line:find("#S%[") - if static_index ~= nil then - use_static = true - line = line:sub(1, static_index - 1) - new_lines = 0 - end - static_index = line:find("#S]") - if static_index ~= nil then - use_static = false - line = line:sub(1, static_index - 1) - new_lines = 0 - end - - local newcount_index = line:find("#L:") - if newcount_index ~= nil then - local iS, iE = line:find("%d+", newcount_index + 3) - local newLines = tonumber(line:sub(iS, iE)) - if use_alternate then - alternate_display_lines = newLines - elseif recordRefrain then - refrain_display_lines = newLines - else - adjusted_display_lines = newLines - refrain_display_lines = newLines - alternate_display_lines = newLines - end - line = line:sub(1, newcount_index - 1) - new_lines = 0 -- ignore line - end - local static_index = line:find("#S:") - if static_index ~= nil then - line = line:sub(static_index + 3) - static_text = line - new_lines = 0 - end - local title_index = line:find("#T:") - if title_index ~= nil then - local title_indexEnd = line:find("%s+", title_index + 1) - line = line:sub(title_indexEnd + 1) - alt_title = line - new_lines = 0 - end - local alt_index = line:find("#A:") - if alt_index ~= nil then - local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) - new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) - local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) - line = line:sub(alt_indexEnd + 1) - singleAlternate = true - end - if line:find("###") ~= nil then -- Look for single line - line = line:gsub("%s*###%s*", "") - single_line = true - end - local newcount_index = line:find("#D:") - if newcount_index ~= nil then - local newcount_indexStart, newcount_indexEnd = line:find("%d+", newcount_index + 3) - new_lines = tonumber(line:sub(newcount_indexStart, newcount_indexEnd)) - _, newcount_indexEnd = line:find("%s+", newcount_indexEnd + 1) - line = line:sub(newcount_indexEnd + 1) - end - local refrain_index = line:find("#R%[") - if refrain_index ~= nil then - if next(refrain) ~= nil then - for i, _ in ipairs(refrain) do - refrain[i] = nil - end - end - recordRefrain = true - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#r%[") - if refrain_index ~= nil then - if next(refrain) ~= nil then - for i, _ in ipairs(refrain) do - refrain[i] = nil - end - end - recordRefrain = true - showText = false - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#R]") - if refrain_index ~= nil then - recordRefrain = false - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#r]") - if refrain_index ~= nil then - recordRefrain = false - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - - refrain_index = line:find("##R") - if refrain_index == nil then - refrain_index = line:find("##r") - end - if refrain_index ~= nil then - playRefrain = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - else - playRefrain = false - end - newcount_index = line:find("#P:") - if newcount_index ~= nil then - new_lines = tonumber(line:sub(newcount_index + 3)) - line = line:sub(1, newcount_index - 1) - end - newcount_index = line:find("#B:") - if newcount_index ~= nil then - new_lines = tonumber(line:sub(newcount_index + 3)) - line = line:sub(1, newcount_index - 1) - end - local phantom_index = line:find("##P") - if phantom_index ~= nil then - line = line:sub(1, phantom_index - 1) - end - phantom_index = line:find("##B") - if phantom_index ~= nil then - line = line:gsub("%s*##B%s*", "") .. "\n" - end - local verse_index = line:find("##V") - if verse_index ~= nil then - line = line:sub(1, verse_index - 1) - new_lines = 0 - verses[#verses + 1] = #lyrics - dbg_inner("Verse: " .. #lyrics) - end - if line ~= nil then - if use_static then - if static_text == "" then - static_text = line - else - static_text = static_text .. "\n" .. line - end - else - if use_alternate or singleAlternate then - if recordRefrain then - displaySize = refrain_display_lines - else - displaySize = alternate_display_lines - end - if new_lines > 0 then - while (new_lines > 0) do - if recordRefrain then - if (cur_line == 1) then - arefrain[#refrain + 1] = line - else - arefrain[#refrain] = arefrain[#refrain] .. "\n" .. line - end - end - if showText and line ~= nil then - if (cur_aline == 1) then - alternate[#alternate + 1] = line - else - alternate[#alternate] = alternate[#alternate] .. "\n" .. line - end - end - cur_aline = cur_aline + 1 - if single_line or singleAlternate or cur_aline > displaySize then - if ensure_lines then - for i = cur_aline, displaySize, 1 do - cur_aline = i - if showText and alternate[#alternate] ~= nil then - alternate[#alternate] = alternate[#alternate] .. "\n" - end - if recordRefrain then - arefrain[#refrain] = arefrain[#refrain] .. "\n" - end - end - end - cur_aline = 1 - end - new_lines = new_lines - 1 - end - end - if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record - for _, refrain_line in ipairs(arefrain) do - alternate[#alternate + 1] = refrain_line - end - end - singleAlternate = false - else - if recordRefrain then - displaySize = refrain_display_lines - else - displaySize = adjusted_display_lines - end - if new_lines > 0 then - while (new_lines > 0) do - if recordRefrain then - if (cur_line == 1) then - refrain[#refrain + 1] = line - else - refrain[#refrain] = refrain[#refrain] .. "\n" .. line - end - end - if showText and line ~= nil then - if (cur_line == 1) then - lyrics[#lyrics + 1] = line - else - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line - end - end - cur_line = cur_line + 1 - if single_line or cur_line > displaySize then - if ensure_lines then - for i = cur_line, displaySize, 1 do - cur_line = i - if showText and lyrics[#lyrics] ~= nil then - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" - end - if recordRefrain then - refrain[#refrain] = refrain[#refrain] .. "\n" - end - end - end - cur_line = 1 - end - new_lines = new_lines - 1 - end - end - end - if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record - for _, refrain_line in ipairs(refrain) do - lyrics[#lyrics + 1] = refrain_line - end - end - end - end - end - end - if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then - for i = cur_line, displaySize, 1 do - cur_line = i - if use_alternate then - if showText and alternate[#alternate] ~= nil then - alternate[#alternate] = alternate[#alternate] .. "\n" - end - else - if showText and lyrics[#lyrics] ~= nil then - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" - end - end - if recordRefrain then - refrain[#refrain] = refrain[#refrain] .. "\n" - end - end - end - lyrics[#lyrics + 1] = "" - -- pause_timer = false - return true -end - --- finds the index of a song in the directory --- if item is not in list, then return nil -function get_index_in_list(list, q_item) - for index, item in ipairs(list) do - if item == q_item then - return index - end - end - return nil -end - --------- ----------------- ------------------------- FILE FUNCTIONS ----------------- --------- - --- delete previewed song -function delete_song(name) - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") - end - os.remove(path) - table.remove(song_directory, get_index_in_list(song_directory, name)) - source_filter = false - load_source_song_directory(false) -end - --- loads the song directory -function load_source_song_directory(use_filter) - dbg_method("load_source_song_directory") - local keytext = meta_tags - if source_filter then - keytext = source_meta_tags - end - dbg_inner(keytext) - local keys = ParseCSVLine(keytext) - - song_directory = {} - local filenames = {} - local tags = {} - local dir = obs.os_opendir(get_songs_folder_path()) - -- get_songs_folder_path()) - local entry - local songExt - local songTitle - local goodEntry = true - - repeat - entry = obs.os_readdir(dir) - if - entry and not entry.directory and - (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") - then - songExt = obs.os_get_path_extension(entry.d_name) - songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) - tags = readTags(songTitle) - goodEntry = true - if use_filter and #keys > 0 then -- need to check files - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - goodEntry = false -- start assuming file will not be shown - if #tags == 0 then -- check no tagged option - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - else -- have keys and tags so compare them - for k = 1, #keys do - for t = 1, #tags do - if tags[t] == keys[k] then - goodEntry = true -- found match so show file - break - end - end - if goodEntry then -- stop outer key loop on match - break - end - end - end - end - if goodEntry then -- add file if valid match - if songExt == ".enc" then - song_directory[#song_directory + 1] = dec(songTitle) - else - song_directory[#song_directory + 1] = songTitle - end - end - end - until not entry - obs.os_closedir(dir) -end --- --- reads the first line of each lyric file, looks for the //meta comment and returns any CSV tags that exist --- -function readTags(name) - local meta = "" - local path = {} - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") - end - local file = io.open(path, "r") - if file ~= nil then - for line in file:lines() do - meta = line - break - end - file:close() - end - local meta_index = meta:find("//meta ") -- Look for meta block Set - if meta_index ~= nil then - meta = meta:sub(meta_index + 7) - return ParseCSVLine(meta) - end - return {} -end - -function ParseCSVLine(line) - local res = {} - local pos = 1 - sep = "," - while true do - local c = string.sub(line, pos, pos) - if (c == "") then - break - end - if (c == '"') then - local txt = "" - repeat - local startp, endp = string.find(line, '^%b""', pos) - txt = txt .. string.sub(line, startp + 1, endp - 1) - pos = endp + 1 - c = string.sub(line, pos, pos) - if (c == '"') then - txt = txt .. '"' - end - until (c ~= '"') - txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. txt) - table.insert(res, txt) - assert(c == sep or c == "") - pos = pos + 1 - else - local startp, endp = string.find(line, sep, pos) - if (startp) then - local t = string.sub(line, pos, startp - 1) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res, t) - pos = endp + 1 - else - local t = string.sub(line, pos) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res, t) - break - end - end - end - return res -end - -local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet - --- encode title/filename if it contains invalid filename characters --- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup --- -function enc(data) - return ((data:gsub( - ".", - function(x) - local r, b = "", x:byte() - for i = 8, 1, -1 do - r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0") - end - return r - end - ) .. "0000"):gsub( - "%d%d%d?%d?%d?%d?", - function(x) - if (#x < 6) then - return "" - end - local c = 0 - for i = 1, 6 do - c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0) - end - return b:sub(c + 1, c + 1) - end - ) .. ({"", "==", "="})[#data % 3 + 1]) -end --- --- decode an encoded title/filename --- -function dec(data) - data = string.gsub(data, "[^" .. b .. "=]", "") - return (data:gsub( - ".", - function(x) - if (x == "=") then - return "" - end - local r, f = "", (b:find(x) - 1) - for i = 6, 1, -1 do - r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and "1" or "0") - end - return r - end - ):gsub( - "%d%d%d?%d?%d?%d?%d?%d?", - function(x) - if (#x ~= 8) then - return "" - end - local c = 0 - for i = 1, 8 do - c = c + (x:sub(i, i) == "1" and 2 ^ (8 - i) or 0) - end - return string.char(c) - end - )) -end - -function testValid(filename) - if string.find(filename, "[\128-\255]") ~= nil then - return false - end - if string.find(filename, '[\\\\/:*?"<>|]') ~= nil then - return false - end - return true -end - --- saves previewed song, return true if new song -function save_song(name, text) - local path = {} - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") - end - local file = io.open(path, "w") - if file ~= nil then - for line in text:gmatch("([^\n]+)") do - local trimmed = line:match("%s*(%S-.*%S+)%s*") - if trimmed ~= nil then - file:write(trimmed, "\n") - end - end - file:close() - if get_index_in_list(song_directory, name) == nil then - song_directory[#song_directory + 1] = name - return true - end - end - return false -end - --- saves preprepared songs -function save_prepared() - dbg_method("save_prepared") - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") - for i, name in ipairs(prepared_songs) do - -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs - file:write(name, "\n") - -- end - end - file:close() - return true -end - -function update_monitor() - dbg_method("update_monitor") - local tableback = "black" - local text = "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = - text .. - "
" - text = - text .. - "
" - if using_source then - text = text .. "From Source: " .. load_scene .. "
" - else - text = text .. "Prepared Song: " .. prepared_index - text = - text .. - " of " .. #prepared_songs .. "
" - end - text = - text .. - "
Lyric Page: " .. - page_index - text = text .. " of " .. #lyrics .. "
" - if #verses ~= nil and mon_verse > 0 then - text = - text .. - "
Verse: " .. mon_verse - text = text .. " of " .. #verses .. "
" - end - text = text .. "
" - if not anythingActive() then - tableback = "#440000" - end - local visbgTitle = tableback - local visbgText = tableback - if text_status == TEXT_HIDDEN or text_status == TEXT_HIDING then - visbgText = "maroon" - if link_text then - visbgTitle = "maroon" - end - end - - text = - text .. - "
" - if mon_song ~= "" and mon_song ~= nil then - text = - text .. - "" - text = - text .. - "" - end - if mon_lyric ~= "" and mon_lyric ~= nil then - text = - text .. - "" - text = - text .. "" - end - if mon_nextlyric ~= "" and mon_nextlyric ~= nil then - text = - text .. - "" - text = text .. "" - end - if mon_alt ~= "" and mon_alt ~= nil then - text = - text .. - "" - text = - text .. - "" - end - if mon_nextalt ~= "" and mon_nextalt ~= nil then - text = - text .. - "" - text = text .. "" - end - if mon_nextsong ~= "" and mon_nextsong ~= nil then - text = - text .. - "" - text = text .. "" - end - text = text .. "
Song
Title
" .. mon_song .. "
Current
Page
• " .. mon_lyric .. "
Next
Page
• " .. mon_nextlyric .. "
Alt
Lyric
• " .. mon_alt .. "
Next
Alt
• " .. mon_nextalt .. "
Next
Song:
" .. mon_nextsong .. "
" - local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") - dbg_inner("write monitor file") - file:write(text) - file:close() - return true -end - --- returns path of the given song name -function get_song_file_path(name, suffix) - if name == nil then - return nil - end - return get_songs_folder_path() .. "\\" .. name .. suffix -end - --- returns path of the lyrics songs folder -function get_songs_folder_path() - local sep = package.config:sub(1, 1) - local path = "" - if windows_os then - path = os.getenv("USERPROFILE") - else - path = os.getenv("HOME") - end - return path .. sep .. ".config" .. sep .. ".obs_lyrics" -end - --- gets the text of a song -function get_song_text(name) - local song_lines = {} - local path = {} - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") - end - local file = io.open(path, "r") - if file ~= nil then - for line in file:lines() do - song_lines[#song_lines + 1] = line - end - file:close() - else - return nil - end - - return song_lines -end - --- ------ ----------------- ------------------------- OBS DEFAULT FUNCTIONS --- -------------- --------- - --- A function named script_properties defines the properties that the user --- can change for the entire script module itself - -local help = - "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. - " Markup      Syntax         Markup      Syntax \n" .. - "============ ==========   ============ ==========\n" .. - " Display n Lines    #L:n      End Page after Line   Line ###\n" .. - " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. - " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. - " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. - " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. - "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. - "Comment Line    // Line       Block Comments    //[ and //] \n" .. - "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line" - -function script_properties() - dbg_method("script_properties") - editVisSet = false - script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) - ----------- - obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲", change_info_visible) - local gp = obs.obs_properties_create() - obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) - obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) - obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) - obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) - obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) - obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) - obs.obs_properties_add_group( - script_props, - "info_grp", - "Song Title (filename) and Lyrics Information", - obs.OBS_GROUP_NORMAL, - gp - ) - ------------ - obs.obs_properties_add_button( - script_props, - "prepared_showing", - "▲- HIDE PREPARED SONGS -▲", - change_prepared_visible - ) - gp = obs.obs_properties_create() - local prop_dir_list = - obs.obs_properties_add_list( - gp, - "prop_directory_list", - "Song Directory", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(prop_dir_list, name, name) - end - obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) - obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) - local gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() - local prepare_prop = - obs.obs_properties_add_list( - gps, - "prop_prepared_list", - "Prepared Songs", - obs.OBS_COMBO_TYPE_EDITABLE, - obs.OBS_COMBO_FORMAT_STRING - ) - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prepare_prop, name, name) - end - obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) - obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) - obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) - local eps = obs.obs_properties_create() - local edit_prop = - obs.obs_properties_add_editable_list( - eps, - "prep_list", - "Prepared Songs/Text", - obs.OBS_EDITABLE_LIST_TYPE_STRINGS, - nil, - nil - ) - obs.obs_property_set_modified_callback(edit_prop, setEditVis) - obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes", save_edits_clicked) - local edit_group_prop = - obs.obs_properties_add_group( - gps, - "edit_grp", - "Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", - obs.OBS_GROUP_NORMAL, - eps - ) - obs.obs_properties_add_group(gp, "prep_grp", " Prepared Songs", obs.OBS_GROUP_NORMAL, gps) - obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) - ------------------ - obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) - hotkey_props = obs.obs_properties_create() - local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "+", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked) - obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons (with Assigned HotKeys)",obs.OBS_GROUP_NORMAL,hotkey_props) - obs.obs_property_set_modified_callback(hktitletext, nameKeysCallback) - name_hotkeys() - ------ - obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) - gp = obs.obs_properties_create() - local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "Lines to Display", 1, 50, 1) - obs.obs_property_set_long_description( - lines_prop, - "Sets default lines per page of lyric, overwritten by Markup: #L:n" - ) - local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") - obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") - local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") - obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") - - local transition_prop = - obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") - obs.obs_property_set_modified_callback(transition_prop, change_transition_property) - obs.obs_property_set_long_description( - transition_prop, - "Use with Studio Mode, duplicate sources, and OBS source transitions" - ) - local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable text fade") -- Fade Enable (WZ) - obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) - obs.obs_properties_add_bool(gp,"use100percent", "Always use 0-100% opacity for fades") - obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) - ------------- - obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) - gp = obs.obs_properties_create() - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) - local source_prop = - obs.obs_properties_add_list( - gp, - "prop_source_list", - "Text Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - local title_source_prop = - obs.obs_properties_add_list( - gp, - "prop_title_list", - "Title Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - local alternate_source_prop = - obs.obs_properties_add_list( - gp, - "prop_alternate_list", - "Alternate Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - local static_source_prop = - obs.obs_properties_add_list( - gp, - "prop_static_list", - "Static Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - obs.obs_properties_add_button(gp, "Opacity_refresh", "Mark Source Opacities if not 100%", read_source_opacity()) - obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) - xgp = obs.obs_properties_create() - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") - local extra_linked_prop = - obs.obs_properties_add_list( - xgp, - "extra_linked_list", - "Linked Sources ", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - -- initialize previously loaded extra properties from table - for _, sourceName in ipairs(extra_sources) do - obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) - end - local extra_source_prop = - obs.obs_properties_add_list( - xgp, - "extra_source_list", - " Select Source:", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) - local clearcall_prop = - obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) - local extra_group_prop = - obs.obs_properties_add_group(gp, "xtr_grp", "Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL, xgp) - obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) - local count = obs.obs_property_list_item_count(extra_linked_prop) - if count > 0 then - obs.obs_property_set_description(extra_linked_prop, "Linked Sources (" .. count .. ")") - else - obs.obs_property_set_visible(extra_group_prop, false) - end - - local sources = obs.obs_enum_sources() - obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") - if sources ~= nil then - local n = {} - for _, source in ipairs(sources) do - local name = obs.obs_source_get_name(source) - if isValid(source) then - obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list - end - source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n + 1] = name - end - end - table.sort(n) - obs.obs_property_list_add_string(source_prop, "", "") - obs.obs_property_list_add_string(title_source_prop, "", "") - obs.obs_property_list_add_string(alternate_source_prop, "", "") - obs.obs_property_list_add_string(static_source_prop, "", "") - for _, name in ipairs(n) do - obs.obs_property_list_add_string(source_prop, name, name) - obs.obs_property_list_add_string(title_source_prop, name, name) - obs.obs_property_list_add_string(alternate_source_prop, name, name) - obs.obs_property_list_add_string(static_source_prop, name, name) - end - end - obs.source_list_release(sources) - - ----------------- - obs.obs_property_set_enabled(hktitletext, false) - obs.obs_property_set_visible(edit_group_prop, false) - obs.obs_property_set_visible(meta_group_prop, false) - read_source_opacity() - return script_props -end - --- script_update is called when settings are changed -function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") - text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") - display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") - source_name = obs.obs_data_get_string(settings, "prop_source_list") - alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") - static_source_name = obs.obs_data_get_string(settings, "prop_static_list") - title_source_name = obs.obs_data_get_string(settings, "prop_title_list") - ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") - link_text = obs.obs_data_get_bool(settings, "do_link_text") - link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") - fade_text = obs.obs_data_get_bool(settings, "use100percent") - use100percent = obs.obs_data_get_bool(settings, "use100percent") - use100percent = obs.obs_data_get_bool(settings, "use100percent") - use100percent = obs.obs_data_get_bool(settings, "use100percent") - use100percent = obs.obs_data_get_bool(settings, "use100percent") - use100percent = obs.obs_data_get_bool(settings, "use100percent") -end - --- A function named script_defaults will be called to set the default settings -function script_defaults(settings) - dbg_method("script_defaults") - obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - if windows_os then - os.execute('mkdir "' .. get_songs_folder_path() .. '"') - else - os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') - end -end - ---verify source has an opacity setting -function isValid(source) - if source ~= nil then - local flags = obs.obs_source_get_output_flags(source) - local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) - if bit.band(flags, targetFlag) == targetFlag then - return true - end - end - return false -end - --- adds an extra linked source. --- Source must be text source, or have 'Color Correction' Filter applied -function link_source_selected(props, prop, settings) - dbg_method("link_source_selected") - local extra_source = obs.obs_data_get_string(settings, "extra_source_list") - if extra_source ~= nil and extra_source ~= "" then - local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") - obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) - obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) - obs.obs_data_set_string(script_sets, "extra_source_list", "") - obs.obs_property_set_description( - extra_linked_list, - "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" - ) - end - return true -end - --- removes linked sources -function do_linked_clicked(props, p) - dbg_method("do_link_clicked") - obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), true) - obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), false) - obs.obs_properties_apply_settings(props, script_sets) - - return true -end - --- removes linked sources -function clear_linked_clicked(props, p) - dbg_method("clear_linked_clicked") - local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") - obs.obs_property_list_clear(extra_linked_list) - obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) - obs.obs_property_set_description(extra_linked_list, "Linked Sources") - - return true -end - --- A function named script_description returns the description shown to --- the user - -function script_description() - return description -end - -function vMode(vis) - return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" -end - -function expand_all_groups(props, prop, settings) - expandcollapse = not expandcollapse - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "info_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "mng_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "disp_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "src_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "ctrl_grp"), expandcollapse) - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - obs.obs_property_set_description( - obs.obs_properties_get(props, "info_showing"), - mode1 .. "SONG INFORMATION" .. mode2 - ) - obs.obs_property_set_description( - obs.obs_properties_get(props, "prepared_showing"), - mode1 .. "PREPARED SONGS" .. mode2 - ) - obs.obs_property_set_description( - obs.obs_properties_get(props, "options_showing"), - mode1 .. "DISPLAY OPTIONS" .. mode2 - ) - obs.obs_property_set_description( - obs.obs_properties_get(props, "src_showing"), - mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 - ) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - return true -end - -function all_vis_equal(props) - if - (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "prep_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) or - not (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "mng_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) - then - expandcollapse = not expandcollapse - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description( - obs.obs_properties_get(props, "expand_all_button"), - mode1 .. "ALL GROUPS" .. mode2 - ) - end -end - -function updateProperties() - local p = obs.obs_properties_get(props, "hotkey-title") - local v = obs.obs_property_get_description(p) - if v == '+' then - v = '-' - else - v = '+' - end - print(v) - obs.obs_property_set_description(p,v) -end - -function name_keys_callback(props, prop, settings) - print("Name") - name_hotkeys() - return true -end - -function change_info_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "info_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "info_showing"), - mode1 .. "SONG INFORMATION" .. mode2 - ) - all_vis_equal(props) - return true -end - -function change_prepared_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "mng_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "prepared_showing"), - mode1 .. "PREPARED SONGS" .. mode2 - ) - all_vis_equal(props) - return true -end - -function change_options_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "disp_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "options_showing"), - mode1 .. "DISPLAY OPTIONS" .. mode2 - ) - all_vis_equal(props) - return true -end - -function change_src_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "src_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "src_showing"), - mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 - ) - all_vis_equal(props) - return true -end - -function change_ctrl_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "ctrl_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - all_vis_equal(props) - return true -end - -function change_fade_property(props, prop, settings) - local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") - dbg_bool("Fade: ", text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) - local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") - obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) - return true -end - -function show_help_button(props, prop, settings) - dbg_method("show help") - local hb = obs.obs_properties_get(props, "show_help_button") - showhelp = not showhelp - if showhelp then - obs.obs_property_set_description(hb, help) - else - obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") - end - return true -end - -function setEditVis(props, prop, settings) -- hides edit group on initial showing - dbg_method("setEditVis") - if not editVisSet then - local pp = obs.obs_properties_get(script_props, "edit_grp") - obs.obs_property_set_visible(pp, false) - pp = obs.obs_properties_get(props, "meta") - obs.obs_property_set_visible(pp, false) - editVisSet = true - end -end - -function filter_songs_clicked(props, p) - local pp = obs.obs_properties_get(props, "meta") - if not obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "filter_songs_button") - obs.obs_property_set_description(mpb, "Clear Filters") -- change button function - meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") - refresh_directory() - else - obs.obs_property_set_visible(pp, false) - meta_tags = "" -- clear meta tags - refresh_directory() - local mpb = obs.obs_properties_get(props, "filter_songs_button") -- - obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function - end - return true -end - -function edit_prepared_clicked(props, p) - local pp = obs.obs_properties_get(props, "edit_grp") - if obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared List") - return true - end - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - local count = obs.obs_property_list_item_count(prop_prep_list) - local songNames = obs.obs_data_get_array(script_sets, "prep_list") - local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2 do - obs.obs_data_array_erase(songNames, 0) - end - end - - for i = 0, count - 1 do - local song = obs.obs_property_list_item_string(prop_prep_list, i) - local array_obj = obs.obs_data_create() - obs.obs_data_set_string(array_obj, "value", song) - obs.obs_data_array_push_back(songNames, array_obj) - obs.obs_data_release(array_obj) - end - obs.obs_data_set_array(script_sets, "prep_list", songNames) - obs.obs_data_array_release(songNames) - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Cancel Prepared Edits") - return true -end - --- removes prepared songs -function save_edits_clicked(props, p) - load_source_song_directory(false) - prepared_songs = {} - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_clear(prop_prep_list) - local songNames = obs.obs_data_get_array(script_sets, "prep_list") - local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2 - 1 do - local item = obs.obs_data_array_item(songNames, i) - local itemName = obs.obs_data_get_string(item, "value") - if get_index_in_list(song_directory, itemName) ~= nil then - prepared_songs[#prepared_songs + 1] = itemName - obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) - end - obs.obs_data_release(item) - end - end - obs.obs_data_array_release(songNames) - save_prepared() - if #prepared_songs > 0 then - obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) - prepared_index = 1 - else - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") - prepared_index = 0 - end - pp = obs.obs_properties_get(script_props, "edit_grp") - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared Songs List") - obs.obs_properties_apply_settings(props, script_sets) - return true -end - -function change_transition_property(props, prop, settings) - local transition_set = obs.obs_data_get_bool(settings, "transition_enabled") - local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") - local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") - obs.obs_property_set_enabled(text_fade_set_prop, not transition_set) - obs.obs_property_set_enabled(fade_speed_prop, not transition_set) - transition_enabled = transition_set - return true -end - --- A function named script_save will be called when the script is saved -function script_save(settings) - dbg_method("script_save") - save_prepared() - local hotkey_save_array = obs.obs_hotkey_save(hotkey_n_id) - obs.obs_data_set_array(settings, "lyric_next_hotkey", hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_p_id) - obs.obs_data_set_array(settings, "lyric_prev_hotkey", hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_c_id) - obs.obs_data_set_array(settings, "lyric_clear_hotkey", hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_n_p_id) - obs.obs_data_set_array(settings, "next_prepared_hotkey", hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_p_p_id) - obs.obs_data_set_array(settings, "previous_prepared_hotkey", hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_home_id) - obs.obs_data_set_array(settings, "home_song_hotkey", hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_save_array = obs.obs_hotkey_save(hotkey_reset_id) - obs.obs_data_set_array(settings, "reset_prepared_hotkey", hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - --- - --- Save extra_linked_sources properties to settings so they can be restored when script is reloaded - --- - local extra_sources_array = obs.obs_data_array_create() - local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - for i = 0, count - 1 do - local source_name = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local array_obj = obs.obs_data_create() - obs.obs_data_set_string(array_obj, "value", source_name) - obs.obs_data_array_push_back(extra_sources_array, array_obj) - obs.obs_data_release(array_obj) - end - obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) - obs.obs_data_array_release(extra_sources_array) - -) -end - --- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS --- sets callback to obs_frontend Event Callback --- -function script_load(settings) - dbg_method("script_load") - hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_next_hotkey") - hotkey_n_key = get_hotkeys(hotkey_save_array, "Next Lyric", ".......................") - obs.obs_hotkey_load(hotkey_n_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_p_id = obs.obs_hotkey_register_frontend("lyric_prev_hotkey", "Go Back Lyrics", prev_lyric) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_prev_hotkey") - hotkey_p_key = get_hotkeys(hotkey_save_array, "Previous Lyric", " ..................") - obs.obs_hotkey_load(hotkey_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_c_id = obs.obs_hotkey_register_frontend("lyric_clear_hotkey", "Show/Hide Lyrics", toggle_lyrics_visibility) - hotkey_save_array = obs.obs_data_get_array(settings, "lyric_clear_hotkey") - hotkey_c_key = get_hotkeys(hotkey_save_array, "Show/Hide Lyrics", " ..............") - obs.obs_hotkey_load(hotkey_c_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_n_p_id = obs.obs_hotkey_register_frontend("next_prepared_hotkey", "Prepare Next", next_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "next_prepared_hotkey") - hotkey_n_p_key = get_hotkeys(hotkey_save_array, "Next Prepared", " ................") - obs.obs_hotkey_load(hotkey_n_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") - hotkey_p_p_key = get_hotkeys(hotkey_save_array, "Previous Prepared", "............") - obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_home_id = obs.obs_hotkey_register_frontend("home_song_hotkey", "Reset to Song Start", home_song) - hotkey_save_array = obs.obs_data_get_array(settings, "home_song_hotkey") - hotkey_home_key = get_hotkeys(hotkey_save_array, "Reset to Song Start", " ..........") - obs.obs_hotkey_load(hotkey_home_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - hotkey_reset_id = - obs.obs_hotkey_register_frontend("reset_prepared_hotkey", "Reset to First Prepared Song", home_prepared) - hotkey_save_array = obs.obs_data_get_array(settings, "reset_prepared_hotkey") - hotkey_reset_key = get_hotkeys(hotkey_save_array, "Reset to 1st Prepared", " .......") - obs.obs_hotkey_load(hotkey_reset_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - script_sets = settings - source_name = obs.obs_data_get_string(settings, "prop_source_list") - - extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - - -- load previously defined extra sources from settings array into table - -- script_properties function will take them from the table and restore them as UI properties - -- - local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - local count = obs.obs_data_array_count(extra_sources_array) - if count > 0 then - for i = 0, count do - local item = obs.obs_data_array_item(extra_sources_array, i) - local sourceName = obs.obs_data_get_string(item, "value") - if sourceName ~= "" then - extra_sources[#extra_sources + 1] = sourceName - end - obs.obs_data_release(item) - end - end - obs.obs_data_array_release(extra_sources_array) - - -- load prepared songs from stored file - -- - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - load_source_song_directory(false) - -- load prepared songs from previous - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "r") - if file ~= nil then - for line in file:lines() do - prepared_songs[#prepared_songs + 1] = line - end - file:close() - end - obs.timer_add(updateProperties, 1000) - obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture -end - -function script_unload() -all_sources_fade = true -text_opacity = 100 -apply_source_opacity() - -end - - ---- ------- ---------- Source Showing or Source Active Helper Functions ---------- Return true if sourcename given is showing anywhere or on in the Active scene ------- ---- -function isShowing(sourceName) - local source = obs.obs_get_source_by_name(sourceName) - local showing = false - if source ~= nil then - showing = obs.obs_source_showing(source) - end - obs.obs_source_release(source) - return showing -end - -function isActive(sourceName) - local source = obs.obs_get_source_by_name(sourceName) - local active = false - if source ~= nil then - active = obs.obs_source_active(source) - end - obs.obs_source_release(source) - return active -end - -function anythingShowing() - return isShowing(source_name) or isShowing(alternate_source_name) or isShowing(title_source_name) or - isShowing(static_source_name) -end - -function sourceShowing() - return isShowing(source_name) -end - -function alternateShowing() - return isShowing(alternate_source_name) -end - -function titleShowing() - return isShowing(title_source_name) -end - -function staticShowing() - return isShowing(static_source_name) -end - -function anythingActive() - return isActive(source_name) or isActive(alternate_source_name) or isActive(title_source_name) or - isActive(static_source_name) -end - -function sourceActive() - return isActive(source_name) -end - -function alternateActive() - return isActive(alternate_source_name) -end - -function titleActive() - return isActive(title_source_name) -end - -function staticActive() - return isActive(static_source_name) -end - ---- ------- ---------- Initialization Functions ---------- Manages defined Hotkey Save, Load, Translate and Button rename ---------- Loads inital song directory and any previously prepared lyrics ------- ---- - ----------------------------------------------------------------------------------------------------------- --- get_hotkeys(loaded hotkey array, desired prefix text, leader text (between prefix and hotkey label) --- Returns translated hotkey text label with prefix and leader --- e.g. if HotKeyArray contains an assigned hotkey Shift and F1 key combo, then --- get_hotkeys(HotKeyArray," ....... ", "HotKey") returns "Hotkey ....... Shift + F1" ----------------------------------------------------------------------------------------------------------- - -function get_hotkeys(hotkey_array, prefix, leader) - local Translate = { - ["NUMLOCK"] = "NumLock", - ["NUMSLASH"] = "Num/", - ["NUMASTERISK"] = "Num*", - ["NUMMINUS"] = "Num-", - ["NUMPLUS"] = "Num+", - ["NUMPERIOD"] = "NumDel", - ["INSERT"] = "Insert", - ["PAGEDOWN"] = "Page-Down", - ["PAGEUP"] = "Page-Up", - ["HOME"] = "Home", - ["END"] = "End", - ["RETURN"] = "Return", - ["UP"] = "Up", - ["DOWN"] = "Down", - ["RIGHT"] = "Right", - ["LEFT"] = "Left", - ["SCROLLLOCK"] = "Scroll-Lock", - ["BACKSPACE"] = "Backspace", - ["ESCAPE"] = "Esc", - ["MENU"] = "Menu", - ["META"] = "Meta", - ["PRINT"] = "Prt", - ["TAB"] = "Tab", - ["DELETE"] = "Del", - ["CAPSLOCK"] = "Caps-Lock", - ["NUMEQUAL"] = "Num=", - ["PAUSE"] = "Pause", - ["VK_VOLUME_MUTE"] = "Vol Mute", - ["VK_VOLUME_DOWN"] = "Vol Dwn", - ["VK_VOLUME_UP"] = "Vol Up", - ["VK_MEDIA_PLAY_PAUSE"] = "Media Play", - ["VK_MEDIA_STOP"] = "Media Stop", - ["VK_MEDIA_PREV_TRACK"] = "Media Prev", - ["VK_MEDIA_NEXT_TRACK"] = "Media Next" - } - - item = obs.obs_data_array_item(hotkey_array, 0) - local key = string.sub(obs.obs_data_get_string(item, "key"), 9) - if Translate[key] ~= nil then - key = Translate[key] - elseif string.sub(key, 1, 3) == "NUM" then - key = "Num " .. string.sub(key, 4) - elseif string.sub(key, 1, 5) == "MOUSE" then - key = "Mouse " .. string.sub(key, 6) - end - - obs.obs_data_release(item) - local val = prefix - if key ~= nil and key ~= "" then - val = val .. " " .. leader .. " " - if obs.obs_data_get_bool(item, "control") then - val = val .. "Ctrl + " - end - if obs.obs_data_get_bool(item, "alt") then - val = val .. "Alt + " - end - if obs.obs_data_get_bool(item, "shift") then - val = val .. "Shift + " - end - if obs.obs_data_get_bool(item, "command") then - val = val .. "Cmd + " - end - val = val .. key - end - return val -end - --- name_hotkeys function renames the seven hotkeys to include their defined key text --- -function name_hotkeys() -dbg_method("Name Hotkeys") - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_button"), hotkey_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_button"), hotkey_n_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_hide_button"), hotkey_c_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_home_button"), hotkey_home_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_prev_prep_button"), hotkey_p_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_next_prep_button"), hotkey_n_p_key) - obs.obs_property_set_description(obs.obs_properties_get(hotkey_props, "prop_reset_button"), hotkey_reset_key) -end - --------- ----------------- ------------------------- SOURCE FUNCTIONS ----------------- --------- - --- Function renames source to a unique descriptive name and marks duplicate sources with * and Color change --- -function rename_source() - -- pause_timer = true - local sources = obs.obs_enum_sources() - if (sources ~= nil) then - -- count and index sources - local t = 1 - for _, source in ipairs(sources) do - local source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "Prepare_Lyrics" then - local settings = obs.obs_source_get_settings(source) - obs.obs_data_set_string(settings, "index", t) -- add index to source data - t = t + 1 - obs.obs_data_release(settings) -- release memory - end - end - -- Find and mark Duplicates in loadLyric_items table - local loadLyric_items = {} -- Start Table for all load Sources - local scenes = obs.obs_frontend_get_scenes() -- Get list of all scene items - if scenes ~= nil then - for _, scenesource in ipairs(scenes) do -- Loop through all scenes - local scene = obs.obs_scene_from_source(scenesource) -- Get scene pointer - local scene_name = obs.obs_source_get_name(scenesource) -- Get scene name - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id - if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) - if loadLyric_items[index] == nil then - loadLyric_items[index] = 1 -- First time to find this source so mark with 1 - else - loadLyric_items[index] = loadLyric_items[index] + 1 -- Found this source again so increment - end - obs.obs_data_release(settings) -- release memory - end - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - end - obs.source_list_release(scenes) -- Free source list - end - - -- Name Source with Song Title - local i = 1 - for _, source in ipairs(sources) do - local source_id = obs.obs_source_get_unversioned_id(source) -- Get source - if source_id == "Prepare_Lyrics" then -- Skip if not a Load Lyric source - local c_name = obs.obs_source_get_name(source) -- Get current Source Name - local settings = obs.obs_source_get_settings(source) -- Get settings for this source - local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load - local index = obs.obs_data_get_string(settings, "index") -- get index - if (song ~= nil) then - local name = "Load lyrics for: " .. song .. "" -- use index for compare - -- Mark Duplicates - if index ~= nil then - if loadLyric_items[index] > 1 then - name = - '' .. - name .. " " .. loadLyric_items[index] .. "" - end - if (c_name ~= name) then - obs.obs_source_set_name(source, name) - end - end - i = i + 1 - end - obs.obs_data_release(settings) - end - end - end - obs.source_list_release(sources) - -- pause_timer = false -end - --- Names the initial "Prepare Lyric" source (prior to being renamed to "Load Lyrics for: {song name} --- -source_def.get_name = function() - return "Prepare Lyric" -end - --- Called when OBS is saving data. This will be called on each copy of Load Lyric source --- Used to initiate rename_source() function when the source dialog closes --- saved flag prevents it from being called by every source each time. --- -source_def.save = function(data, settings) - if saved then - return - end -- we only need it once, not for every load lyric source copy - dbg_method("Source_save") - saved = true - using_source = true - rename_source() -- Rename and Mark sources instantly on update (WZ) -end - --- Called when a change is made in the source dialog (Currently Not Used) --- -source_def.update = function(data, settings) - dbg_method("update") -end - --- Called when the source dialog is loaded (Currently not Used) --- -source_def.load = function(data) - dbg_method("load") -end - --- Called when the refresh button is pressed in the source dialog --- It reloads the song directory and applies any meta-tag filters if entered --- -function source_refresh_button_clicked(props, p) - dbg_method("source_refresh_button") - source_filter = true - dbg_inner("tags: " .. source_meta_tags) - load_source_song_directory(true) - table.sort(song_directory) - local prop_dir_list = obs.obs_properties_get(props, "songs") - obs.obs_property_list_clear(prop_dir_list) -- clear directories - for _, name in ipairs(song_directory) do - dbg_inner("SLD: " .. name) - obs.obs_property_list_add_string(prop_dir_list, name, name) - end - return true -end - --- Keeps variable source-meta-tags up-to-date --- Note: This could be done only when refreshing the directory (see source_refresh_button_clicked) --- -function update_source_metatags(props, p, settings) - source_meta_tags = obs.obs_data_get_string(settings, "metatags") - return true -end - --- Called when a user makes a song selection in the source dialog --- Song is also prepared for a visual confirmation if sources are showing in Active or Preview screens --- Saved flag is cleared to mark changes have occured for save event --- -function source_selection_made(props, prop, settings) - dbg_method("source_selection") - local name = obs.obs_data_get_string(settings, "songs") - saved = false -- mark properties changed - using_source = true - prepare_selected(name) - return true -end - --- Standard OBS get Properties function for OBS source dialog --- -source_def.get_properties = function(data) - source_filter = true - load_source_song_directory(true) - local source_props = obs.obs_properties_create() - local source_dir_list = - obs.obs_properties_add_list( - source_props, - "songs", - "Song Directory", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - obs.obs_property_set_modified_callback(source_dir_list, source_selection_made) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(source_dir_list, name, name) - end - gps = obs.obs_properties_create() - source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) - obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) - obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() - obs.obs_properties_add_bool(gps, "source_activate_in_preview", "Activate song in Preview mode") -- Option to load new lyric in preview mode - obs.obs_properties_add_bool(gps, "source_home_on_active", "Go to lyrics home on source activation") -- Option to home new lyric in preview mode - obs.obs_properties_add_group(source_props, "source_options", "Load Options", obs.OBS_GROUP_NORMAL, gps) - dbg_inner("props") - return source_props -end - --- Called when the source is created --- saves pointer to settings in global sourc_sets for convienence --- Sets callbacks for active, showing, deactive, and updated callbacks --- -source_def.create = function(settings, source) - dbg_method("create") - data = {} - source_sets = settings - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "activate", source_isactive) -- Set Active Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "show", source_showing) -- Set Preview Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "deactivate", source_inactive) -- Set Preview Callback - obs.signal_handler_connect(obs.obs_source_get_signal_handler(source), "updated", source_update) -- Set Preview Callback - return data -end - --- Sets default settings for Activate Source in Preview --- -source_def.get_defaults = function(settings) - obs.obs_data_set_default_bool(settings, "source_activate_in_preview", false) -end - --- On Event Functions --- These manage keeping the HTML monitor page updated when changes happen like scene changes that remove --- selected Text sources from active scenes. Also manage rename callbacks when changes like cloned load sources are --- either created or deleted. Rename changes color and marks with *, sources that are reference copies of the same source --- as accidentally changing the settings like the loaded song in one will change it in the reference copies. --- - --- Called via the timed callback, removes the callback and updates the HTML monitor page --- -function update_source_callback() - obs.remove_current_callback() - update_monitor() -end - --- called via the timed callback, removes the callback and renames all the load sources --- -function rename_callback() - obs.remove_current_callback() - rename_source() -end - --- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events -function on_event(event) - if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page - dbg_bool("Active:", source_active) - obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS - end - if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes - dbg_inner("Scene Change") - obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change - end -end - --- Load Source Song takes song selection made in source properties and prepares it on the fly during scene load. --- -function load_source_song(source, preview) - dbgsp("load_source_song") - local settings = obs.obs_source_get_settings(source) - if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then - local song = obs.obs_data_get_string(settings, "songs") - using_source = true - load_source = source - all_sources_fade = true -- fade title and source the first time - set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in - if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles - prepare_selected(song) - end - transition_lyric_text() - if obs.obs_data_get_bool(settings, "source_home_on_active") then - home_prepared(true) - end - end - obs.obs_data_release(settings) -end - --- Call back when load source (not text source) goes to the Active Scene --- loads the selected song and sets the current scene name for the HTML monitor --- -function source_isactive(cd) - dbg_custom("source_active") - local source = obs.calldata_source(cd, "source") - if source == nil then - return - end - dbg_inner("source active") - load_scene = get_current_scene_name() - load_source_song(source, false) - source_active = true -- using source lyric -end - --- Call back when load source leaves the current Active Scene --- just resets the source_active flag --- -function source_inactive(cd) - dbg_inner("source inactive") - local source = obs.calldata_source(cd, "source") - if source == nil then - return - end - source_active = false -- indicates source loading lyric is active (but using prepared lyrics is still possible) -end - --- Call back when load source (not text source) goes to the Active --- loads the selected song and sets the current scene name for the HTML monitor --- -function source_showing(cd) - dbg_custom("source_showing") - local source = obs.calldata_source(cd, "source") - if source == nil then - return - end - load_source_song(source, true) -end - --- dbg functions --- -function dbg_traceback() - if DEBUG then - print("Trace: " .. debug.traceback()) - end -end - -function dbg(message) - if DEBUG then - print(message) - end -end - -function dbg_inner(message) - if DEBUG_INNER then - dbg("INNR: " .. message) - end -end - -function dbg_method(message) - if DEBUG_METHODS then - dbg("-- MTHD: " .. message) - end -end - -function dbgsp(message) - if DEBUG then - dbg("====SPECIAL=====================>> " .. message) - end -end -function dbg_custom(message) - if DEBUG_CUSTOM then - dbg("CUST: " .. message) - end -end - -function dbg_bool(name, value) - if DEBUG_BOOL then - local message = "BOOL: " .. name - if value then - message = message .. " = true" - else - message = message .. " = false" - end - dbg(message) - end -end - -obs.obs_register_source(source_def) - -description = - [[ -
OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian
-]] diff --git a/README.md b/README.md index b523624..68220f4 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ Now hit the refrain again! ### Song Title (filename) and Lyrics Information -![image-20211022011756528](C:\Users\willi\AppData\Roaming\Typora\typora-user-images\image-20211022011756528.png) +![Title Lyrics](\images\Title Lyrics.gif) The song Title is also used as a filename to store the lyrics. If the text of the title is not a valid OS filename then the filename will be encoded to create a valid filename. Alternately providing a valid filename for this field, the actual Title can be included using the ##T markup. Song lyrics can be added in the dialog, saved, and deleted. Songs can also be opened with the default system text editor. diff --git a/images/Display Options.gif b/images/Display Options.gif new file mode 100644 index 0000000000000000000000000000000000000000..bd6696fa72b32fa6a7d18ee198b66f1f067f99e8 GIT binary patch literal 8310 zcmdUz^gU%v;WB3pPY^2p~jP>lz?CtFAY*MZN8v}#DU=S4SobT%D3eKAZ zS7U;tWrC#5f~13jOyPchenEkVfDCjH0uhAx&oL3<5z*2z(bDSB|3!kKAZWC)9@IHM zCME`&HyK@P5?$*AZEJ(JVPMLvDJdzi;Sty{25u??H#LKs2El#dnIAIY2n>7-0Utxd z$1wTMp!|sZqN1YwHcbA+Wc~!E!dRxpnYF@8ud1r5qSmQu%&cP2u4ZQh5h#NQG(!Xi zArj$;472};8X6i9WAG;QIARQg*g+t6(1;yO8x+(Ag|@--+w#C|dHMgbAd$${#|u%G%!g355J5<0^So#BTrDIKY<8Lm}FBZANf#PFcg$S@2& z21k!Ijg5_=$1uaICc~@t=p6)l2aVprOy+?n^CBnPpp$KFlN0&>CCtvwPVThL9L&rd z94sv@VN#Vawd$B!6AU8gKP=3k9%j%4GYrFw!7*cK%tStB)daKZh}l74cG@sI=!1h9 z%ns)E_V%Gx{b5k=Vb$be)$!rs;eT87|6Iga99Ci`wU*MM7-DLEi}9AS;RN6_jY757 z^3h~^QP+*})(UhQ+nXdN^|s2154`&I787k%Q#nHRlZEQ-)zbx1e#aXV?KL`3;>X0y z8XdKB6>1;&Ehjtb7HV`WG>SAj>lYhL+Fds%I~$gpZHAMW1!$o7@sA&i{hsJTtRua@ zO%_eHD&lvNUCwV#A)B`cAoxMO4ZtC@AsDrQ)n`=eG$An+i#O)!H5}Qu6GyY6%c2VMYUf#!vTt z{CU9UNL`7zgT`r1uwoyl)=2q&BOJiKVb$sH{nHC4@icA%_W;*>n}^Z=%=p*=Ta#Sy5TTNWRA1LS%HMJ3@BH^F}3j_-jX7!wz*E(r`6{C8^(!M98>hn&cz zZ%3}5j*TgtBUVaXIpDz3U>2igJ6FQpPlPb~+rw{ZUM{@A1iZ!C$>^z3aHVh8X+q&+ zONU`^>uxc=N!`lHH@^zxh`O4Po($ zt@a&F#hX94le6tpMsBB_)Z(uwI~S~0fykA3fiskIwd&c&!|Ho@*Kya$Sr2n7*?I5v z(o}ly?S8|{zTamj=L6qw$u0%~JObWB#5}V5L!^>FyyG6dettRfSs8RWN+0FqGsYCO zu`TTJP5+`GS=2mhM*05wt9pT3{(_0l#^Tp!g+7FfMvM+lOAhxvH*=QB9Fxmx33BGE z-gMzzbIvf45vp>?Tp-pt9DVw$-!Dw}7nSli1ai((hlq}#v1N9SnBslu^>${3m?V8U z&{J&7lx+*SLLx2*{FbWoHdKk`^Ze9tysWp(ak*Cql-zaM^ZvBl-|Ej97nd?lEQ;+a zb1n)T*+CN(9C>pH4}978hm%;}N$Zk7Nc zy@6;01zfW6QbM|P2$92iOnLg+9~HGvRQVUNyr42tl!<;kqiX#p!vG9(d)Hr9V0zv858McJsM6s}FP37DQmJ+QYVY5C*b zkW#5iJEf-veBEMmR-ZCkyUIp=t`huXGF}NgPf&PU2Aqlmsxs*Lr#&AByigwnX=gyt z{u?55zae@I87T$+h18GdMq(^xvzk-5&dyL%zWF~34?#6D%hF2Bp5r;+Vl;qH>-z25 zUgj5I|9^$D(G|~tzS;=BDbV?} z4M+Mc#Q$0U0zhcWxMNq#gJeQrO=wzdKy;V-!%uS#?mJ^3&-JIMdi(0XMydnhGuPGC zkJygf*OCojXC>6D3S46OaYU$}6}nYw@1#okF$4?vxuV5HQZ=%(b3eJ4G+|T=#8eZ? zH;qq7EH>kHQN7F_Rq`YPN(PBb-CLaA(d~>|D@%*Wh{Yxd0hQH4S6AJTcni#u-Wavt zNlAa(Y_HkqFooaQnEo?d$8f`%XKMA*fL0)<$jnK+r*|gb$a7d0;rb_QZLzz{5)_6- zxRWoft*+6UxAq}CCo z&D9|5TlNH&!X1EIdJHd|&XP=PnTPx$E_8Xy%9@GLigCr00?-DXxUuP-ec?6gdY^OM zh3q?Dgb{FO6qGzd4bs+4Cp|9A*q;f)&z&k#I1?cFmw(VJcX=MQ%(#OUf(@~Acq=}B zWLJ`z%R!C8j{=C;(7;d}j5|#VBBb%1q;Grwbb7l9eWoV&TImC*3rBGSiB5V=*g55W zb+mgCAP9P#xQY05{~p3_uoB~(+rsN!YJYUK{8E0)8^U9gz@th=my#Qcr3DY+E0M}%}c8umLx=yXF!ay)3(uCS@5GM)my~k$nCxD}b zZ>!=B4(4x&IM?lU#{lygEe|8H4(&KlTMKwuZ0n0)`P5KC?N;=eVUTDwwRtqc!J5pR zxQ}qFIoHF1+TVF)nzkb$Xw=S)dVM!qyKUq}ItdGJ;k1TR#|P{}#O3=}jPe~TL&Fpz zj?sfSgn*APbAD|WMjm|M{&9LyH7V<@^elKs$@>jT0PB~0Kq&p_S4f9y%4PLH(xAqH z)NrCUFyjF7T~OlVNKEgQ!BjBYHUi1(3-Ws(@$se9f%hV-qJOIO=5Fufxp71viBJ|J zMcSP z>Y{##zkX#s-?W8OvT6jD=#DVDE*3l3zsf5K-eH_M+Imh*Mfb8LBHb|f=_=u`rwMlx zugH5>jhU4uhCgw(IHc?Le0?%67}Q5*bqS}t zZ<)htHBCkK!~!r1?`}?uld=CijRuBaoZXz1M9lqsc*rY@#F2vdvO@sD5D!}jF$yAE z26^-w60{cul!E$WAT+^H(PvPa+^DfQD0wZEf;Xz`O%%(a2pc`!DLPW%S>)!8B}Uxp zP(ND27AiaxA#)%+CmC@YXDwnvEk;Z&0dvqGqy(u1zu}EF52999qb?w_bzLx^xCoJx zqJI6GN-BYDZPE6&j97C8D6kXyBq!FA7x?FrRfEUlOAb}PQLMdM0D+Iwg>4X#UaT-T zk!ZH0#4V1LM4X6ToZzLG2^jyQkQ2X=r_XFWmXs1;a$yHrivOTOl-U-(X9Rn@Plb>6 z-=U*~9mWIZd<_E~-6|YK(FrHM(cZ#t&|I?39Dz4E;K+U3c)|c}Zd`L4CI^WCd1M&M z$fXsDm&69>^NF9$b{@D2q%;OgUpT?JT|uA1+H&zbR-6JPoLz5|402e50*R7QPM@n& z3{``Ad6|Z4NsQQ>130{{C4lu^fX02-N|U5tMD*n8vZzw4I+IWWTep9$9A8yJx`%=t zyYPGm38{sMK03+a))L7b#i4E~88%dp;k8><14%UM+n?wc@k|7EF8Sjd3>-Bp-^?ZdYa)pXKolIf} zw+n+CT^mTK$ExkjtW66rY;jMs!F8A9f{($t5`0*HIS_F>(3o>60v7N9i)tK=;|l~RGoMq&Qo5vuRfmo{C&&Gc=%!3D;RAO+8;#C3MMi zOo;$)DKl==475~WDEj-a=!1j`{;~@GjOgV(%Hb6z`AI4clUJPJO4a%TvT^9WY8A|` z%51IbDYVk6zRHH7T1BSXVY0+2v^p}c+Vw8geXTnDceMzt%9g)|p1#Uew90X^%AH+1 zk*iV=94U;d!9tGc$}j9so+v@HV#LiNOp~C23{(k8wf&{F4{eDHzOhVmv6O^y?_d=f zq*Nc0YO_fbT-0Jep$i||vc9`|kosu1HwxJ{l3*GsF^NXpz%u%{h@*zMHM*g0+u^mqaTFUqsq6Hi-9m0? z4}J2kMgS6)l9TOz(w&Td$KP&}>;O)+2rfIG%ySD2pz97RF=E;YEzYEMo{x?{bbYTv z8$@LW-W+!+y>jx>eGj2(3BOJ14h9oue5hNp`0*R(k4kOTBK`WE_2y*wST)NF^`i4J zcXbtPAs<=gBf?+?z7GJxDB`2^Q;9-c>2v~9f8o5Ml=5Kct-Hc{@Qc7Ue4qA}YLC_U z=X8JC_&&lV-Q3Tli*S!QmZ5NBiB8_tbQ!V?Kw0anj}mfqUMmc3DZ*{1hHW{A&9n#J z6FK#IlG(L<4X7co`A4{JNC$5YzUu_OzG>E$40wV&te)SuxULLo3(r(XCFbRO=ViQE zL2>t>9HhhB*AnLse1#4|mXoRnj;URztlSIgm*zY^@ddM?3;pgDCju zsU&&dm;B;1eFLU^=<~_Vmk*~`oj9WXsI{{$0f`Oi zvf87sD5z~Z$`)w|3P-}O?g9nLdj9*vTuKA+>PqLNi^uB<4G$c2G#lq`JA$*jx5iK} zZ#(vrk)&(x=cdgj>zUi@?%WC`E9#zvJ|z_CB}?5Ur3Hxa*1luI?s~K5sPr;Ii!iiX zQ+{@@$x(ZtCJI=S%$D^0ITh>g(Eg+XCO5lqqVk~};$*hM{BUGpmU+u9Wozxz;UsmN zmw!un8&k!O4J7Vr>FDcO6^F7kxCKI}KD$!sdp3qgx#mVM zyMV#`q@hWDkpwlYiZxdCBd#E*=if0PJ=*s(#NPr<9E=VIK|*`cI4fvuYJ4ufT zgaWHWQz?A%3pSn=F;PNbnK8MO4Q7+ANp-2e>uVVrn!{~PH|!q*(r{ILcQslwX`AW8 za&X;HQ{is8+4wX9tx--N_l`qZEn2C=82kTD?KbOyYK&h@8`{QN-F*uNA9BBZz0J5!ncUilqUxo4W16XYR4|${B`q8F zF5%;V`a{Rwy$};7qQwG zL%0x-?EmlcyeD5i`-^#S4}Gr-e!t61v*uuXLIyS^F_oz3%O8(zzJ)oX!Cl(o-IKt_ zBppwm7Plf7wwXS+snj({`DAhT>+TPaj!pYdOa~nen_PwQB`wYVwR8B_#q_+K#iBIs zpiHEGF$Z^s!geCmQX>2mm~3Gg^-mINxl$jn0`o9S>RTyXTuFUln$Elm6JO0FH_47z z4ai!}KOMK&T-CiXHKsIE`(Ry2z6N-?Y973YYdLJ}MPSV zb~&wfxg&RZi+B0^cb{+VKK-gl&?M0~tu1EtO`=#kuZo5VLYzRe4HDMrlY2{yyG7rx zfDUsD{N`Y_@5G|k z=Ror+5=X8{O;Z#!D~F-%R979*#~@VtO37+OHLyE!DuLQs_*+0z>|U(ZVSMDF0Lwmu z_`dVjVJh}eICK9kJB~pq|3!o$_xGLY4=UQ)2jj6iBtyzy9kdG1Walfk+2s#m{l|@4 z$2KhA*a1gv@+TerO46QcWT{77l}GOlj;XlxZ$$O(IJatb-}*JZ9To)QSN))Icuf;0 zOmDcu6tIap`=+TVYi20LCtmye#djjy&9{yQ&%JRQo}IA@0`G_Q9e@VfH2TjkFyE~V zY6XQgMh&!;Pc`QDjk63@hYhEhzmND+LA528PNyfPjm-CqZck4)SvG%0ZirBCgm$iz zv0jiL;x5r;LtY&E`+jpFb7O#gtxOE9hUVd=XVExt3|a26>69y_`&IodCJ_6BIG5$mR*_CbV{YuA11w9 zSm>?oZ_iih?F57#TA^;^UPPl_$&Ogs7o(TIgMvJ1XFXotIS{WFeMUA8c9s7P`y!c` zu5#bGO9=0$c$cd~ollYPJtc6_`*;3tciRLQmSLt@c#=>ZP|}=eQu#!za-+-ZrFJyg;B9JD}=K=9|J=O-bJYOuQ3AH?rNOIL_G^k3UBF#5lF z^n|e~*sy%cIu0u2}B(eHO_~agAgY@12|T*fr ztG%+kbJuX?=%crL<2+-_DMZVv83Nq9cJ}gE0H?bca)0=oJyE{R>vei;E-_UMLwE%{ zan`!$$JihCEoJpo(Lg#9^+C2zBp6Y}*A|(Y&B}DvvBD}(SrxoW8Jr7U)66;xQWoT<0bj+5Fos9_sLAJtgG@efm8iyVKG}V@7Z(JG487zN0nQBD-_PfxhY0*2#1%2rsfYcj+0oox!U+7QdpY<2kWO#$ z3H@x!b~?MtwLW7>eI;h^@Mbs=ElHZslH#&$Q0=-xf=FcKk_i%yThWcAnPylv!vd%@ zT|bE8-N`il>Xi0S^1;4yW)X-t-@V6cXG5f5@9&Od#H8;g0Oq+-plSAez}`|fd|>kLH)s(TWIGJ z(W@6cFN3${ld&F1#ChimG8RIeDisr}>mZ8M$hm9pR@{y7Io`mV@kT)lwq%m=zE}mW z4LC0ZzVL}Ue{J4UEBTh>yqdSW&(O%bf=DZzl`b{!UFFEfdzMvEYO>{R*uFUM<|!xS z0BO9hZ8oL3Jwh@AL_43JueLf>C)`7F3R;^RmR|P)ez%8Dqs!=@+ z7ZGtFR$!0OYSRNyPrTvl;njF|-1vk}fK4Bs2DBdIWb7yWqpks_Ch%e0TSFklN% zBNbOS%J_XApN$P5mS#F55N|`XxUnlzK!W%B@G{@@uQOA7ot9#~OLnPEv~byMf*#dR zGb^AYy=z&lUeX}#^fiQ+`WPA~;csgNsS$otN|JJ{pH$um9o9XG zIMNMD@+rlx?I%Chz*p4mAiSm-;s@txWN!VeEM;pjopaR;V^MrWKg}zbSdRt3Llpo@ z@9oT9Ll){LAvrELCojp!z8GvNAPUs>Xv!eT)*{(eu7>=);rWw8D%rJ*plUH5yp<6% zqf7?^gwy67pd$LR;h3}``7wXY#6E7GiJSnRi~3scJS|-pa}{_~9TakFloZBXBOh0= z{^{zfT^Hu=mnB1*)Fj%HxmP9EP++_tzhnmaci<~}vf#o21@@AEb6a?_PCLC+tfJsz zol|cf%io1OfB#Yn9WuFloY^zMJVN)qVbzzTy-;`djo$RrjU&(Hr18ZU0qBBNBf_sJ zRc7xXl3wlL^5x{?2->g((RHKvRLYSVD&Y_{ ziV<-6;q}KJ1m+E7h z>I?GB54K5>pA1I*lzHIJ7={pz*Vx^hh$s7(wN$CJ*mC0TW0!ZTdJBEK9l;u#D!O5m z-3?YZbs<^D7lUg{$kfV4H)XerowenG+lnTa(UbF9$`!QAEyEG7JNSu#)|hr+N7egL zN6|3LnXbUD&IoRIul4n9?px%j$FO(W`o@drzdkN%DT#Sgx^13Edf;cq;sgz^Zxy-Y z;A!ka@W0+~Jvi-_e?Rqt(Df#0%Ru_|q9OQV8yJd&;1Mo)pCsdrT{?8|n0UWWYS6|v yR-%w`WqDtC&c=5h)sRWUe&1~5#vZvbYRFOEFMoMsUlJWM6VUJXoB)9H>i+e0YOrPfk;VxP)R`)VS|m1v5})oQevZX)QBP7%}5cX zYt%qML6nj6=l22ppTYliypQw7b>lp)<9gq_v~)BT6yJr@&{D&xsQ6E5sj2=E|6ly? zEaETKUtS(wULGl49uTjjA+M@6KOg@+{(JZN?+e@)cp&gVL_|bXMD!mqQ895*ad9zm zDIQ)a9w{jvkd&m9l%%25KdRPJ2Czp`kN%Nh zR8ms3P*Q{|nI|cE<3K#TARZ7%(hy_-16f&vtdJVdG<0=!U%Yss_dgIlJ(M08qldy9 zO7a>?f()M;8d@O@p)f-z(n3+m!aT{s8)uO?V6Dn)ttw@$3bHnkvbF+QLt)kkYiq>6 zMELE$0je=`I#kb)Vgz+RictUxd;YZ%lJ2DOGE{tXhYs03HEfSV`5 zy>W0H5orZNLJg4!Ya{}OL?BWByATAWheCldC=?!r9zx;qDEz-9CYdWG;gXX2i5RdR z1`NS~Q5b&=293wyi5LPNLm0vkh?zJd&RYrh&ngj*(!--5_Q?-!t>Q0dTdX;7u%=)jp6^*S*C{g6Z@d?OPIO+yK zE|N~Sp>nCk=ji*=`PUG@6;?pAYyzr^*b{k6%zA{~9!MJul%D<4h~FGemw_&qqfniB zfGuYp{pQa*lO;mo3I_N7Pyaw&9r$qiDo_u|0mRyN*}IlqHQ zwpqP?zo`Jid`{o#3pwfkq#OYJ1Ts761XYwzdIYULH(0?}606^M&^~0d@@Tc|rVBrL z-ayaBVnO2fpsNSJbz$g+zYTa0fY;$!EFI*I%!!QTr!{?k7b@eCd`7t0ZmCVo?i`FdE`h)n#($SRtOtX%Em$S5yjZvJJ3D#TxXH8 zkQ^zW@9+c@L=os`ANgJdl23HER*weFTeE_0ISB2|>Eijiy(_DT8am3l5v> z1G?=V4hp2f*aii$6~Bg5^G1FROAU-94NK3!jz&2qVvoj@$*~DzD!_-IM?p87eoqLp zef<4Z&#=OC(nxD`XVTcz=}*wBx41vk-FBmYW^AxqUb7$4KF-bBV{ymtAXWRv3!Zh? z$cvuq0`Mgt(o+u#6!3kYyrM{W+I>Yc&+dd6DO7o~7AybbWIaJkqR9lHETyjQt}1o9 zl?nfGO3ICpINL6K@rP>5Pe|uKVo}47v)$?eiSxbBF@5vnrMz_i?YEMDod4_uO17Gn ztk*AV^axd5{2Gz}d9l&Q8F6tmW$JSIXV#YY@{q;ur@tAk*-dH_V*KCB(`{^r|H`D~ zbI={zUS8Uln?R$#uYL}wYO;%%y{265+@iYCX`_OrIuIql$URvUYQM5}BfvIN{Y+3> zZ5C(fwf|E0k_*yt)%{BlBZ8HRriRAEY>jO;+3D$mUIYqZ*Z z>^ycB_P}w9Mq2XY;eA`p^Rx+b>(#g?DcF}Hx?eLS z9-|+AP*dXG4(5O3A@badU$y71NojOlsmW!j^*Gy9cAl zQUs_4X(}7POPf<3vhO)HliD!JILO@7eA_&;W?xm+Qou_qi|H2?cglqe7%zA{ zQ?VMldNI8-u#=R6Acyc@uV7cx)9lJTv$R#?*X~S@>uT~zDdUOnwmN)$*t*Bf`{=(& zR+YHCMDNTEj{B}w)E#bZzZk3C*z}%m{7C5PenQ%c6H#YVKk24;RzH(kZ1MVGn3(KQ zYN9zvWGOK0x%Q|g+H~aooq}}!3TRC%SZ`f_`3E1mye1AlV!M}h9o}!7qXB>i@o=6F zYF92%i<94XDxMPVH*tQ#>TO;lRSubr;WY~@HYH5a!`9bple@&Xq)GeEl0hg6)AY!e zT+8XGYg}z6Nspwoa60BQR+~*8A*l>g0VJhlmNt%QR5{PSL>hd~XB&m604=__Bxhha z^>=hF&%UM`crx*E?}!-+Oy*v%E75XtFlfwl^_Yqr2KXEXMUa@jbiEBc@( zlhbkal~6wVXPs&QB0#o&xOo4GWdKk+Yy;DjD=h9ACrP^Vw3=;f?}6~e zT3RK7H+|pD?qwE>Yc+Ly%cYYlRv9K%RokXYbb`iT-s;ypyZdX&4Q1H9!nM*=v%vKO zy|B3F^iyey;}TXz%36Q@(-%)qp?5qw-L+wCsm<_rEtd%xm8lNlU^x8QvCH}6r%CVP z$1!`k3+kI6I(@&9t#?uxyMH0rT90R7oqo$%84I>STZX>-g>I zmgPwn^W*iNr`{pY0xx>ApAh+PMrCOY9Bd29%@g=8Jk~E)&KmqYP5l?J{*WB5IvPzasOqrq_Z zM!4Kb_@jbgkMszfeS~yK1o9-Jus;G$7tBl@8Eyup1k*)?o-jwuM*5~l>NiGSuYnEB z_`GijV5I}^Y(;jPMD?*nCNxS1Z2NC+1)@u$(i@SPC6En!=bUY4Y&tkkI67&T`8(E4 z6YO##=hmP_?`Gs429BZ^^0*FkW6^RGY=};aj0qt5=rgAO;#ra zF`9i+n+rObh1~)Mw2|F~(9z-bNyoy8jB1I{ey@F@WF2`ptUB=w47(ga__GB86hCP? zq-r5jHPES7xBYKyJKbdWp+|obL4b`FrOA;FcZHK4`=s$Cr=@8I`^}{Vn1{V8We!27 zN13FD&m}2sFx!@-C!xV!gXsny>Gc0WlAF-*t?*QWgxVlFpB}7vg1)^TR*c3(p)h50 z7~d{TB}ax(2pVsmkw%?SkIs04%4nX;c+4MOz>yiag>F|Aaum+&*2(PC5pavl>@r6W zI|u~rWM;Bwc}ZtY%|%XAbcANnS>cUY{W=-IM95U56F)k683C5#3&PW7KbOw3ouS)A zfQ?q^VrODK34Wy0=no1`+lpWdEnx6!)Qz3&XEf>8;LeYw=#Ct~&;!>mnjf#t=Z4Ql z7n`NIq`LkZL;|8^*BqGrqizQH7=+MC^Efkg z<-w*X&3xP=2XUIl>9Ns2o%7Ss3Na=ZaFgRKh2nQPQnW?+mP!*<+(?e9a>y*YI%D1W0>DjnXjsvL%T>8sU zM%BqJ7}(uY`rp8yt896134uiI$}=)>5K?F-g1ab7rOf-^M|-+*aQ|+=)y-!wJMyF> zA;EJHUv&2G^y;3P>Hxv~Pt(=R3t*P=YF%Asj$t&XCC}(-O$dD!|2gM?4&JXcQ%H=$ znOBP6F~dAukmY_-D+j&Vil~*(s8xDWtGrMvbY2T;sZoFOnVvCIim?U^`3!qmOOuDz zIj<2P{_M8?S%4Y+Uq-gzi@MsKbk@W=(c!u(hdP@V`1j>(td{lVKJ|#iddDY@C-lkZ z=3rP1TT@w}v}l8sY5Ac8#NW3XxZvo|7{gA;Ig83`H)*gEdn#E9wvR|2tZ(2)=Q7FD z-G){$IyO$FkIX22%vL}uE;xvGA%2gq63aQ93AzLI6H;<{ zFdy*6q^0JC&pLyf9aZd^1+>l=H$hFu%!aJxY*3XSS0rphI9y)2aW;_h;mgKA!u9y2-FcewOCz3L9V4B5$o1pp(2t#t=jBfjgCA^8` z@tDCU!7}&Tf^_k2qoku(gd-r%F~6_AEa^olm2V2^yfHlji3DTcpXRf3G~(^C@4ovW zL|tBM`5Y^j@cZSY9F%b>b*dzmuao`I+hwl6j*sNVcuZ+J*_)R{`vV>=GXpmpC=t{{ zUQXxp?fKnfYGz%`cQ9%f8|hXib`RNK=}&F{c3c7H0}?Eps7HKvc0Eq$O5A2T;CJEu zLjmt&^RHLn$Q+&LXH`=e|K+rPdt?V5+kU=KvT@!aE8i+g-$!})rZe${@A_H(F!k9Oeu;XTFi^{(6f2%@lI_vm3IVtqSPWM}5^jMz=HCnNbyb$wj7 zM9N3<73)S9StJ=plSOJL{cb(a7_Dp?U97+?UyK%WjIF`Y8*j$K5M$eZY1+`yxW>=B zu#s5Kn?}21<-((fdo1^%<8gx{r%boHN5;iZ#!&^sH2#saPVxZ%FTI&xI-0(O5V{6; zzTEI1;ipSJQW|7m>dY0K2+o)YIPPlDo1nTjQ9T2@5kqlUtS=~%Z>X*RnlA>vw}cb0 z`g+GI5^LG}ySSB{>Y{GidvMp4Z=^Z3WU_p5azoIau-yV{m~7q-1nMDw0+4#6ol^6Y z)Qp9?quJi(lX6eT74{oz&fK>e+X$M$x~732#F0yuo)5LArGf1~e4Jk`2K|Cg?{iM# zkEd6{J$BBE9%Fl*bZ6Qw2CQ-8f)6KwFz*Omfa8F7SXob;5L~;oC3ZC4H7$4Fv?Ri! z3wb$l==d$EIsTen_d_M*r}N$j9E)*gP6j(7*%XnuKQ|LyVic7ms{~XVDIPv^%Ncd7 zX;?_??8$IUz~Vmjm(7=dWWiZYS?c-L%lDsAI0Gaj`g$W4Ohyn(a>z9-Y)-#PBBIQV zYv{7MDU;-`=i2W$nqAuFD?Iu&ylt@}Yc_(hBkCeRZONCme44D)vxn{Cz!9#-bRW)F z{ygj7xHhoKUA@&p_&UsOYDMsDnB~JQ{^GX#Jz5~K6j=w$?%)QqwX;w^b>^f*wEFeL zkcpLd)mHG#DiSQ;{Wgr! zH+08B^_DlZPd1DKLX1^5h0QiioKgFF8#{F4=Gibxhr~c!qp*=CBr8e!;pyy7J}6G#+dN(mtb@y9*B0uF-h6Zj zy8gk&pA=^qje#ypU2~3-2u3f1V=uRh?(6`F-6M2?PcMgLS7W*@r^LTcm1Rsl7@8W{ z`fiprXs7V)gVDECT(h~;clA-(?mCN`Yi|oD@BbW|oh{b?p{NJ&oyP{A1@>M4iF;TcCe{^tz4<+T$~Ejl zv2f{n`@RhOVzKt3dTeyaUCLb3kJ4PxvgUZ%cI;k{^=^yJ5JG3F1YpczIKL+?&e=KOowhf)n*QZP2l~Q%;dkLMbzypoPM1E zo+NsnW6;}ATe!jV%hf~2U8c;2IK3t_jsx*3lHW2UKmx-p;e24g>@g0{kU$Hy!x4dd ziyjj`)(NO_mBD4^$JQs|KI@*9>#;78cG;WXxK0Bh8;JPL2p4-y$Lag&)9jyixx8l< za%Y9{cEy2buj)5bd7Z2)&vIF}aO0@t8|Mmg>$M%IIG1x@ooIa zRl&5tmBy^E4=*nRpuM>Y6zK9refp%$&~C-~R1LI)Od6!(f0o;MH}TV8(6623;s-vg za`zxcfNL7yd*6r}#hOKi*{f3tp4#ttLm8gl=`Bet(Bj~{?Wm++w55lUb($Avm%$Fl z@;yIu)BgHkIODlp0toYJ$GBKOOC{{?>+i41UgNC$Ft2w_Dy`ZAu5wO8sS2r*l13d_ zu^OCGB1sM$r38US#}U5ZmX~XtnU7f$L*1jc44S2;* z%ezyzX;}EXHay%0@2Qr#s!U*Fw{z=FG)f*tRcCIuO2~3mRPX+hmeGGa!v(7fe&ylb zEuW}N`Hzq5XRnOmQ*!T1EtcpO-LAG=g`Fo~cpP_HLS|2B^HJP+7TUiB7J zrLHS8u#JCSFDRC5S8oo6GRRptV2kR<@yvR==1vVx$*-L|uA1T;2Oc!M@$leG9dMno z`}c*M^Hkm%K`Lq}^{fphTKSVV8{VSdm&n_ofN{sx71jsx@50!;kFN&vhAXVyy{4i7 z2$$?vu+4byYW6-}70@If{Y%3BsX5Z|9ZCC@Lpo#8$A=s*ZrCqFutBoUzid60u+<;&UEaMtg z7nDU9WD~+X68^-wiGQn;<_e#-qe9Ug1wN{dzW`;|E}&4WYtIcWWw$;yaI4z@Z-lZt zK?vLGJ}fz)>@g}&YV{abHC;u0KFV(KoHPtHbD1&)xBY>L1iU0s;bZGHG%$YjUY*a;yKM zf}D(;iIItek%N;R3aFrt@vLKv9`*!x3{+rX|~P*S(k&o_yxT9^}P7K zymV9D+}yl8>)cYtz3S?`>cHM!-v0jnq5J}&{Nkbfav}d?U>%|u5~{5hVwD>l6dd{= z5{*J~K%td7p_LXP&CMarppa&8v=n)?lytO|b+i?Ev{iI;R7HGzd~6*!dZZ$H1Q(gPTJXn?tOdLqeK!tebOk|6|?W-rhW!+dK(sn>}sb zZ*JZPHSdFadwWOR1V-G%N8IFqQPRMuKwwn#NTT>iqS|?(dBO5v+8)m@$it+ur@%{1feK07;8kCa?Dk}#yhk%-! zr^ix3lex3AZ3_zvp#A3A)7jb6)7Ag53$|1%IFP=tsivWjPrAl>e z8l_~9Q;K!CqEchD_=~Mhw`X^jLgXwLbP|>lb1E!P4Jhv4_N#cf5{p zt@lIfETre1rn4!d;YteCwwyMQWb(NNS!9abVTLk)*)KSJBjF_i4WlB_W!@S_Q`pOz z#tphm6vpy5P6S6y3O^eK!$D7gBM3R84g>gzRlgg3rWJ&hy|>`0Hb)rI?Z%UVjNv_N}5H zU5lYgPu6nGBHuf?KYCP{4?H2KGk5F;AYTJ<{?cCeeOSI*q*>!lH+5Z(NpF|FXewM! zVEsAY9)EX?x)(hq)5i#a$0un?99WSd&R)BJX&zX0K^E`8#dqNTfzKscKh{QTH{*9M za>2d48CdBa?*xi#ie^2D@fcng-r;o9*4FVzgjkN3w8n_|K6QhTbR7mP=<$(>o`t)t zAg(mzSI>I_v@BRb0cNeOO5qCe_EZq)>z6~kuM9t$>wMii71Kww=TnlsuDc9FgIM3r zN~7p}&MPbGyA_HWuHXJN+AaHDwyrt)8CUGpzh8IE&A#6ZCSCj80`YI2Z^!5xdMhW` zZa)4V@qGDwST3OpcwDzK+IZaby6H374q}766rL(WUe6A31K%z?f|lN{$Jv4uZs(l- zeNwNe-~fmoZvN8&)Iul(*+Ceh+h9b+LKyssL4d$*2u6G%9KGxivfgbd!E_;jzA_kK zDoXi=jr@;Sb{H%7Hk?7R2w8h#7_a*_0{5~I!Q~qfaPT&gZ@LKGYhr}#?KTR6mu>=U zql5AH5*Ws+tes5`WI)4&K~P#Ya!rOn5eb8}S5$u1WL@dSrM z$>@%0VT4cw9RV;AW1@tS?~``crK!yLC(J7ce@zaC@h$ksBXpW3G05gR2zb zIiWLoA1M$L7-XX4rl{S6@!OA}Xls#4?2I3RI?d^LZIK2WJ$cZ8=@%M=8<9s0R7F;g zzqK4T2Fzx~Yun82c#@%_I`kfDDA8(-5qiXws4&Wqa|Y$j2t%x2mXpK%>CN3h$^~y4 z&2?}z-^lEi#W`1-_>UPei*Mz_urP||j)G}-ePPY5gOQycAyAwr0-7;nqHVPvEIQQB6m~B3Izl3aDNJ!qb+vdiTf)sl z1U#59Jf-mp(v)a?rvB*OglhGv&4;U^VXY*JaMkK0T;t5g@-%Pa4Y>CmQ;+86+cHBJ zlN$b!B;MGAlK9p|GI|q2&#%4y^LdzHw((!H-QX2bIKgGxxq{inj`{jS`iVuE!knFK zDRL>a6pIsgbg)L*Noz(x_=QcG?_P|l2dty4IE|_30^x+|yn7 zrwqI_f%b^btgA7pozKpdck1uI~M`hk$JG9!Y$ZTehU zT|IaeSk5aIlZ-)kRK-+O<*Ot(SiQ?B?c?Dg-t=~HBt$)ANm>)OMpRG$PXMZKNPc zC;)#S;ZsQ};`_$|!JIb~HTqzTdVc&1O4kHL*YqMK@8bxKksqWz>zX{(bIhLfDxtyp zH+L)Pgm=#8-FY`H!tNuSY>ofcyC3`aKaw-?FJucIuI_kr0cVKcO_wxZ9`;y->D4QI zSG>GD^38=K^iO@4XS~O#*yb<4NcsUr>U-+CPDn_-Hd{m(S2@bcJA~e_#&CbH zZ8NC098Nv+1`Tsgm7M|qau*UZd2XSBg1^}|y%DdUJ0BzV_mRFo^^%aK>Ipj&n;Md| zRu5x1Iyk!aR%_R1{;b!o=lK)$>AON_h1sLwX@C@e9Y01^-)(7k;q%4!@MN>5+QX80 zIr+WstMlKsul`CRdj>{R!wq9j=FIGn3+S$?r>?#X+hz~qKpDKX%=mOMBM0I@*#X?5 z!<)Ub-+qqIDjwabsNYk3Lc*=ge%{U_2rkNCJImO2X&I|OY4<+s!IQ#+75 ztPY}Y3a?_?lj;+_u}LL#x4Bb`rA^A1w;rDy>1{;3dC+W|F_);1e73dLnV+0!RO^$d zDnZ0oZ+s1JqdsZaMoWdsNHj=CVHi+byUWWSMb>ZK*$(bWYtmC22ndNLP#cR>o5Isd z3Xe%4fc@5JgCJ`Vxf&3;uHe5ORYM)YX(CQPA-zBwsyrTBeHhCD9j7cS7p0@nx+320 z9T#McZxkgylr7MSh&}2pJ{~PF`GO_g5s#1KKJ&sqhnTRKEw-$aP~euZ{gQxvn6S^6 zSVWb0AD zyu@_VW%$#JI%5S0UD=SZoI9dSvja#U1J)3<2&0W(vjW0j0$b_ed!EA8W4>#?Vrs~S zp*=-+W`;2##h7ITXHs~fr)O#qrpkatFZeB1~2g&Ch~-&a=NEF&xq56dIH%Nz6&8J@%h>gFH1%nrV1gT;LQXJ#=$(rf4gnyx5Ca)I4r~FpK!E96#Nk4S zbKy#`S@h6SgmzIxm5XI@BxvD|VH=7q$W@H0TWn@U);Wy&9fRaSjQdN-SCkUW6&KQ8 z?r%sYCCmcetW3Xf@!1kNxn7}5QB#V^hsYrd?5IVhV%;7RZ%(M2ywu|CuOZZ5i%Hpw zY1LuM>|trxn(4IEX$Zn-bPvmvnyFQ(Xk)p`RJ6;aY|CWzD7n*0G2M8)@9EV;OB|LM zrWP0=7x%3EwlHZ}j1HU(--;Qnkr&L5-u-cb! ztY9&&7qeif^QADkrx6urfJzHDS`t$ncIhSJtHmv8=0>hR;5$!j3Z zBvZ|lf10sTTGE+Xz7jX_3O4iQwGb$@kWIA!{+59h#_}?v21v zu)|%y!_%+BI}Xp;t|>aDBk;W=n7A`EuiY=N*;IibEUz=Rr!#)5(@d?Cg}4Rpurpo1 zE7Pwl45cfzrgh)lOgyBkkhr^8uv@>T%iOOUrbx!GRrhRE86`!AnHEDW+45J9Eh>~* zaH|TQSaZx!O#r)<(~ z*3&my^Yc_n<6Tna2*>g22rd8z{`bCaH8Dcxlng7BEIh8lO^@YSsq$Zi9`o?Nx1NCy zzphQc&h47+eNWvh518UnOVJfw#(DMI*1^4S*@-w?%vv;ATwSg)DAijfspSEZ?IE)K zj(-Zh-^NTamW8`8*HVjIO_3^kJ&{V%C3feaVqlv?0xGT@$z$&F;{a9W zgFf{^m4aQ>AE3s|p4#a?Ka=(*|EbQ)PUESLJQTwA{HZ~bX`h~{-fd7maT|d^+eB|Q zsCU|Rb9$Dfbe?2}T3}}RvS?M2V42@x)n`T~WyZg0=6H2xR}p8Q*#k~{*2iVmJZtvC zAOG^w2laXO7PT#M!TG^JL|eZ1tX3@BIx=Tm>eT@MZaNfA(s-OsG-xt~cRl!KEhPnE z0groL)@)ALXI^GiRmUea*-8uQm+05YR2&B!c(iB&L;o``h*k>{nzWp+IANZ2j<4XE zKFgKwIB#C(c)Dhpw*RqqI({y-!z`W>!E%=OTb8+@ZlFaMoMDc)V04(-x!+k9%7s*k_l7Faf-^-OytuEU(&U23#DWy^0bNW6Ub9PMS1cIFGIgd7r2e z%kn!(`)94FoU|RXW)y0f9-fZA@RJ>iXUMKx0fb;5G#px`czPDY{`CltiJXFS zd%NWI#7O<{9JLg!$dT3@9m7qHSntucSW;aU= zhj>h5j`j>7w{3E>2Xoaw#6-EyXY3QZo+TAuI&KuQvmFw)9YE^5e1$!?ll3=$N2qB> znsRpga%azB$B%NiKzjCw6nnpK7jI+tdZ+k=bk7%Q@1Za0DPZq!cI%hwy<+|%L~Uuj zi@lzzBEJL-c=`QypW>0f_@8zAm{o;9gDUy=sbPOGK>=8e0}PE0#@H+%W38ekJ}5>y zpeb;p3q0V-I$$DmyQ>1TlO3|N7E&zxAnhK&R26<9Yewxqz_+6Bjh55J=ykpy;s z7#!VX@5wvHDJqk4Vv}!d9!(&&ame#Bm@q^QQ!Zn23&$QKLsw}i)6OW;Df(jR1)h+C zOW9#-Q7BK$0x2vYRH&g;#(LC0FQ{lk86aGXWq$=|Y<(-E#ml##&n!XI`Ab~#!!*$M zG-}NaF6c~d{UolbWWlEN0rG55p>%<<=V3qCUpUTgm=NQsI7?W|V`9%ThglOZ>Z*y( z0i35GSDv0>g|T3^M`-3edFFr3d>~W0!Z(KESccNr<8s?SL@g})(5H>NiCRC)_0`#K zL;t*CF@G$w_x1DBQLRd*rN_iWZU?A8yyu}?wjW|U9n963J@YMK5r4-K7P)n6V$ z7#O;1mwYc)1lZO=jVaA_f^U^)1=p_q*E;>&7jHa6dL*i0w>q%Mce%X4>-q?yhT0!z zk8gC(vEQ1DzMX*@;2MZum1?*sA&vPUK1w42>fan(O^s00DCl|jJA8YbbFnHv|Bm_Y zQz!lf%l{Q35B~P=@7L?U;%udoMh_3>JsM(@YC($vl7jC${lG-~bQ#Z1Lip_5`y{~;U4s(^e}5S! z?m#RuMP*hwZ0!fxA)lmc+zR&pQ5mRK=;`14A$|7b{j%VZCU|ozBpUiuL1<8AHz9uWpISU0=@YkPZrZiJCsZG zc}_vd{Hn-T8*Pz$^@P4U_3Cf#7b|r};{}F(3IzxWx6mCW}Dg=-a?#QG(Vh57h5ESCYuZ0g%MYi!a?MkLn(ElqjA=ZDi@0h zBn$!0OJU-6gL8;z;U-~e__yojzcv6l6<9hHt9rt;ppp_aL0mVCkMRO6!dx3mNepkO z=FS5GrGyX^W?UG1E@i$OQzqoP#kVQybk`}1@er&F%+>?N=33Q5CS7XTp9F$9l97m` z^Nn*fs41}{?t<=%6r^>n6B5EBCpn~OeJO1kW|AK2$mAocYOGG9Jlcj{$88jB1KSo^ zxHP70O2v!ZT1w^jr7B`|+sjTcP*u$r%GLQS@v9im_%UWXM*~U8sqUG9A_(Axm3}1oS@rD<@REFk)fCz}K)(!XeSeB*fueE0 z60*F%J*4FyElnOnma%|{bd}PH1a^3n3CvOFu?Zx%bzLdNXQ?!-XsQvYvAzCLEjD9}+)s5cw3*sLn)`gLf82}#vy zj2iVmFK*F%T;ct~<=j@c=U&^q^TWj7VxNb%eZFngw(9d()7JAVYtIPn@$4xnet+tI z1oc#Aj34+Zr2nMv_dZOL`B}cMnMhB_`+|of2eE^HJa7Ofmjgj@&J%M9WB;Vr6tn(($H5NrvyD{o<2i(pres z1|DD)Sp%so7vhZI%m_yL#)v|l;*@}*unP!cob3w?k@ypiq@Cl~vzt zC%R%`2m&6lV@zjk1}t~ECp8Ngst9O zV%YCCmW<&^P6)>MtIlcNEIHakS!^iAP2x>cVe{>5EqP{;&_xis^Cx+Z`0}f#k0o1j zBOxbJUo^8^7n22|fZ}N7NOW7P{6A91>VCn6pD2{`rqJ&w$a8W=&qbHcgqX8P5Rj_H z_`WXYZ6d0Pz{FG#;kA}DS<5QJ*cR~}6;ZPwSKxr*vEaA_veK8UwKL4{zr;wY6UoWu z4x?2M)1?w1I%JvhWYmb5v9-jm7&YllucH2#8sL}I2qxAnrjUogALyXhW{&_iX3sHe z@jOfEXD-N%?q^~et2vRmvc&?q2y=%kU6b@+&0)tGx`^NkQ$aas?lvS|q?83Oi`p#k z`pPGSt8G(~_T(7^GkF|zwgywVDe@BeC{lPPrJtL;x*1zRZ_yNc_NmeKAtl9}Xof@w zIdepsT8Cs=SaROi@+S9s*WH}$lLEzxbnhLJzSr(_HY>gUC~TNrxSFw?4NePsQ*$1r z-yZTc55*<-G#;a!zwsG30*QFAh5LIS6&9fe?Z27jOG`m&6u6Gor0^Z@yKR zGvAm0db|mp(eHMFSkNEb5&O;* zi1gBZl)1kW3ZLUcHlkL|t~>xe;OZZnONH*O%w6&d0}DXTn+WS~hqyIo4{1whAMUkj zBo|gpPqd3Pi0T%cJA^K20=>Ley_~-kb@AWBGOsCxDwuTr=-l}vccyEv&*#1%aM!}k zOj~2*X4`S$)EVeU>$_nR?g;_|;Kb$*@vBTv`h266op)vw?oke0iekSu=IAE!Ip zYEPfeB{Z2X=#4&!Z1$(7quS0!eg%j67$NKAMSCkGDL0Q4P+pmoGbrNcK*pcs_N7*Q z7Za|VLoNLEOV@7a9B!=!kE^ap_~#iO@$DKZuMIXr-}OoD2a?#1<-48Q0}N`%f-gdv zIeH|U+76Cyw)K6vu1Y&-CyaWitc{$Qe)}ado<#lED_^kv529Y2(>HTHqP(&XXoJr5 z=j zLkM~q{T_IqwfAu<6ZEz+5ct%w_jxHZ2y--c3{N5oJvAPbhg%5so`>_?EFAkjH2_V+ z1^N3OG}Q-Z4h4rH3gbS=rVa%cO~Lrqi_khqw^fZ@Jcx2M$UHTOCOKG%2xE@{M@BS+ zg-VS)2ryF>GYc-l&M&~rFTjL@e@Yg^`XOcs1&y8xr8_T%5-K+OE~>Ugg}aWR~EQ}A)elGTcLZH#QG4YS6ReJ5$9tnD2;EEb}P z{VFNRY%YP}EQ#JdEWcey<_^$+3PIq5)q;}XFc4D|0@BA1zjcWh6G)caO8oGTMGy_+ za+lOTqR zh8QYFF;VQS!0tTOzdY3=G|ho3i*GR8b1BxQsEGA2GFht7BB=l}P>j12Z<-#(11bu9 zD9A|;G1pE(#z~Zfjuc#IVn;5;7!1VmAI7_Alq9%kildb(4HO5sC5_D$=nRxZ+!aZC zfxD9Oo#_g*%R>kZu$YK5quf&o;fia(K_r^tc1np~+X`j`Q>Ly%JLbWa%Q$B2vV=5q z9YPZK+^9o1;~E+xPvK(YrAk;w%3>6AUfjbu26E)6(A>t6|4>ICBo&Wp=dfs0-?$}6 zqess(vtG`?W*ja4HxX zk|G*vNkSl4c(wiQQM`xw;zAWdrQzpG<+}$}+3C^O+Ia?pc|ZbZorb6>&E)&GDuVF1 zC=db~5Sl|@8K&@_BL7f8wLlhi2Vre5U<2nFY2>`ON7ra3#DI%1!m4`)3l09$2rO{h z9|}GBs@wTLwvQHFm*;WqrM?Epl+(!J3eA|J%9D&LeJh(_hKJ^%9a^MW+AftNbzdet zR^x0_Bb8hh6<(h8mn8qSAQT`&DJ((7Gsl&ozP-(PX#k`}Q%kK~rbkm_6jsOW9ffc! zznhD%*J@h2&&vnIvR^3^!fWL+FI5c61Nan)>=louHN}jzLbugekLKKZH3bu9^+)CP zKUcoCDQkWxeos&?>ebvyhff7grz|gwAFX-_&s$q8>pZTo3Tx+}j>tJ^I0=KSAUxVP z(^75$tDZ{8CVdJ%!YjY?wdL$*Pf4^1m9%X8)@sdHOqOR&Vc>PR1<(!{5`6qe!PSP%cSXtjJ-obYJwWUhfLLum`;ey4^_M_oQ^N!Z%)*kI{D)P@*mfa(k+C#LR zLCxLM$NHTFgR`<-Ez_Ucb-S0E<$sP1uO5G@9PA>0?A}FCpYIsnOYPxu?d^K(K6va2 z>ltkX?7e0*z4RHrjqb4{8wu$*KL6eO2fq)6w+~IX55v0;d(w}-xwqT)6V7uVA#xuv zb04XEA30J4zTF7yV5HEzkN&j(1pvmtGeHHhqjcU@z}yX{YCG?`OUS^H`dpK)@6Q1@M8~UqHW*s}GyvL|~F5 z_`OA724Q$sMMP&!MbQVo(j8sc9cUsRvH;BP1dpzI4p4PWSv(JT+m93i_rKvCV2}M| z#L8)zr{KfHaIGT8p8&jwTKou%6-*SNIXOJ@JL0Z4KO;WUCHpSXe*DMoKyV%Ij4Rk= z!r~^*LO$@or2N=;?8scp@_FfquO572fnB|xr|O99;sT|{UdT|HCKN-g6)6Ja9f6iiyEfB+fDh?bVMfj@d;+cPF(Q4(A_G^= z?6UzbnO35mq2DpApl?IJRvv|pMf%qtF|Aq!@|p#UK#>HW!ZuoquAhcP9>*qH&+(ll zTwBMT99USMXNnA1W|Hya@qF&behk94U?f9S0qVgy)uP7`*~mP)(ER$Of0ZcpP{+k% z=Y`ic$^6jx>=*5~vEQ1K_*5}^bx`={VP8~_%U@2^D}!Co!)bYg!*&ncJWXMvt&(Ps zeRdCM$qrdg4m*FIH%nW$vz|27L-q8V!k$_6j)ioIz@s>uc8LUc(%Jad+qF3U8D_m4 z?f)bD^PnH{C#gM@&D$bt^>XG~B%7?)>iP&`QIwgyfLt1g(nNvMe-_uq;LrgT*O?tB zM48h&gx720(7X;<7rC;&MP|4~7B#_GDvv{qj-;9ob8y7qem!v}I|Ys%GtD0c;vMNi z&KS|{ou$tP1Fy|kPk@=$3Ch-I0J|$s)5}1S`)fyE&ud-YV6J+{-tyxhk@K7DgC|F{ zN6UzTlgrN&rwg+rI;aDvsBmLUX9~yAQDRdxyu$RHB&en&$H6bTCts4AGKF!-hJxtUrOk@24sf<%^AmsY%m?LqJ6A$2s!pW6Q^-@#k4`l2fVwptAa0|T` z!TKBg3VU^pdsXa>oz#V=ot@@T@2ocFTwZyO{?ED582U9D8=eED52^rsVu@ySC-Ob> z%5x{%{%k5(iNc&D+nnU=phe`;nb)4=`@=2dZhX~A!O5vr>#_^o3jZ4vJMRNM zFH~pd0ey#s7<%{s>m#!0CDUMt;@rKilg->JX%5~`?!Ba(qAxa8x3(#_Ihgp4F4(5Q zcsuP^7)BU^cW{7tH`pQ+`^ii#_E3vc2d)&6n1ahkWv6!$o5X%s&#_3kU5gP4_hVV7 zmq-!kJ;#`ze@Jd%Tr6GPAweSV0Lw>ZFK4S$%QIO2XBTnL=YX4MCg}sY^2abEOMSK? zjy)#Bj@#s}FAWqqj_f(eK`+?eglSbMeJ?g;WVWb5IWN&D^5-S7 zeBc6Kiz?o#Z@|U)f8jd7R1lh)=eGugceqtjid}Q6NK3%=Tch@SoAx^a`rm9zZ^*H6 zd&PS<{u`Fa+c}SE&*^*rsZ;N__jzUC+sF6eZy$Q66nS3^sb|k(w4Uz;&1JrSOjh{+ zh4qL3@-eF7r%>Sz;r2W-rVG|Mdj`>eF5d{?oJvK|brm)w4J}n-dk&akjIx z^3N-6%dPRxt%28-xwEuDrvWWHqrhkRC%X}dDW?_$x$zE`@1L(q5P^{g<=(-Yh&4^U z{bQELH9|Bj3<4Ij^)1D4I5Iw`{n;%gFdB7TYI>DS0Oc?srYuaEHGJ-qSG{u7`z=YRAn6^9p zWF=qc3&AbKM#)a;7u?T2f{7)1-YT56W`{pdEZd!~r;BxVYZTMHcq*nxehUqIeH4#a ztoARB@;N&2{yEf{_Cu)aCy41nR`wf3axLg27>F$GHaRTX1=AYz>eDd2!85TN%tt-V zR!j8`@4VOB=$I|2moANR`@{i%Ssgz*$7gk%LLWa`&)q6!1rdt#!GAzciJ-~+#@Tfor0)Al zn{5(1sg)sb`kJMri+Z6T%xp!s_JgrbW$E@q}nDB zlO6U~rP#B&$bxP=qhFsXgjSAVC?Z}wEN*6NykcI;W@{HtusCtE0886fPi|oC#+X2O zomS$TLsjOzfTJr6+HH+1&(P?y_$AZoX3r4o0}-@Nr#?&n>ny;wp!7tZsp}9FUJvP- zZ}(smN3@D)^u3VoG3JJ9_Yf&4cy$_#!i) z$BOZI+~Aw0SLlq!|7>^yqAd#|D1TT|#quf1w25kA3wHqV!ceXkyto{ymsm$Kb5>6K z*$2ta2+Iz78q{74yBW)AJ%MO~^>|`JhK6YeA!f9E;O&La*H%yR*rtw>o1NAMGL`Hu zk32pLb$iAoVE0Uf~4M%8F}SGRH>y1-pcY@qvY`R_p6y?ftC2$eZp>9CCM z^<)9|S`VAQTQ{l`MEGfpOjT6rvI*^dUtdne^zO>fw>M)FzpuAo=JYa6)yiaT2S2?+ zwrlTUtr?08(JS93I}F3B1wl<#3ML&N9qOzfi5EFA{m1U&gYcgEs-`LtP6=qrmO}SzinlC{!@ZLRa;)COJNh5$ zEfS3JG`mDP_fnG)QppU?Hzn$Ws3~COl7(ZZq66WQDX?>7vaBJ-_&tNcbT zVtY%8HAA6gIgJq$kMLl*2PYjvQj*GQX|On|uCk&NZwmBYUD9og;(EshVk3qwPM!+*q9VwJSpNXt6?hhC)LDQA;DCtv+e- zNXrEoq{RDJ_F4kDTwJAO1DWV;@ZlTj1CB-IX_>Q1@Bgr6yF zP3M?5hix38Gj!x?b;H$8orjJ6AtnA*C4b*UBI$-%{r&T}^A&qaR6ds5$pZ{iz%gs~ z{_4i;%Ya&YQ_ZZ+aeQZUMGv=2h~OodE$pS(VMjJcd&r#PTTX|#rCT09Cmd&|Mw$89 zP=1brp`ah(oDbPe*st0XS3=)mxJdSNVkNN5SG}3kf``EK+H?Anmqn!VheG_FuPP`6 z%a^#1m2$s~)Yk}C?6lhab^FexMg#_%A$YAdgob|}c5~Zqc6b8)47bH$^VV|4JZg5f z&P_|S*Jo&XTZaDFi*J3~oDt!@?>2C-Qy|{DTY75fs@t(aAly7qX6?!dICU$@qZZzG z`}&{j)4pP3@iOvS$^-dS8^q5MgWgtili-x7x`A%i-<%V2u_7;{w?C*9lfvbTS#WJS zX?5b-M||zPec+96FUpw|fO@Md;>H_HH8~Z2XQLh5g$MfqwW|iS8Rbq8p zZZ`+^Of;Ih%sT(NA!)@MYmDS5`0IhcbH-{DI_gyaP{h@Do~`t(&g1x4NxGY3H&EmXH;N6h~0l1 zIZz+}8u&8G)qhv1__0XJ_ByE!aZj%BJ?3h7omo@4rP1aBa8tZ3r1d|Pe;2+`Z+Kg2 z>3^)h;W{4R_QGx8eSARV1$_Q7C$s*3@;&I!NCRZtYH#gC>idmgavZ^6(DR>x zAky)MfV1&Eh8EF&ghF`E3G&YsqiZlZjCcy9>$le(8G?p5ku?;8(D2=kC=W@Gm?bHo9fOH!#As;1)Tz6$2ganPRA{B9ELv2blfTgl!L$NJXD*`j7e&_*#T=#} zhD?3ji=PPVp%Che;xENuG|}OINM>Jh3*ZY10h?3^yHK>I2dJXPEd5}fRTFCKV6??z z1Tt#D*?ICGCX5%mfWZ)C+d*8@5f&XP!qpd(?@ zXVPvgKxlvJl^K@*3SbO2g3ua6EPkSwBVEz%d}hIThncd{nfZpnV9=TC?P}~=81a@{ zh|qn?LNNsg7g*C-*@j!S!@<9VQ-oWZpZqcbt@zQFTX51@>V@0n(wR<_{{)u*H<{7T z5FLI39XX*K#G$ByLo8DVEeq&umFbM?E$zDLq=A+$_tH-N;cl*U@a!X2FtYAXbPn=R z%tz%;A{8#1hPJz;cB~ax&myk}v<^g*U!9%J-=p`sCx z&>`3(dfsKLP`XeRQCLY%6M>?;I zK|<>@OPF)7)L>@BP$yPKSdCMUAHf_Hl}PMisho_d=vK^w^zi#9QnFQkrZ!1|6^Jx2 zX`mDT0!DB7s)S;zxMCQW*uiY+(>Mr`E19K5?y1kcrEjr#j_-M-X;>C7qn`JdfDl$vU(`DxR)5^TH$Yq9S!Llg{S%HB(8btqaU4!N-L&dJUv8@S>TH zvH7ApkM2)-b#>*a^-kY~g(+OQUO18>BPIJFY<>tqc9nE#RXf=*I$VX5XGPa=c{9vc zE3NZZkfn9IU7pryImoUlwMNR`E~A~Hr@E$p%+BwG;dj4H{{7cBJ))Y_ivQffPVyK2 z7xWdL4E7?nIh;0GCzcrLXOZGeEt}Tkrm&3J7YSH@{!={%*IJeI%U<~l>(tuh6YJ?r zX2kf~8BWph+{y$xyEyra;YFPh(Amf;gV2J_{C!c|iJmcUoe>CUk&Xq2wQgCfZV|n1 z#qw&|v~Jat1)^5B7J2m;Qn#MTa+^}OQC@daRJYk)cT`rs&|SC9d0YTPKnDZ7+CE_c zGeW^^pTm$q?OxRFACS7t>r1XU?4#EoROc9xiSF&5OHqh!E7fNSU5`4LE3?+05EXz6 zj!pvW_dFa=LRrsuf1Tx8oR`;oU;mPt?U6?9tL`x~=}nv%$@z;W@G) z%cbEZ)5#cG`Zl-W4WPB-2;8Y>`yj&KbA-DcYxwlcbfUQaylzlrX^8Ons1(6aqKvF=F z5@9_4{)^{xzUO*zpZnbB`{H_cYietN)z$ltr=A`} z&!b2Wg3#C3H#9UfGBPqYHa0OaF*P-{u&}U}2V2WqTg!)8TV-4S6AHJ^`WLi~jg76X zt(~2nqdeGAUe!@v+tI?^qpr98oUWCbm!{n{Q z_8v*G`O%|^kqQRr+Gw#Y-T$iuTJs;CH3RD{45O(2RU zCW|Jp;Jm^gUW`Z&~OwMj)B85XgCIo zfaoD05JaKpf2AfS5f}sl^KW9iy1Kf%yL)Dg+`;WXbc>UL8CF)$wJS`n%YSOWD&j7HNz{T4GVzSQH$KLSz5knHj_` zTVj_Tu*>dP3>=F=U@@Io3>u4>#A2{_Fzq{}@!hiJ-Lk{ovisfL-G5v6e})u?Cqv4r z)mlCjOG+nfJKkC`k_hC{Db{*lIhM*O<-Iokz6zbe`80)98&N%xBWT)SJAtT~$`^N; zEY@zTop}ceK3JP*tNU1{5=+je(+>MorIjOWH`!i4UuVR}4Fr}qd}*|7^Zqv3(fGB+ zaU_LJx3g)v&2zrNZmJW$+Vy;EvP8G5`5OV5sB}9gr)mN$hLD0?ue)`U90-th6vRNV z=u95nQr1EONIWUG(fV{x+wMnsE>0ss%%i<|m}#TEKFtVTJig^rseWJQ?h;wx>iSGy z*GW|`^}&TBX<^`2pQ|+QV_)qjH0Z1dox-Qrs(a|VyY<5Y7kI{EaJkm#|{v zbNzbX-^-r?DFT9}udcTyYAjm@b*F#r5G-pNg`o7mmPWsdiJ26WlTKuloKPw#CceHO zzVUGG97-NHN#x1)gmx0>oWNqh$Q{2tR=1fVN-n*javlXMhb(G4=`khnhOsAo`qa%1 zCE?ED6lT8n8<5CWZcHdF{gRtAgE1%x6D6dYZ=0s3QZIt#;pVGzBvS>PIUS(!>q zriBSS3_%i0;vH)N1APtZ?K7VefNByyeM7iMIB?T_&rqGUDh}-WA#eWzy4k~D`8rQ% zzq-Ao{z=-3k`S;YkeLAUg#5x{uLN2N+Kx-{W)`eu*d#lE;&8ttGHcUQgyv!ojT;V| zyN2?166d0XGwDa9v#KbAnAL0-XJz5v< z#XW46+vnlB|gKR z_sD0ewTfPPiyQjB2OdqoP>N2|ztEhC{n=>leBaXv=B%{SNztz2cP(OQQK>26kG{WW zn7^j|o>e?-2>z%-0^j_o%HY=?pvD0|``pMkb2e`ju=8TU%tC2t!Q2CWZekg5bpG{8 z2-U@s>l8`wvS(dM&XRZ6k*v1whliJILS_1w-@?P_!*FIinU2F7F(hJFm^cRiD{PWL z^VMdW-0{^`rl#1R?Y9>Gf4&!ZH2>Kt7Qw}A@uoS3g%`h>`?C+LYrZ~c>N>tYY(*uz1LCsiAw^6#zapRg$=QjwpI=6#~iXSVAn4|3^9gJzH)_f=@gU zm``y3-Z9{XqnWVf-Z06^&jir4(=InD8Sf1h87_02^H{?I2xM+$nr zN1hho5xoh2Dzd3UDFOo$rHiKv;G?&pC_zM5EkV>$KFqp;nU%UeExp1`*PX=_f}HGiGL*)vRZ4sGAPGiv^Zm8b^*!nd zFXzhF^eULY$`d_gt;~fBLY1uQRV~;P8IPn@#qLlVQAKCff=6u1G`ohnAJ%%*Y*{sT2&qGtL0h+Za)gUy} zVBXcxL)y&w++kn~c-k&&jU<_^5}NKJUWP~a@xNFpO-Y>tBF ziHYvYlhqk$mjBL4gQ@cDk$nseI)9(}rs)^yWfITL5ord{`Qk>qU}S)niBoAvvE}8n z4DPK~_)XKJB*&%a&;@{XE3U!SL)T_PBaEE@e%Y0?Y!`~wT|cGuuSw8d54*Gg|L(mU zSXK|f`U`x!HgE^<#k53U`q*rL`a1Yy=3NYlZ`K;k6_P+!9Lk_5Mu$yjrG-*zvlduXQMctp@3@X=Gn?mOb}flQi~6c4>E7KG*V1ES~2 zbv9e1Amv z!_tdVJgXp!A>C!0vPylN~By(U(iebIEcAzv| zM1z{6fS0^5ASF+>v?`>div(x?oX}v+nLkYBr~&x6mnD*G>dB`2 zIoy^cbSXWLUbFTLG2ShgAl zyD<&Ir{(!KdlTovmR|7hnhIuwiPbM;bFojlvR5)JD245q($B>WN@iK!Unw|9#6Lh> zv^G9=^TF>j7?aHVp_LK&O325G&M|NQZu^Jh=<)E9>T3I_eq_h9$k4ByQk`pgeVaa! zt82%dUCy~}HQ>IhNpw`tOqAHVOd9=EXBs6SYiJI^uY!Ba@y&3A=6_nFs&XLb*6_igT6 zzi^NzhT%R&al}TB3cZfHL~ppPByOB-8-7T!cpW_sySv>;xBbgOcz1mwH?hNVdvvMn z-zqSM!`rie=B|9z7QrhEe5)1`q7i6-icxpfb9MK|2SJv`-05APFw#fBEqsLQC<;`x zzVE!IaqnzZ(-%O;b3K*m)tJX$vw8xbh*LPtsr3N8?xZwAIaTbX^&@~96KdY( zauncjBe^djmH=(LGxcIjNS?VjQ3ce*(-uJt3gva}TzoLIQ38^{$Ks(J< z#j7TUFjwnQhtI^K4|vz~$A$lBUU0s&2eCwa_>x1W)azh=RRu=qa-2U+n7%`;#n`qo z?zgPboSHs`deYAa?vR2cf~>^1(;^)v=)R&fua4!D1wdG6H65>WN+9z~+Lq>*^OV zbmo?V5o9#(+;Jo`qvodhDZ!yyGe#Bw*EcWufaW9?Rt#|=>SlHV-VR}jlilee8e#dl zR*x88Su-;Iu*?Wt2=5)UA%=xPoicDs8U?$y*8&+4XqUV2Oe*@9x}A{Uj4q^qGAT!+ ztt>MyiLxG$DeuT-LXVho zq~-agta3n19CFKatCv}idz3aVBPB><6YwLIBi{Z6$g65+QCcz9CB8MXQluq&b3m7C zzO2{^%Tf!?wPjMg>dI7`czd>#>$ zf)1~%VJf|mM?vGW)8@eaC=jmQT9&bG=*i%4PSK%xxZ`rOZ z_`x9j`iNhkw*ae3f$Pds7tk6}9-t=+`s=OuL4~PyDSuv-cv04XHWS-^6~{`Xu29~? zq8y!uZ0??F18WI|Kh-AIEXLp(^A&0Rmo?VjIaa+j_6<39U`C}vJIrN`plOY~)7$SM z)$T=7PRxv;BD(O^fXdYjQ>B8$>BAo`z*FXta8gv8g?`pBdT2@ zzSrY+ELbkhssJXiYMl?>1QX2Rkx7j#RgKVzMlosI+uDGW?ZDFQMkb;#OGl#j80|fB zP~{imy_I?odK=7{Hrx%+{nPBXH$lmn+_5#_=Vahlo@W2a42VR1gG@Nbv(V@r0}czK z^qI_ zdU^>Rf`xl?PJ13z)NYT}!uq%y`g(ih><2G<`)T_+-}UiB`g&LUxQ6-$S^8P=`-g4% z8J_f`-u2T$`X^WWsUR|@P7I$F1{Q1v7IBdSOYa6&`Ucik2i7SV!Ydf`y@g(38D=8~ z`5=rb5`)!P#=XCTyiQC%Qh|l|A1)MVD~A$m0zcgRrD+~YlwJ8iKuP0~Esd`z8ZQY< z`AliMJe0tXVI4hT9P?+?TqQ?X8tbGnstqyh^GaNmR+tI_;6aEBW zd+7cOKT6X4OLy-{TfEtYoLwW1p&IFKt3u*p95wPS&YD zJjC)jZVD5b@)f5Ekel#oo<6jq6?7sH%=MkXWA><0B&Js6BPEzoC79A`Vh@F)*AV>G zdQ)3YlpZnUhxj9duPCT*Cd2!AYSShcPti*^qRf(989}Ic1o8z`@N56WkCg&ytRJJZ zxhI;YpD=p>V=4JXXI?qZEl_eUDheNo@?JRcxFAQ#RmmyYW_^bz=bUF_xdp!9lXecz zUagJ894TvBIXa!^c^P+<9Sw9E)3SVQ% zW~N7PD(BkM9x|42C@2Y%yk5_o zB5Yu#Y9_O0VC8moMeWZ@Yt#qC!;E&j)t`~8qTuY_lCl1y)t=kcU6wVigVj+bB+74X zf^D_o+ZytAZ4T!*GeUh7MC>jzZp zKW^z=bgJW+-ZI&4I16vgkX1MPZOEl;^!8M>&1_H~ZuH2P;?ItW>tcfb%B zkXDPO_D6n#rIhZ35YiK9o5k&0CKly|=v;Z_Sj82ADhRo@qzph+@{&(r1#EyvsD|>av)?#MlcCZ(wTj@K%`1?qLUKS{c=)M1^xRi0G zWIH4oF1vf%+dj|t7z0|zf}@KaT8kpjlZmoQP|1aB^4QebT~FL@j9Ay-87v9@Zn}MJ z*IuIRrO^&qSq5*}Xj1U`ei4TwnYxVDf!4n5MB3}EGC|FG|HnX7JLUXY(qg-s8qDHj z`x~bR>2cEOMV9HANfxGPQxp35X57KCXV}kz0>AWq!u22KOqJK3TYKM@?>vsa+JmO~ zG6jz@a;2DcTo(BdUuXo%Z>dZ+_SL4NS&@0)CAG^3~fn#U5`@ z+0!<}!aOYkSUO;jPom=H-36MDPN=rjH#WQW_XR+kQ5^v{p`do_4x7|^*6;O>0oE1F z=ZBggr3L6#`r43PAviqAmEJp-(`(B0omN>K+G|@sDcx{Sv!Vc~^ZhKo)4SgK!qM;o z>d~6|tkKsY+TgK|`N!pZ_ujddQuHSH9+B-Q#AyX+1-DuGzD@SUe|AM$VdixPpXIk< zm+3&^H_tH}NWE=(X@iLLX}xxbbH>BRdf|0n;2T9(Ij>D1gU1pXz)-P(9^COw>0V4| zb4mJaURKf$+yErAP)|nT>3;~v#CdN3Y!dkl5Z2)_s21YCEnK5&*x9A8jIbK%{EE?l zWt}86Jv<4-EgN4wp24SC8!j`(r$wIz8#gZGejw>On9_dz=Qp2jrf?>hH{ttULX_bw)kUa~Z}9rdaG~#6MXZ zVD)jk8A>)$g+NFc)>?PP)f{a|@8S!lS`#=YxWV@-r@4nl6Xi*9=2u(=CH;2EvuaNF z4@_$%EFz}~JieG(`dN|?O}-V>*U)tsJdLus)LI->t~K~${wHW>rbIcB-R}BqH?XZ4 zFImZFeUPOuaorir{mxAzig>cKTN^X{&OVD zpuB3Bj4kVz5HVZ(8!%hg_Ipi^*8oYa=BAN@EFMMhCJlVJ?NA}UxL@RvP+c`bN2Em* zCQ_edb--I{@=J@t70K&Ww{8C}Gplh_Eg^R;PO?G2(s3KN&$#+Rm0zAGJZQci77hk{ zEbV7C)p|;;E^VyN@GVO#GdzOZpT<^4UkhjgL}7AB1giKgMZhXZs*iTD3Hq=xt(s84CPwO%?wTc9@iX0 z4SBa*i`h=QFef*8_d<`_9`|!8f2o!~CF(PesA6WBx&(U)M%LpKq1Axix{uF-jr8 zgEy%!;KN;Lk#A|0!HWOD`HsRPphniv{1{9AY2aHvlK!A6w+F>`!&$O7&->{NNdacz zr*T0B!Fch%pbuJ}oPV(^(|z%oExJE+>FEchi#7R!7^>3fOIMxwM*@T4Gd+2czbh0j zUj@V~3c6BKDPFmRi&lw`gdD;RNnh^QXDo$|-`^ZuJe3D;gr9HC-Z->HxvvM$)d{$g zt<7VSsi|B!+>=-ItyD=C9F?ho!(=Ng>W4U2P6gK=&X6jpgfnszmA~=+FK%;K4>ht9 za1lu0pBy`YOeSF|=w}E8pd61HM7Y7l6LR%50cQY%tnX95T;9UqyJ3cIvRWZqt7QkO zAYy7_YCs+5Q;OKG$NbmusG~!xn|pG~R9g5VRSNmlaLw{NGK|uY#ih@2mvhOfI5WOF}$vJo5XjE=(U`$dx}? zshL&9%rfN~pN0bIMc;Cfl1FHrN*H9`xePN`=MVDdefBX_WS*@~>h4Lxq%i4~1MwN- zB~*sUD)62Nl5-P1HZq7MXWiZl$9?|rraLI(&3yGek_k;Mayq6FPbgPXJPA=`AusfjT`WeKFV#9ELW? z?v!#gGILWrk^Nvhm|k8LUd4VXkc7eF0js9@3BEB7(1c24uy}a^U6?iWS00h{;ki=@ zT+QG9fh1H?vgyk>&P&{Ud_BFrgEMkcd+Zz(Vn74~816k`GROq#W4w%Ydlwsfg8k!7 z>wnXJRwz|#t-RskG~|%SsWw}iL7LSP1Q9}dbh!7;RsH405!_49C0q03&NqMb)>)9~ z%%;@acuFNQsle8s6+~TeO6DagxT?x#aC6q)*PJq66q|uh(ya*p));g9Me7sk=jX3` zZM}`}=l4OKr5_L>vwE=DeYov+n%-x-BirRZ!B!kxqhNRNlCl@w=pWkAXBThxHENA0 zAbhM(^d!iqe^))=<$IQI>(LeiC-EoYQ!EbGixz#qI|8E5BW-Vl{Qxt=Uo*7kT5ca$ zy4|WXcN2*660j@{@jm@Ihu81OX?HlpK)kGN8X-l_LO#MVTbQts+;)$GY*YYc85eK9 z$yu>9Mq?T{w*sxjF`XocNV_kjhOD(R5>h=31e2!^5Zsq0W8!V3p)Uk>xbO=!p_LU< zX|i1iL;;DS8nbhm$xs3seJz?4@1O#T3)iwjB2CQ)WTih`E5J`+_X7(gvx4bJRC?5i zLQQ^`RBuvjE~u%#*3mZEDb8M4h!F5uH#JOq`anXC+1~ZI(8_XwHU}m&#czaf;%rj{X}xc3wQ2rv4o*o$y_RXgW$JLjA_PT6+aminA-f z{85+tR#5I~;CW$nb{(dCLa+&^-5hkO#K#s?(f5t}IgWiUkI6F4G2YrcxX2Ouv$kVOBCSAMn0uyKXf zj6Ti%@M=eH#ykOi;fRb$6*_Pr{s_XmMxL?^XoSzWlMj)WJI5~ zY}~FG-2MGtc69Y|1LwT6xP9{Bj?Kkhr+D`Rp4$7lob1mEO89C>5}N^`h=C!M8w3z#Kpw_kr0=V z6ql5gkd%>@`RA_8-G5~7%F4^h|DzzUps1kukCLL2vXZijvIq;0fZCcv zgYBRxE-*fR7@sbT4+hgg{<{V+k(rVn3rQb%W*E{I8W3F5$(l~_R>Xr!O&4ibb>A#1w*6I=zb)+ACK-Qq6ug; z0gomS>q5bG{}6~+gfSKY!~UBxBM+Mk!{+AwH|akkdDxLr>7~CKH zpg-84KjlF`#-JbP*}r1fzv9xr;)zet#b;;ZQ7}9TjqgX|`_Xv9zry1Q#G!P^&>(S$ zKpg2uj^@HfD^a6Z#3&X!I+8a!f*Kw9*Nl!5u%m>3NhGE`Am&>UQ7|ILfQYdp)`k6> zkBBoT;_Qh1NaA1yad3z@+)NzFBd*vHS6qlIo zHHS7UH!L)4u3o6Id+)O`+FY|#5AA==Yt&M^@*Xx=Qv#K~l*9}alA)sMU(;XK|ZoInW_{I{g-*xj)lIqq?0B7v+kPb-nnq(lB8Gmwc| zW}>6{%Tj~%J4I#gJ-VP-jttNzU+wEbT~wQs{y$HeZ?T0vDf_D${`J#zIeV>1+8J^U zS7@s?Bc}Sp*Cm`3d_}zn{ubBzEciZYZ7JZ*VP>X-)J0eSDO1=BJ?8-R7bzdJ5{jQv zvH{Y~?RwG<^i_GvF#@&~>jH_GV8J=QjD6d#ZXcV{U1jf;Yr!?ctKy=_ zz8)78EjXB^=wn}z(@Q4i{x~MXd+&vWjn8MjJL3Dix$RxIYNe?Xe|woeEF>>0V} zh+ixad;keUUVeZsPr-Tev1j~oKp$87I2W}ZDNolm#YZ~VfqMR|39~Qie?kj?W!bEz zRI#rJdiQ!&qAZ@g{7a)Z-P12sCNZ4`jeXA5-#S=5R=0}VqJTB+{EOmdRkLXl$t5@) zj#`bCx1IH2Q?vF698o9^c=Jxv;C~NewI$uY*dt_F|>msmAeai1>;^~r8p%3t{ zWD9Bb4$a|uz}e;^NVBKx@I7I7;g8&tR_uB{+Fp3`Q9k+M9)06pb)5)Nu_fZ9Ye;8o zD?Q(oCt$=&RjOt8V+SJ_>-Rl9Cq(Tk6|=N?K(zbZ}%%6rx_T&1+3bi6#2vs?a? zhW-5O>5}6!T^_^DnSNOiL@zx1-38|lc=*azC`nr-%}z?Vm}_K(S-+Q#O$Wg?UP7&u z!_E-&w5Muo?NW)m06OTZ;|MLID|#cs;i8x6`3O$bd(6ye$C#mKPY?V zp5$p<&gC65sL*kgEbd+IsjY#iY}!o;A1~+oGdy(f>?jq^uw%=lLB?`hJ0-UiF2FuA ztPB2`o|aT`8)SgcTAbDhEvOJy8X0*Q`7^V`-N=VIMqe`i=NpW1rMT_L=zYj+yqHg= zq?f@MWHsn*eL>3W$0K9TXFs!X_(xLT_paYG)1svVkqM3lGC#UCCn&B#b4x$MkbGyG zx;+PBRa-RyjXcgLFo;;itE92fQc`W`>w)t$Sv;zb3n!IPa)niCB5x=PzmwF4xG^)a zk7Bf`_QLm`mOX$wr}cB2LFtrwZ^2cWYuU`a_q7V$O#FN-%QhQz6zeU_2pUgIw;8JR z^$zrdghfH`b3AFGfh>_s?nNYReOily+3dl2!BO`(pO$v>xD$CvfkH#Ge2bZ4YG|oe z)+j(0lmK9WUe{9)H$WRMR96pFNEsp}*EN`*7xgm^TE&7xUQa!xY^ov2?_K98oeUQz zE1ta#)Yyu&d~|YL_xX*d-4HlN1Y=phfYbp)dTp?KA$d_ABL3m46Bxyoa-a}S>N*%; ztWjD{go*1Y(XGTjw#m=Q(lY1%vW%G}Q%+H?)|6X&Ga9J~;HV?%c-0xuJkcgHRXJ6A z){U-ho-G#G$L(rvr!je^lu$NJSEg`XQSQK~ND$d;dIT52IpwE442cdASWEt{(rfhbIaayTLt)6tb@dT<>L1juyCkCX)%-|g8-1CA zXaMusitvZUM&81oH{W?%RRo>nS=`&tQgw8d3=U;?_=Y0+UaD>@c5>zjdFs=;A3b7S z%H?{fT=Ue@af4Hkb7;LzB9&^xE&5$pht|7qPV}-{1B-te)tj+^1A#chae*qMmY}S| zF^}0u&Za`tYv1;j9eJez`n0qk`L+{2_Jf?D>E)x~tY@YNvY<-vW?<=72V+A#dk1x{ zuZfeIZ$4Y(j45+aak9N%%+B(R8SO#TalR|%7x;DNfgj~}ySC5)mTTr7KQfGbC%DYk z)>?mjt`r$N^wRHSXZCOI`oueG%O6|!8Chc+)$PmLb==jFA|1HF<&K!V$#%YmCfNB>BPV(!^MvlE%9iQ%39!)OdjP$9I9=>qt*mY0Tqi*0d;4rQ-*BhDVr!8e+9=@*^jhcg!1nKb_n@yZ)6Ug=*-@kGa~iK5JJ&x={x-~*J)|rXB;TL> z)6>dD7AL97Lt`4A{RaC3X{x5fRDU|C>I_PH(X}JO|98Z}`6%l{*B(vr-$5LXMg!Qr zuV;ETn@{?MG3`FEojP0WB>iqkRsS3Xcsaq{aGZ!hd<~mA-#jP%!Ci|!^uMpVEYQ-x!_QXJ&d+*!7yG6k&YDf5 zJ3{#Heo6gsv9J~WC!ZhO$xki^7fb?iyre7r^a9XEwE|+$5P;LPF+>3+f8Q60W+VB{LefVu4oW?GOdl>OTCWZ^hqt^E9cbMGwFb=E;WpxAmfd(CrBi5>c zic^%bMu36w6z-75Py~%5Lo%QVh!L3>Esc0SWc;WZHTh{wlvx}pj7o4hMgtgUp--nV zP3^i&eTj~D6HS1(B~Z6fxt_ix-PTXg5RG?(B^b8Fo&~>{I*PrAL~6pQ%rP-nL)7$b z)ZS3Iw#-XS3^nyKb-7H8g8)OcXuOkh0^Kk&SUJXtBl11wmF;vqDVyp#5^kOIx@H(& z4FKu_6NQwMfPG zMZ%9xkz*JslYRDXnwYtOq(%KiCS^)IG+omS=xGKNL#FN!XrG+E2v<&%YGa5Y#L$Sv z;)to`H0el=j71D7)e4?asz*sy^-6;yF;O&udpKjXEsa6!RU`2kqjHLhH$zSv{QffB zn>ijrpxHoXYQYn)^x#yd@&2$lhlWHkm2|XHqU_&PG!PE_%u7A|Mp7(&Fef8%_?ZUh zn+bgZtXKN0;kUF^adN}dBMorvG={Qaq^2EJLICm(ELn9w{wfUNnDgq2BfTFJga45M z6HTmvXY4BHDA*(7!x0ImDZUYJ{HxNQwBKS}VaJ9ca5IP@W%$ZWE^XC|LfRa#L1ML8 ztOhWxWd!b^pJ8PdSDui^gL&ghOPRuv5r&~d5Mu2Arf4ZM-9^Ug{(V;NEmk?gejHY~ z91%4~vKL!uXJ7eSNZ@?FW{~%QmYWz+v`_o&W4qDrU#{Woq92^aje z(q@s;Y3{hn2OgDnH$YAkzq|w~Q2|`&M<8ZNXymWYw;V>xxgwd{S}a(5QEs=uqfg!02FVBaO&GC85Y56&Syfh-R>8rBXSR?KSksx>Cl_o-moL{FhiEz77A6C8-*N-R)u_gL;A=mGUEwmmc@^A;^naA9FR`VzItN41ly&x%(7E>}P zt4HevUDI>bN?;=T{c1S{;>j6x)x}#Ul1Mod{>2HgO-j@!2rEUGqlOKbCJZ268zCh?C9hq3mNfY z{?3J2hIHR(F(WKO35#YtgDo;+R-A92(pv+7AwAPI9p63@qw|J|ra`k}7d(-&d-}rlxtSSODRB3&Djsxx&Cgdy>-&G=I zO9uENZ`y$X!7>z%GM+1t!O*YOf&}sgaPb}7;4L36l5J`4CJU|PdBctP~87PAv-;k?%4G3tF|-4U=>wse%EMQQ-z7) zsDt0A6zYaqkj^mm5F~r!wjOGaA^AVt=^&rp0 z&^q(lm$7?11JjZdtBw})>tmxlIAI1WYK4I{$4Sk#iIMUCF~7;Ljz+_hBegS=Kfn6y zGfctNrcMhCzcY*x)JD*bBP-8+46>&vo!HLDyRnX}eQHA?U#YGqjZyK=I0jBH`W1^9 z%fw#T&5qq1HypoyIb*Fkvvo1%xHi7qIWu`cni1%p{b)6HVLWSnG~lW>*syE1a(7y? zaLy7md$2xrR8UR%XpFgV1ne|#+B>IHJFdqto+&UcQWq@lKQE9xUs*6utInGBb=;oK zp4+L*;&S2AaY8U^0_wDQ@nsgsIN;g6NS(aM8nxiZyX2y|(CD||b7v`Bede+HB3m7+ zc`{o$t3O%cQk2n>zu!&k8GG%+rI#jih`PrwHlVSL==jT_N>|p|#pPEU%Q+{@o{IAi zDpxWYE3*9YN*ya{q27v04dtO&6BdWpyupf2!GaQyQkywkskvJI=(~-ZrM`^bU(6*O z99pv+zNxN?LRJlROp~H;fe<7@VN z`#+W#B&VlZGO_v^`k3hHlDX?4_RT+AvWtJHCza}nf6jC1E~)p#%?u$6NV{ zb6m!2GkLu`8fyhL8y5FE3{!%9P_%qPyJkJaWSy=h@Qz^5k~-sDO-t)1lRaPCoqJJJ zb#Ft!gl)fjp^sDcLhh}GJzOM3y!+&tLeTA*e4x;f6SbB;!J6RA_G%6Wz(G+_r-#(JYm2evlMWc>y zw|6F74<1e~@VPBT?-){hILc79Y5-`aJP6UAmbk4n#!lb2eZLt!-{pHwJA6MrwEP}5 zrZ7oBaUWfgJjtZ@F@+@8G&5RxGa9>T9Np9rFVn>DPBRS3=h`(N3HIZ^PQ<^Rtr^i) zWYInXd_Nb94iKch>BXPl&=~X8J23N;AKyHKl3qJwtF>>Zx_s$+)@!C7*r_&#o2d)6<84Z+raJr91Py zb7pFI=HY)vUdvW=xL}{Y^2WIbm2&2aJ|nJg(n&2VnGzky&f|(!MIN3*z0OGm=Yt>Y zJFgKNrI&FoKNCrp7k%|l@mF(}WNQW~YnIMmTkm}BtowS?BRFR3kN&+=;{E9lyz491 zNO<+bA>Kf1PJFwz_s#kKl|$d^WW807|KwOjn~XR z!&tB8=v^+2G9@g{nx~E^`;D8@oKHwpJa#{zeQ}KZx(t?8re#_$z@A+8Kix;Of`tbQ z?2kPs3)NqcsCgm5=EE9yiggR~ep1ZTIem!dbvW@`jpt5dfM6q76^JV18;l>B0YtEw2 z&wd|n4HdgwMUyUnk7ZBYC!@LDph0m>TSk+D%&I|?hCf#30j-F3nkJnzUPg;iX{Vw8 zZ^5yQHjCcvM(rCB?|@Kl)bveB%8g;p!-~)B}p=OPG{4ah<>j>n=fb;}Y zwPg(iBY3a|BE^!X`l87`<^~e=_}M{z)DG52S}619SRwZI`*FzuZ8;P9e&ab~rLA#u z6O~U-Nf{>cM7*3S$LF2*rZ*06TAFGSf4w);Qlk7dCXoOg6nDkC&(COjs5My_fsE&k zB;w2}uL;BoP#HZ`rwWfU<5c=zB-qwY76ex>+C_ zFB8vPqegbiBB-f!+t9i=PVv4?dl!~52$h}=^jp)gad630K-sg`4V=tbr){`#>2@v* zz7FnXwH<+ds%-7{MvrG8eKY-^+KV?)#Uw+{h4hv=ep@T8yJu-?xw_Q{*ttr4scdum z`7JJE+;PTk(9*36Se7PdT)O>;TPZY$hh ztlJwLPbOD5!e&g`?A_}2=XYOu#DfopVWG350!+CJmcAom!n-b6{+!5~_j|1bS3%N} z_zRb}9saGsFY`?;X{d4zTfc|q9|(UM4EB9_IH($s`(hB#FQ>o-hXnN#j7xTR=-Q{s%g~Lm z`|@F%FKF(WY`qleIR$f~GY4j}Uj*uXES?Gozkb)NCsM#T;odWvO8%L^FWK3S&yHAk zoblg3@ZZ0rOV2)j{$(Md823}_>4&q^@86$9U!ziJkLEt2DQQ|p3+J;i*vlU9Cy}lz z3wo%J29U)P;JX{<`{vK-Q!$p@CDkx)tIwDJvX3Ru5c$!7ve7J zwK$+r*x=G7zJ}dy(-N?wkH4hLr;lK*mFqKdv7ycH(rzCcm6z2bjoG#rkp_+26yx5! zCil#^IH_@Eo6FTZ5{l2LYWK1^Rvr44@du^S`on%MAN*=B4%0f(?)p7{kpZo&qc^FU z^{wFh1+ytRrD^1A*}~s=^tW%hrqP3si!Uif4@jJ5F%KV?Q0a(nGMx$RHqMoP3wC}W z(L7C^d|b-jT%;moDDqldKc9oDYOkZ+{9cH>CP+u@SRW#qc}uiXdSc!1a(T*kLI5R?_ScyIIQeMM$dF~@Bl%g-9zv{?vB%j!wPlcCb< zArIvh()d_J8!00jtRiC$cqZujs)JbdinU7}CUzF;xmaCXb_A9MY)DkK-OR7$C~X+0 z;9|EWwcEO6UBM2h@7{0m z$5e+;#BP>z&q12P17+KGLq6uVb)ps*8S0~+KML@YAC9BSu}gT$#oPj*v$QH@_Wpr? zHnZ5v*+O;`-u#{O28P)0GyTDws=@KIGW2dlwg%z1z?4md1PSUIl+?fE>jHb_xxzB=Opg}c??wHnk&PMqo6ve%j;+tTgf8dSzU{Et@@ z?sd%SV1MO6iO^(-5}oCX+eh{?tR&><$F4G1YusAsa#{Bb|NSMpj^Bb1 zH7C;@w1yo@>a1m*J04_>!MT~+Y!!lcK8u;J-p`{BDN>#gGX3+Zyi~V2Ll=Ai%F4UX zW)f1HT48Q;KD;T1!%g0X%cO8L)UhlVzF())Yc zt{IK@m}-R;wj&{(LGwN9Ej3HNPtZ*3_rc9=*IEyPNHuh8zs*}OSCcz~&qX)I3&WZ= zO^TmMOK+XM3rjsym%rf~(RmfU*h73NGnaaksw`^G~t=Fbx0mG-GN#k(?zqgs2- z#PE#81L2aC)9s1zw#u8NA`=Dw)PlHLPyYP=;n^Qea?+AgRnV1&S2h!p^0ni%>8rOZ z)vP7Lt|eLa-Zf<*=zn~^A9z^Pa@}TWzUEwpOIpdxO^qVu2Ae~;7`PUPdD2N`$WJgn9@OiA~fYo31;@o7h&U}W0ZdAeoX z0=R7#Wi#W#M=jr~@i)JQlCk%MB`BE>IZy{>T3-Za d``$tUbnsqo_demLeIl`aqHp@ZbU=XW{{WpgqhkO7 literal 0 HcmV?d00001 diff --git a/images/monitor.gif b/images/monitor.gif new file mode 100644 index 0000000000000000000000000000000000000000..3902c59216c2aa32861dd9121587b6b9c4406fbe GIT binary patch literal 13392 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
" + end else text = text .. "Prepared Song: " .. prepared_index text = @@ -1822,7 +1846,8 @@ function script_properties() obs.obs_property_list_add_string(prop_dir_list, name, name) end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Prepare Selected Song/Text", prepare_song_clicked) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Add Song/Text to Prepared List", prepare_song_clicked) + obs.obs_properties_add_button(gp, "prop_preview_button", "Preview Songs/Text", preview_clicked) obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) @@ -1837,11 +1862,11 @@ function script_properties() obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_list_add_string(prepare_prop, "", "") + obs.obs_property_list_add_string(prepare_prop, "List of Prepared Songs","") for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "List of Prepared Songs") obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) local count = obs.obs_property_list_item_count(prepare_prop) if count > 1 then @@ -2383,7 +2408,7 @@ function save_edits_clicked(props, p) prepared_songs = {} local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prop_prep_list) - obs.obs_property_list_add_string(prop_prep_list, "", "") + obs.obs_property_list_add_string(prop_prep_list, "List of Prepared Songs", "") local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) if count2 > 0 then @@ -2399,7 +2424,7 @@ function save_edits_clicked(props, p) end obs.obs_data_array_release(songNames) save_prepared() - obs.obs_data_set_string(script_sets, "prop_prepared_list", "") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "List of Prepared Songs") prepared_index = 0 pp = obs.obs_properties_get(script_props, "edit_grp") obs.obs_property_set_visible(pp, false) @@ -3044,7 +3069,7 @@ function load_source_song(source, preview) if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles prepare_selected(song) end - transition_lyric_text() + transition_lyric_text(true) if obs.obs_data_get_bool(settings, "source_home_on_active") then home_prepared(true) end From 28a1edbecb5bca74f6d66063f44fca049997af28 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Mon, 25 Oct 2021 17:12:35 -0600 Subject: [PATCH 076/105] Update lyrics+.lua Removed preview button and made preview part of selecting a song from the directory --- lyrics+.lua | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index e275f6b..6b12005 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -451,6 +451,30 @@ function preview_clicked(props, p) return true end +-- called when selection is made from directory list +function preview_selection_made(props, prop, settings) + local name = obs.obs_data_get_string(script_sets, "prop_directory_list") + + if get_index_in_list(song_directory, name) == nil then + return false + end -- do nothing if invalid name + + obs.obs_data_set_string(settings, "prop_edit_song_title", name) + local song_lines = get_song_text(name) + local combined_text = "" + for i, line in ipairs(song_lines) do + if (i < #song_lines) then + combined_text = combined_text .. line .. "\n" + else + combined_text = combined_text .. line + end + end + obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) + + preview_clicked(props,prop) + return true +end + function refresh_button_clicked(props, p) local source_prop = obs.obs_properties_get(props, "prop_source_list") local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") @@ -575,27 +599,6 @@ function prepare_selected(name) return true end --- called when selection is made from directory list -function preview_selection_made(props, prop, settings) - local name = obs.obs_data_get_string(script_sets, "prop_directory_list") - - if get_index_in_list(song_directory, name) == nil then - return false - end -- do nothing if invalid name - - obs.obs_data_set_string(settings, "prop_edit_song_title", name) - local song_lines = get_song_text(name) - local combined_text = "" - for i, line in ipairs(song_lines) do - if (i < #song_lines) then - combined_text = combined_text .. line .. "\n" - else - combined_text = combined_text .. line - end - end - obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) - return true -end function open_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_directory_list") @@ -1847,7 +1850,7 @@ function script_properties() end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) obs.obs_properties_add_button(gp, "prop_prepare_button", "Add Song/Text to Prepared List", prepare_song_clicked) - obs.obs_properties_add_button(gp, "prop_preview_button", "Preview Songs/Text", preview_clicked) + --obs.obs_properties_add_button(gp, "prop_preview_button", "Preview Songs/Text", preview_clicked) obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) From 03d99a2f465332db8b799585123e6243d848768c Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 26 Oct 2021 11:06:21 -0600 Subject: [PATCH 077/105] Some REALLY Obscure mode change bug fixes and documentation Corrected some really obscure bugs that affected changing modes like deleting all the prepared songs while having loaded a song with a source and paged through to a previously prepared song. Also made Previewing a Song an official mode with its own button. This logically made more sense and allowed for songs to be prepared without affecting anything currently displayed. Started on internal documentation. Will continue as time permits. --- lyrics+.lua | 395 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 248 insertions(+), 147 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 6b12005..8601788 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -11,58 +11,85 @@ -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. +--------------------------------------------------------------------------------------------------------------------- +-- Lyrics or Lyrics+ is a joint effort started by amirchev and joined by wzaggle (DC Strato) +-- The lua script breaks up text stored in files to be shown as pages in an OBS stream and is designed to be used +-- with songs, scripture, responsive reading, or any text that needs to have managed pages. + +-- Songs or Text files can be created and edited within the script or with an external text editor +-- Files can be either A) Previewed to the scene, B) Pre-loaded into a "Prepared List" queue to be displayed in +-- the order of the list, or C) loaded on the fly when a scene loads, using a Prepare Lyric source within the scene. + +-- The general function of the script is to modify the contents, visibility, and opacity levels for text sources +-- that have been created within OBS scenes. Currently only a single Global set of text sources are supported for + +-- Text Source -------> The text source that will contain the Pages of song lyrics +-- Title Source ------> The text source that will contain the Title of the current song +-- Alternate Source --> The text source that will contain the Pages of Alternate song lyrics +-- Static Source -----> The text source that will contain any Static Text that will be shown along with lyrics + + +--------------------------------------------------------------------------------------------------------------------- + +-- NOTES ON INTERNAL DOCUMENTATION +-- Effort has been made to try and lay out the general function of the script for those wishing to contribute or just +-- follow its operation. The terms Text File, Song, or Lyric all refer to the text that is the content to be +-- paged by the script, and are used interchangeably within the internal documentation, whatever the purpose or +-- intent of that text might be to the user. + obs = obslua bit = require("bit") --- source definitions -source_data = {} +-- SOURCE DEFINITIONS USED WITH LOAD LYRIC SOURCES source_def = {} source_def.id = "Prepare_Lyrics" source_def.type = OBS_SOURCE_TYPE_INPUT source_def.output_flags = bit.bor(obs.OBS_SOURCE_CUSTOM_DRAW) --- text sources +-- TEXT SOURCE NAMES USED BY LYRICS source_name = "" alternate_source_name = "" static_source_name = "" static_text = "" title_source_name = "" --- settings +-- SETTINGS FOR WHAT OS IS BEING USED (FILE OPERATIONS DEPENDENT) windows_os = false -first_open = true -display_lines = 0 -ensure_lines = true +display_lines = 0 -- lyric preparation option for default lines to display +ensure_lines = true -- page padding to ensure line count on/off --- lyrics/alternate lyrics by page +-- LYRICS/ALTERNATE LYRICS BY PAGE lyrics = {} alternate = {} --- verse indicies if marked +-- VERSE PAGE POINTERS (IF STARING POINTS MARKED IN TEXT WITH ##V) verses = {} -page_index = 0 -- current page of lyrics being displayed -prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected +-- MISC FLAGS AND TABLES +page_index = 0 -- current page of lyrics being displayed +prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected -song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) -prepared_songs = {} -- holds pre-prepared list of songs to use -extra_sources = {} -- holder for extra sources settings -max_opacity = {} -- record maximum opacity settings for sources +song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) +prepared_songs = {} -- holds pre-prepared list of songs to use +extra_sources = {} -- holder for extra sources settings +max_opacity = {} -- record maximum opacity settings for sources -link_text = false -- true if Title and Static should fade with text only during hide/show -link_extras = false -- extras fade with text always when true, only during hide/show when false -all_sources_fade = false -- Title and Static should only fade when lyrics are changing or during show/hide -source_song_title = "" -- The song title from a source loaded song -using_source = false -- true when a lyric load song is being used instead of a pre-prepared song -preview = false -- true if song is not in prepared list nor from a source load -source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) +link_text = false -- true if Title and Static should fade with text only during hide/show +link_extras = false -- extras fade with text always when true, only during hide/show when false -load_scene = "" -- name of scene loading a lyric with a source +source_song_title = "" -- The song title from a source loaded song +using_source = false -- true when a lyric load song is being used instead of a pre-prepared song +using_preview = false -- true if song is not in prepared list nor from a source load + +source_active = false -- true when a lyric load source is active in the current scene (song is loaded or available to load) +load_source = nil -- indicates source that loaded a lyric for use in monitor + +load_scene = "" -- name of scene loading a lyric with a source last_prepared_song = "" -- name of the last prepared song (prevents duplicate loading of already loaded song) --- hotkeys +-- HOTKEY IDS USED IN SETTINGS FILE hotkey_n_id = obs.OBS_INVALID_HOTKEY_ID hotkey_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_c_id = obs.OBS_INVALID_HOTKEY_ID @@ -71,6 +98,7 @@ hotkey_p_p_id = obs.OBS_INVALID_HOTKEY_ID hotkey_home_id = obs.OBS_INVALID_HOTKEY_ID hotkey_reset_id = obs.OBS_INVALID_HOTKEY_ID +-- KEYS USED FOR HOTKEYS IN SETTINGS FILE hotkey_n_key = "" hotkey_p_key = "" hotkey_c_key = "" @@ -79,14 +107,14 @@ hotkey_p_p_key = "" hotkey_home_key = "" hotkey_reset_key = "" --- script placeholders +-- SCRIPT SETTINGS AND PROPERTY POINTERS script_sets = nil script_props = nil source_sets = nil source_props = nil hotkey_props = nil ---monitor variables +--MONITOR VARIABLES USED TO KEEP CURRENT STATISTICS SHOWED IN HTML FILE (TODO: ADD PREVIOUS LYRIC OPTION) mon_song = "" mon_lyric = "" mon_verse = 0 @@ -97,7 +125,12 @@ mon_nextsong = "" meta_tags = "" source_meta_tags = "" --- text status & fade + +-- FLAGS USED IN USER INTERFACE +expandcollapse = true -- flag used in UI progressive disclosure +showhelp = false -- flag used to open and close markup HELP button text + +-- TEXT STATUS & FADE TEXT_VISIBLE = 0 -- text is visible TEXT_HIDDEN = 1 -- text is hidden TEXT_SHOWING = 3 -- going from hidden -> visible @@ -107,41 +140,49 @@ TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change TEXT_HIDE = 7 -- turn off the text and ignore fade if selected TEXT_SHOW = 8 -- turn on the text and ignore fade if selected -text_status = TEXT_VISIBLE -text_opacity = 100 -text_fade_speed = 5 -text_fade_enabled = false -load_source = nil -expandcollapse = true -showhelp = false -use100percent = true -fade_text_back = false -fade_title_back = false -fade_alternate_back = false -fade_static_back = false -fade_extra_back = false -allow_back_fade = false - -transition_enabled = false -- transitions are a work in progress to support duplicate source mode (not very stable) +-- GENERAL TEXT FADING +text_fade_enabled = false -- fading effect enabled (if false then source visibility is toggled rather than opacity changed) +all_sources_fade = false -- Title and Static fade when lyrics are changing or during show/hide +text_status = TEXT_VISIBLE -- current state of desired source visibility, one of above states VISIBLE thru SHOW +text_opacity = 100 -- used to fade text in/out +text_fade_speed = 5 -- speed used to fade text + +-- FLAGS TO CONTROL BACKGROUND FADING +allow_back_fade = false -- overall fading is performed for text source background colors +use100percent = true -- Fading is 0-100% of opacity or 0 to Marked opacity +fade_text_back = false -- Text Background should fade +fade_title_back = false -- Title Background should fade +fade_alternate_back = false -- Alternate Lyric Background should fade +fade_static_back = false -- Static Text Background should fade +fade_extra_back = false -- Extra Linked Source Background should fade (if text source) + +--[[ transitions are a work in progress to support duplicate source mode (not very stable) +in this mode OBS keeps separate sources in preview and active windows. Only pointers to the preview window are +accessable to the API. Changing the Active window requires both changing the Preview Window Text sources then +transitioning those changes to the Active Window. Fading is disabled because its effects are not visible in +the active window. --]] + +transition_enabled = false transition_completed = false -source_saved = false -- ick... A saved toggle to keep from repeating the save function for every song source. Works for now - -editVisSet = false - --- simple debugging/print mechanism ---DEBUG = true -- on switch for entire debugging mechanism ---DEBUG_METHODS = true -- print method names +-- SIMPLE DEBUGGING/PRINT MECHANISM +DEBUG = true -- on switch for entire debugging mechanism +DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false -------- ---------------- ------------------------- CALLBACKS +------------------------ PAGING FUNCTIONS ---------------- -------- - +-- +--------------------------------------------------------------------------------------------------------------------- +-- Function to move to Next page (lyric) of a song or text (HOTKEY ENABLED) +-- contents of text sources are only changed if they are showing to prevent accidental background paging +-- Manages adding a transition for duplicated sources mode if checked +--------------------------------------------------------------------------------------------------------------------- function next_lyric(pressed) if not pressed then return @@ -165,6 +206,11 @@ function next_lyric(pressed) end end +--------------------------------------------------------------------------------------------------------------------- +-- Function to move to Previous page (lyric) of a song or text (HOTKEY ENABLED) +-- contents of text sources are only changed if they are showing to prevent accidental background paging +-- Manages adding a transition for duplicated sources mode if checked +--------------------------------------------------------------------------------------------------------------------- function prev_lyric(pressed) if not pressed then return @@ -181,6 +227,12 @@ function prev_lyric(pressed) end end +--------------------------------------------------------------------------------------------------------------------- +-- Function to move to the Previous Prepared song or text from the Prepared Songs List (HOTKEY ENABLED) +-- Songs rotate through the Prepared List starting back at the last prepared song, if paged backward from the 1st. +-- Songs prepared through an Active Prepare Lyric source are included in the rotation between the last and 1st song +-- Songs currently shown in Preview mode are discarded and the Preview Mode cancelled +--------------------------------------------------------------------------------------------------------------------- function prev_prepared(pressed) if not pressed then return @@ -188,6 +240,7 @@ function prev_prepared(pressed) if #prepared_songs == 0 then return end + using_preview = false if using_source then using_source = false prepare_selected(prepared_songs[prepared_index]) @@ -208,6 +261,12 @@ function prev_prepared(pressed) end end +--------------------------------------------------------------------------------------------------------------------- +-- Function to move to the NEXT Prepared song or text from the Prepared Songs List (HOTKEY ENABLED) +-- Songs rotate through the Prepared List starting at the 1st prepared song, if paged forward from the last. +-- Songs prepared through an Active Prepare Lyric source are included in the rotation between the last and 1st song +-- Songs currently shown in Preview mode are discarded and the Preview Mode cancelled +--------------------------------------------------------------------------------------------------------------------- function next_prepared(pressed) if not pressed then return @@ -215,9 +274,11 @@ function next_prepared(pressed) if #prepared_songs == 0 then return end + using_preview = false if using_source then using_source = false dbg_custom("do current prepared") + print("INDEX: " .. prepared_index) prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song return end @@ -239,42 +300,10 @@ function next_prepared(pressed) end end -function toggle_lyrics_visibility(pressed) - dbg_method("toggle_lyrics_visibility") - if not pressed then - return - end - if link_text then - all_sources_fade = true - end - if text_status ~= TEXT_HIDDEN then - dbg_inner("hiding") - set_text_visibility(TEXT_HIDDEN) - else - dbg_inner("showing") - set_text_visibility(TEXT_VISIBLE) - end -end - -function get_load_lyric_song() - local scene = obs.obs_frontend_get_current_scene() - local scene_items = obs.obs_scene_enum_items(scene) -- Get list of all items in this scene - local song = nil - if scene_items ~= nil then - for _, scene_item in ipairs(scene_items) do -- Loop through all scene source items - local source = obs.obs_sceneitem_get_source(scene_item) -- Get item source pointer - local source_id = obs.obs_source_get_unversioned_id(source) -- Get item source_id - if source_id == "Prepare_Lyrics" then -- Skip if not a Prepare_Lyric source item - local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source - song = obs.obs_data_get_string(settings, "song") -- Get index for this source (set earlier) - obs.obs_data_release(settings) -- release memory - end - end - end - obs.sceneitem_list_release(scene_items) -- Free scene list - return song -end - +--------------------------------------------------------------------------------------------------------------------- +-- Function to move to the FIRST Prepared song or text from the Prepared Songs List (HOTKEY ENABLED) +-- Songs currently shown in Preview mode are discarded and the Preview Mode cancelled +--------------------------------------------------------------------------------------------------------------------- function home_prepared(pressed) if not pressed then return false @@ -282,7 +311,7 @@ function home_prepared(pressed) dbg_method("home_prepared") using_source = false page_index = 0 - + using_preview = false local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) @@ -295,6 +324,9 @@ function home_prepared(pressed) return true end +--------------------------------------------------------------------------------------------------------------------- +-- Function to move to the first page of the current song or text (HOTKEY ENABLED) +--------------------------------------------------------------------------------------------------------------------- function home_song(pressed) if not pressed then return false @@ -305,18 +337,31 @@ function home_song(pressed) return true end -function get_current_scene_name() - dbg_method("get_current_scene_name") - local scene = obs.obs_frontend_get_current_scene() - local current_scene = obs.obs_source_get_name(scene) - obs.obs_source_release(scene) - if current_scene ~= nil then - return current_scene +--------------------------------------------------------------------------------------------------------------------- +-- Function to hide/show the current text within sources (HOTKEY ENABLED) +-- option exists to include the song Title and any linked Extra Sources +--------------------------------------------------------------------------------------------------------------------- +function toggle_lyrics_visibility(pressed) + dbg_method("toggle_lyrics_visibility") + if not pressed then + return + end + if link_text then + all_sources_fade = true + end + if text_status ~= TEXT_HIDDEN then + dbg_inner("hiding") + set_text_visibility(TEXT_HIDDEN) else - return "-" + dbg_inner("showing") + set_text_visibility(TEXT_VISIBLE) end end +--------------------------------------------------------------------------------------------------------------------- +-- Functions used with paging Buttons on the scripts Properties User Interface +-- These buttons simply call the same functions used by the hotkeys +--------------------------------------------------------------------------------------------------------------------- function next_button_clicked(props, p) next_lyric(true) return true @@ -351,16 +396,25 @@ function next_prepared_clicked(props, p) return true end +-------- +---------------- +------------------------ PROGRAM CALLBACKS +---------------- +-------- +--------------------------------------------------------------------------------------------------------------------- +-- Save is called when the users enters a new song title and lyric text to be saved, or modifies an existing song +-- If song is the one last prepared for viewing then update it (allows on-the-fly song edits) +--------------------------------------------------------------------------------------------------------------------- function save_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_edit_song_title") local text = obs.obs_data_get_string(script_sets, "prop_edit_song_text") -- if this is a new song, add it to the directory - if save_song(name, text) then + if save_song(name, text) then -- new song so add to directory table local prop_dir_list = obs.obs_properties_get(props, "prop_directory_list") obs.obs_property_list_add_string(prop_dir_list, name, name) obs.obs_data_set_string(script_sets, "prop_directory_list", name) obs.obs_properties_apply_settings(props, script_sets) - elseif prepared_songs[prepared_index] == name then + elseif name == last_prepared_song then -- if this song is being displayed, then prepare it anew prepare_song_by_name(name) transition_lyric_text(false) @@ -414,15 +468,14 @@ end -- function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") - if #prepared_songs == 0 then - set_text_visibility(TEXT_HIDDEN) - end - prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") + set_text_visibility(TEXT_HIDDEN) + prepared_songs[#prepared_songs + 1] = obs.obs_data_get_string(script_sets, "prop_directory_list") + prepared_index = 1 local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_add_string(prop_prep_list, prepared_songs[#prepared_songs], prepared_songs[#prepared_songs]) -- next line PREPARES the newly Added Song -- obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[#prepared_songs]) - obs.obs_data_set_string(script_sets, "prop_prepared_list", "List of Prepared Songs") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") if #prepared_songs > 0 then obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") else @@ -438,16 +491,25 @@ end -- function preview_clicked(props, p) dbg_method("preview_song_clicked") + if using_preview then + using_preview = false + if source_active then + load_source_song(load_source, false) + elseif #prepared_songs > 0 then + prepare_song_by_index(prepared_index) + end + transition_lyric_text() + return true + end local song = obs.obs_data_get_string(script_sets, "prop_directory_list") using_source = true - preview = true + using_preview = true all_sources_fade = true -- fade title and source the first time set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles prepare_selected(song) end transition_lyric_text(true) - preview = false return true end @@ -470,8 +532,8 @@ function preview_selection_made(props, prop, settings) end end obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) - - preview_clicked(props,prop) + update_monitor() + --preview_clicked(props,prop) return true end @@ -544,8 +606,10 @@ end -- Called with ANY change to the prepared song list function prepare_selection_made(props, prop, settings) dbg_method("prepare_selection_made") + --set_text_visibility(HIDDEN) local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false + using_preview = false prepare_selected(name) return true @@ -555,16 +619,20 @@ end function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") prepared_songs = {} -- required for monitor page - page_index = 0 -- required for monitor page - prepared_index = 0 -- required for monitor page + page_index = 0 -- required for monitor page + prepared_index = 0 -- required for monitor page save_prepared() + if source_active then + load_source_song(load_source, false) + end update_monitor() -- required for monitor page + transition_lyric_text() -- clear the list local prep_prop = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prep_prop) obs.obs_property_set_description(obs.obs_properties_get(props, "prop_prepared_list"), "Prepared") - obs.obs_property_list_add_string(obs.obs_properties_get(props, "prop_prepared_list"), "List of Prepared Songs", "") - obs.obs_data_set_string(script_sets, "prop_prepared_list", "List of Prepared Songs") + obs.obs_property_list_add_string(obs.obs_properties_get(props, "prop_prepared_list"), "*** LIST OF PREPARED SONGS ***", "") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") local pp = obs.obs_properties_get(props, "edit_grp") if obs.obs_property_visible(pp) then obs.obs_property_set_visible(pp, false) @@ -624,11 +692,6 @@ function open_button_clicked(props, p) end end --------- ----------------- ------------------------- PROGRAM FUNCTIONS ----------------- --------- function setSourceOpacity(sourceName, fadeBackground) dbg_method("set_Opacity") if sourceName ~= nil and sourceName ~= "" then @@ -863,7 +926,6 @@ function update_source_text() local static = static_text local mstatic = static -- save static for use with monitor local title = "" - if alt_title ~= "" then title = alt_title else @@ -1023,7 +1085,31 @@ function prepare_song_by_index(index) end end --- prepares lyrics of the song +--------------------------------------------------------------------------------------------------------------------- +-- Function to parse and process markups within the lyrics and break the text into defined pages and verses +-- The first line of song/text files can contain an optional list of meta tags that organize the files into +-- user defined genre or categories for later filtering during selection + +-- Currently supported markups all start with # or // + +-- Display n Lines #L:n +-- End Page after Line Line ### +-- Blank (Pad) Line ##B or ##P +-- Blank(Pad) Lines #B:n or #P:n +-- External Refrain #r[ and #r] +-- In-Line Refrain #R[ and #R] +-- Repeat Refrain ##r or ##R +-- Duplicate Line n times #D:n Line\n" +-- Static Lines #S[ and #s] +-- Single Static Line #S: Line +-- Alternate Text #A[ and #A] +-- Alt Line Repeat n Pages #A:n Line +-- Comment Line // Line +-- Block Comments //[ and //] +-- Mark Verses ##V +-- Override Title #T: text +-- Optional comma delimited meta tags follow '//meta ' on 1st line" +--------------------------------------------------------------------------------------------------------------------- function prepare_song_by_name(name) dbg_method("prepare_song_by_name") if name == nil then @@ -1452,6 +1538,7 @@ function load_source_song_directory(use_filter) until not entry obs.os_closedir(dir) end + -- -- reads the first line of each lyric file, looks for the //meta comment and returns any CSV tags that exist -- @@ -1652,12 +1739,10 @@ function update_monitor() text = text .. "
" - if using_source then - if preveiw then - text = text .. "From Source: " .. load_scene .. "
" - else - text = text .. "From Preview " .. load_scene .. "
" - end + if using_preview then + text = text .. "From Preview
" + elseif using_source then + text = text .. "From Source: " .. load_scene .. "
" else text = text .. "Prepared Song: " .. prepared_index text = @@ -1666,9 +1751,14 @@ function update_monitor() end text = text .. - "
Lyric Page: " .. - page_index - text = text .. " of " .. #lyrics .. "
" + "
Lyric Page: " + local pages = (#lyrics == 0) and 0 or #lyrics-1 + if page_index < #lyrics then + text = text .. page_index.. " of " .. pages .. "
" + else + text = text .. "Blank
" + end + if #verses ~= nil and mon_verse > 0 then text = text .. @@ -1850,7 +1940,7 @@ function script_properties() end obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) obs.obs_properties_add_button(gp, "prop_prepare_button", "Add Song/Text to Prepared List", prepare_song_clicked) - --obs.obs_properties_add_button(gp, "prop_preview_button", "Preview Songs/Text", preview_clicked) + obs.obs_properties_add_button(gp, "prop_preview_button", "Preview Songs/Text", preview_clicked) obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) @@ -1865,11 +1955,11 @@ function script_properties() obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) - obs.obs_property_list_add_string(prepare_prop, "List of Prepared Songs","") + obs.obs_property_list_add_string(prepare_prop, "*** LIST OF PREPARED SONGS ***","") for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end - obs.obs_data_set_string(script_sets, "prop_prepared_list", "List of Prepared Songs") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) local count = obs.obs_property_list_item_count(prepare_prop) if count > 1 then @@ -1887,7 +1977,6 @@ function script_properties() nil, nil ) - obs.obs_property_set_modified_callback(edit_prop, setEditVis) obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes", save_edits_clicked) local edit_group_prop = obs.obs_properties_add_group( @@ -2071,6 +2160,7 @@ function script_properties() obs.obs_property_set_visible(febprop, text_fade_enabled and allow_back_fade) obs.obs_property_set_visible(oprefprop, text_fade_enabled and not use100percent) + read_source_opacity() return script_props end @@ -2344,17 +2434,6 @@ function show_help_button(props, prop, settings) return true end -function setEditVis(props, prop, settings) -- hides edit group on initial showing - dbg_method("setEditVis") - if not editVisSet then - local pp = obs.obs_properties_get(script_props, "edit_grp") - obs.obs_property_set_visible(pp, false) - pp = obs.obs_properties_get(props, "meta") - obs.obs_property_set_visible(pp, false) - editVisSet = true - end -end - function filter_songs_clicked(props, p) local pp = obs.obs_properties_get(props, "meta") if not obs.obs_property_visible(pp) then @@ -2411,7 +2490,7 @@ function save_edits_clicked(props, p) prepared_songs = {} local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") obs.obs_property_list_clear(prop_prep_list) - obs.obs_property_list_add_string(prop_prep_list, "List of Prepared Songs", "") + obs.obs_property_list_add_string(prop_prep_list, "*** LIST OF PREPARED SONGS ***", "") local songNames = obs.obs_data_get_array(script_sets, "prep_list") local count2 = obs.obs_data_array_count(songNames) if count2 > 0 then @@ -2427,7 +2506,7 @@ function save_edits_clicked(props, p) end obs.obs_data_array_release(songNames) save_prepared() - obs.obs_data_set_string(script_sets, "prop_prepared_list", "List of Prepared Songs") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") prepared_index = 0 pp = obs.obs_properties_get(script_props, "edit_grp") obs.obs_property_set_visible(pp, false) @@ -2467,6 +2546,7 @@ end function load_prepared(settings) dbg_method("Load Prepared") + prepared_songs = {} if saveExternal then -- loads prepared songs from prepared.dat file -- load prepared songs from stored file -- @@ -2485,13 +2565,16 @@ dbg_method("Load Prepared") local item = obs.obs_data_array_item(prepared_songs_array, i) local songName = obs.obs_data_get_string(item, "value") if songName ~= "" then - prepared_songs[#prepared_songs + 1] = songName + prepared_songs[i+1] = songName end obs.obs_data_release(item) end end obs.obs_data_array_release(prepared_songs_array) end + if #prepared_songs > 0 then + prepared_index = 1 + end end function save_prepared(settings) @@ -2992,7 +3075,7 @@ source_def.get_properties = function(data) obs.obs_property_list_add_string(source_dir_list, name, name) end gps = obs.obs_properties_create() - source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + source_metatags = obs.obs_properties_add_text(gps, "metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) obs.obs_property_set_modified_callback(source_metatags, update_source_metatags) obs.obs_properties_add_button(gps, "source_dir_refresh", "Refresh Directory", source_refresh_button_clicked) obs.obs_properties_add_group(source_props, "meta", "Filter Songs", obs.OBS_GROUP_NORMAL, gps) @@ -3066,11 +3149,13 @@ function load_source_song(source, preview) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") using_source = true + using_preview = false load_source = source all_sources_fade = true -- fade title and source the first time set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in if song ~= last_prepared_song then -- skips prepare if song already prepared just to save some processing cycles prepare_selected(song) + page_index = 1 end transition_lyric_text(true) if obs.obs_data_get_bool(settings, "source_home_on_active") then @@ -3080,6 +3165,22 @@ function load_source_song(source, preview) obs.obs_data_release(settings) end + +-- Utility function to retrieve the name of the current scene for use in Monitor + +function get_current_scene_name() + dbg_method("get_current_scene_name") + local scene = obs.obs_frontend_get_current_scene() + local current_scene = obs.obs_source_get_name(scene) + obs.obs_source_release(scene) + if current_scene ~= nil then + return current_scene + else + return "-" + end +end + + -- Call back when load source (not text source) goes to the Active Scene -- loads the selected song and sets the current scene name for the HTML monitor -- From 94b033ee63697e434f3413c29f50e652f22e5a56 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Tue, 26 Oct 2021 21:52:04 -0600 Subject: [PATCH 078/105] Update lyrics+.lua More internal documentation --- lyrics+.lua | 3714 +++++++++++++++++++++++++++------------------------ 1 file changed, 1963 insertions(+), 1751 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 8601788..1b31d70 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -398,7 +398,7 @@ end -------- ---------------- ------------------------- PROGRAM CALLBACKS +------------------------ PROGRAM FUNCTION BUTTONS AND CALLBACKS ---------------- -------- --------------------------------------------------------------------------------------------------------------------- @@ -422,6 +422,11 @@ function save_song_clicked(props, p) return true end +--------------------------------------------------------------------------------------------------------------------- +-- Called to delete the current song entirely +-- removed from prepared list if present +-- clears display if last song in directory +--------------------------------------------------------------------------------------------------------------------- function delete_song_clicked(props, p) dbg_method("delete_song_clicked") -- call delete song function @@ -463,9 +468,11 @@ function delete_song_clicked(props, p) return true end --- prepare song button clicked --- Adds the currently selected song from the directory to the prepared list --- +----------------------------------------------------------------------------------------------------------------------- +-- PREPARE SONG +-- Adds the currently selected song from the directory to the prepared list. Sets index to first song. +----------------------------------------------------------------------------------------------------------------------- + function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") set_text_visibility(TEXT_HIDDEN) @@ -486,9 +493,10 @@ function prepare_song_clicked(props, p) return true end --- preview song clicked --- Prepares the selected song without adding it to the prepared list --- +------------------------------------------------------------------------------------------------------------------------- +-- PREVIEW SONG (Preveiw Mode) +-- Prepares the selected song into text sources without adding it to the prepared list +------------------------------------------------------------------------------------------------------------------------- function preview_clicked(props, p) dbg_method("preview_song_clicked") if using_preview then @@ -513,8 +521,12 @@ function preview_clicked(props, p) return true end --- called when selection is made from directory list -function preview_selection_made(props, prop, settings) +------------------------------------------------------------------------------------------------------------------------- +-- LOAD SONG FROM DIRECTORY +-- Loads the text from the selected song into the Lyrics Edit window for editing within properties +-- Selects the current song for possible addition to prepared list +------------------------------------------------------------------------------------------------------------------------- +function load_song_from_directory(props, prop, settings) local name = obs.obs_data_get_string(script_sets, "prop_directory_list") if get_index_in_list(song_directory, name) == nil then @@ -533,11 +545,17 @@ function preview_selection_made(props, prop, settings) end obs.obs_data_set_string(settings, "prop_edit_song_text", combined_text) update_monitor() - --preview_clicked(props,prop) + return true end +------------------------------------------------------------------------------------------------------------------------- +-- REFRESH SOURCES +-- New sources added to OBS are not automatically included (yet) +-- Also refreshes the Directory of Songs +------------------------------------------------------------------------------------------------------------------------- function refresh_button_clicked(props, p) +dbg_method("Refresh Sources") local source_prop = obs.obs_properties_get(props, "prop_source_list") local alternate_source_prop = obs.obs_properties_get(props, "prop_alternate_list") local static_source_prop = obs.obs_properties_get(props, "prop_static_list") @@ -583,6 +601,10 @@ function refresh_button_clicked(props, p) return true end +------------------------------------------------------------------------------------------------------------------------- +-- REFRESH DIRECTORY +-- Refreshes the Directory of Songs +------------------------------------------------------------------------------------------------------------------------- function refresh_directory_button_clicked(props, p) dbg_method("refresh directory") refresh_directory() @@ -603,10 +625,14 @@ function refresh_directory() obs.obs_properties_apply_settings(script_props, script_sets) end +------------------------------------------------------------------------------------------------------------------------- +-- PREPARE SELECTION +-- Selecting a song from the Prepared List will prepare it into sources -- Called with ANY change to the prepared song list +------------------------------------------------------------------------------------------------------------------------- + function prepare_selection_made(props, prop, settings) dbg_method("prepare_selection_made") - --set_text_visibility(HIDDEN) local name = obs.obs_data_get_string(settings, "prop_prepared_list") using_source = false using_preview = false @@ -615,7 +641,11 @@ function prepare_selection_made(props, prop, settings) return true end --- removes prepared songs + +------------------------------------------------------------------------------------------------------------------------- +-- CLEAR PREPARED SONGS LIST +-- Removes all songs from the prepared list +------------------------------------------------------------------------------------------------------------------------- function clear_prepared_clicked(props, p) dbg_method("clear_prepared_clicked") prepared_songs = {} -- required for monitor page @@ -642,32 +672,10 @@ function clear_prepared_clicked(props, p) return true end --- --- This function prepares the currently selected song -function prepare_selected(name) - dbg_method("prepare_selected") - -- try to prepare song - if prepare_song_by_name(name) then - page_index = 1 - if not using_source then - prepared_index = get_index_in_list(prepared_songs, name) - else - source_song_title = name - all_sources_fade = true - end - - transition_lyric_text(using_source) - else - -- hide everything if unable to prepare song - -- TODO: clear lyrics entirely after text is hidden - set_text_visibility(TEXT_HIDDEN) - end - - --update_source_text() - return true -end - - +------------------------------------------------------------------------------------------------------------------------- +-- Open_Song_CLICKED +-- Tries to open the selected song in the default OS text editor +------------------------------------------------------------------------------------------------------------------------- function open_song_clicked(props, p) local name = obs.obs_data_get_string(script_sets, "prop_directory_list") if testValid(name) then @@ -683,6 +691,10 @@ function open_song_clicked(props, p) return true end +------------------------------------------------------------------------------------------------------------------------- +-- OPEN SONGS FOLDER +-- Opens the folder where song files are stored +------------------------------------------------------------------------------------------------------------------------- function open_button_clicked(props, p) local path = get_songs_folder_path() if windows_os then @@ -692,1858 +704,2049 @@ function open_button_clicked(props, p) end end -function setSourceOpacity(sourceName, fadeBackground) - dbg_method("set_Opacity") - if sourceName ~= nil and sourceName ~= "" then - if text_fade_enabled then - local settings = obs.obs_data_create() - if use100percent then -- try to honor preset maximum opacities - obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity - if fadeBackground then - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity - end - else - adj_text_opacity = text_opacity /100 - obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero - obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity - if fadeBackground then - obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity - end - end - local source = obs.obs_get_source_by_name(sourceName) - if source ~= nil then - obs.obs_source_update(source, settings) - end - obs.obs_source_release(source) - obs.obs_data_release(settings) - else - dbg_inner("use on/off") - -- do preview scene item - local sceneSource = obs.obs_frontend_get_current_preview_scene() - local sceneObj = obs.obs_scene_from_source(sceneSource) - local sceneItem = obs.obs_scene_find_source_recursive(sceneObj, sourceName) - obs.obs_source_release(sceneSource) - if text_opacity > 50 then - obs.obs_sceneitem_set_visible(sceneItem, true) - else - obs.obs_sceneitem_set_visible(sceneItem, false) - end - end - update_monitor() - end +------------------------------------------------------------------------------------------------------------------------- +-- READ SOURCE OPACITIES +-- Reads the current source opacity levels +------------------------------------------------------------------------------------------------------------------------- +function read_source_opacity_clicked(props, p) + dbg_method("read_opacities_clicked") + read_source_opacity() + return true end -function apply_source_opacity() -dbg_method("Apply Opacity") - setSourceOpacity(source_name, fade_text_back) - setSourceOpacity(alternate_source_name, fade_alternate_back) - if all_sources_fade then - setSourceOpacity(title_source_name, fade_title_back) - setSourceOpacity(static_source_name, fade_static_back) - end - if link_extras or all_sources_fade then - local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - if count > 0 then - for i = 0, count - 1 do - local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local extra_source = obs.obs_get_source_by_name(sourceName) - if extra_source ~= nil then - source_id = obs.obs_source_get_unversioned_id(extra_source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - setSourceOpacity(sourceName, fade_extra_back) - else -- check for filter named "Color Correction" - local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") - if color_filter ~= nil and text_fade_enabled then -- update filters opacity - local filter_settings = obs.obs_data_create() - if use100percent then - obs.obs_data_set_double(filter_settings, "opacity", text_opacity/100) - else - obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) - end - obs.obs_source_update(color_filter, filter_settings) - obs.obs_data_release(filter_settings) - obs.obs_source_release(color_filter) - else -- try to just change visibility in the scene - local sceneSource = obs.obs_frontend_get_current_preview_scene() - local sceneObj = obs.obs_scene_from_source(sceneSource) - local sceneItem = obs.obs_scene_find_source_recursive(sceneObj, sourceName) - obs.obs_source_release(sceneSource) - if text_opacity > 50 then - obs.obs_sceneitem_set_visible(sceneItem, true) - else - obs.obs_sceneitem_set_visible(sceneItem, false) - end - end - end - end - obs.obs_source_release(extra_source) -- release source ptr - end - end + +-------------------------------------------------------------------------------------------------------------------------- -- ADD LINKED Source Callback +-- adds an extra linked source to the linked sources list. +-- Source must be text source, or have 'Color Correction' Filter applied +------------------------------------------------------------------------------------------------------------------------ +function link_source_selected(props, prop, settings) + dbg_method("link_source_selected") + local extra_source = obs.obs_data_get_string(settings, "extra_source_list") + if extra_source ~= nil and extra_source ~= "" then + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) + obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) + obs.obs_data_set_string(script_sets, "extra_source_list", "") + obs.obs_property_set_description( + extra_linked_list, + "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" + ) end + return true end -function getSourceOpacity(sourceName) - if sourceName ~= nil and sourceName ~= "" then - local source = obs.obs_get_source_by_name(sourceName) - local settings = obs.obs_source_get_settings(source) - max_opacity[sourceName]={} - max_opacity[sourceName]["opacity"] = obs.obs_data_get_int(settings, "opacity") -- text opacity - max_opacity[sourceName]["outline"] = obs.obs_data_get_int(settings, "outline_opacity") -- outline opacity - max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity - max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity - obs.obs_source_release(source) - obs.obs_data_release(settings) - end -end +----------------------------------------------------------------------------------------------------------------------- +-- DO LINKED Option (Progressive disclosure) +-- Shows list to allow linking extra sources +------------------------------------------------------------------------------------------------------------------------ +function do_linked_clicked(props, p) + dbg_method("do_link_clicked") + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), true) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), false) + obs.obs_properties_apply_settings(props, script_sets) --- removes prepared songs -function read_source_opacity_clicked(props, p) - dbg_method("read_opacities_clicked") - read_source_opacity() return true end -function read_source_opacity() - dbg_method("read_source_opacity") - getSourceOpacity(source_name) - getSourceOpacity(alternate_source_name) - getSourceOpacity(title_source_name) - getSourceOpacity(static_source_name) - local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") - local count = obs.obs_property_list_item_count(extra_linked_list) - if count > 0 then - for i = 0, count - 1 do - local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name - local extra_source = obs.obs_get_source_by_name(sourceName) - if extra_source ~= nil then - source_id = obs.obs_source_get_unversioned_id(extra_source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object - getSourceOpacity(sourceName) - else -- check for filter named "Color Correction" - - local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") - if color_filter ~= nil then -- update filters opacity - local filter_settings = obs.obs_source_get_settings(color_filter) - max_opacity[sourceName]={} - max_opacity[sourceName]["CC-opacity"] = obs.obs_data_get_double(filter_settings, "opacity") - obs.obs_data_release(filter_settings) - obs.obs_source_release(color_filter) - end - end - end - obs.obs_source_release(extra_source) -- release source ptr - end - end -end - -function set_text_visibility(end_status) - dbg_method("set_text_visibility") - -- if already at desired visibility, then exit - if text_status == end_status then - return - end - if end_status == TEXT_HIDE then - text_opacity = 0 - text_status = end_status - apply_source_opacity() - return - elseif end_status == TEXT_SHOW then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - apply_source_opacity() - return - end - if text_fade_enabled then - -- if fade enabled, begin fade in or out - if end_status == TEXT_HIDDEN then - text_status = TEXT_HIDING - elseif end_status == TEXT_VISIBLE then - text_status = TEXT_SHOWING - end - --all_sources_fade = true - start_fade_timer() - else -- change visibility immediately (fade or no fade) - if end_status == TEXT_HIDDEN then - text_opacity = 0 - text_status = end_status - elseif end_status == TEXT_VISIBLE then - text_opacity = 100 - text_status = end_status - all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden - end - apply_source_opacity() - --update_source_text() - all_sources_fade = false - return - end +----------------------------------------------------------------------------------------------------------------------- +-- CLEAR LINKED Option +-- Removes all additionally linked Show/Hide sources and hides Link Options in UI +------------------------------------------------------------------------------------------------------------------------ +function clear_linked_clicked(props, p) + dbg_method("clear_linked_clicked") + local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") + + obs.obs_property_list_clear(extra_linked_list) + obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) + obs.obs_property_set_description(extra_linked_list, "Linked Sources") + + return true end --- transition to the next lyrics, use fade if enabled --- if lyrics are hidden, force_show set to true will make them visible -function transition_lyric_text(force_show) - dbg_method("transition_lyric_text") - dbg_bool("force show", force_show) - -- update the lyrics display immediately on 2 conditions - -- a) the text is hidden or hiding, and we will not force it to show - -- b) text fade is not enabled - -- otherwise, start text transition out and update the lyrics once - -- fade out transition is complete - if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then - update_source_text() - -- if text is done hiding, we can cancel the all_sources_fade - if text_status == TEXT_HIDDEN then - all_sources_fade = false - end - dbg_inner("hidden") - elseif not text_fade_enabled then - dbg_custom("Instant On") - -- if text fade is not enabled, then we can cancel the all_sources_fade - all_sources_fade = false - set_text_visibility(TEXT_VISIBLE) -- does update_source_text() - update_source_text() - dbg_inner("no text fade") - else -- initiate fade out/in - dbg_custom("Transition Timer") - text_status = TEXT_TRANSITION_OUT - start_fade_timer() - end - dbg_bool("using_source", using_source) -end - --- updates the selected lyrics -function update_source_text() - dbg_method("update_source_text") - dbg_custom("Page Index: " .. page_index) - local text = "" - local alttext = "" - local next_lyric = "" - local next_alternate = "" - local static = static_text - local mstatic = static -- save static for use with monitor - local title = "" - if alt_title ~= "" then - title = alt_title - else - if not using_source then - if prepared_index ~= nil and prepared_index ~= 0 then - dbg_custom("Update from prepared: " .. prepared_index) - title = prepared_songs[prepared_index] - end - else - dbg_custom("Updatefrom source: " .. source_song_title) - title = source_song_title - end - end - - local source = obs.obs_get_source_by_name(source_name) - local alt_source = obs.obs_get_source_by_name(alternate_source_name) - local stat_source = obs.obs_get_source_by_name(static_source_name) - local title_source = obs.obs_get_source_by_name(title_source_name) - - if using_source or (prepared_index ~= nil and prepared_index ~= 0) then - if #lyrics > 0 then - if lyrics[page_index] ~= nil then - text = lyrics[page_index] - end - end - if #alternate > 0 then - if alternate[page_index] ~= nil then - alttext = alternate[page_index] - end - end - - if link_text then - if string.len(text) == 0 and string.len(alttext) == 0 then - --static = "" - --title = "" - end - end - end - -- update source texts - if source ~= nil then - dbg_inner("Title Load") - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", text) - obs.obs_source_update(source, settings) - obs.obs_data_release(settings) - next_lyric = lyrics[page_index + 1] - if (next_lyric == nil) then - next_lyric = "" - end - end - if alt_source ~= nil then - local settings = obs.obs_data_create() -- setup TEXT settings with opacity values - obs.obs_data_set_string(settings, "text", alttext) - obs.obs_source_update(alt_source, settings) - obs.obs_data_release(settings) - next_alternate = alternate[page_index + 1] - if (next_alternate == nil) then - next_alternate = "" - end - end - if stat_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", static) - obs.obs_source_update(stat_source, settings) - obs.obs_data_release(settings) - end - if title_source ~= nil then - local settings = obs.obs_data_create() - obs.obs_data_set_string(settings, "text", title) - obs.obs_source_update(title_source, settings) - obs.obs_data_release(settings) - end - -- release source references - obs.obs_source_release(source) - obs.obs_source_release(alt_source) - obs.obs_source_release(stat_source) - obs.obs_source_release(title_source) - - local next_prepared = "" - if using_source then - next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song - elseif prepared_index < #prepared_songs then - next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song - else - if source_active then - next_prepared = source_song_title -- plan to go back to source loaded song - else - next_prepared = prepared_songs[1] -- plan to loop around to first prepared song - end - end - mon_verse = 0 - if #verses ~= nil then --find valid page Index - for i = 1, #verses do - if page_index >= verses[i] + 1 then - mon_verse = i - end - end -- v = current verse number for this page - end - mon_song = title - mon_lyric = text:gsub("\n", "
• ") - mon_nextlyric = next_lyric:gsub("\n", "
• ") - mon_alt = alttext:gsub("\n", "
• ") - mon_nextalt = next_alternate:gsub("\n", "
• ") - mon_nextsong = next_prepared - - update_monitor() +--- +--- Progressive Disclosure functions for UI +--- +----------------------------------------------------------------------------------------------------------------------- +-- Working function to return UP or Down pointing arrows +------------------------------------------------------------------------------------------------------------------------ +function vMode(vis) + return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" end - -function start_fade_timer() - dbgsp("started fade timer") - obs.timer_add(fade_callback, 50) +----------------------------------------------------------------------------------------------------------------------- +-- Function to Expand/Rollup all groups in the UI +------------------------------------------------------------------------------------------------------------------------ +function expand_all_groups(props, prop, settings) + expandcollapse = not expandcollapse + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "info_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "mng_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "disp_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "src_grp"), expandcollapse) + obs.obs_property_set_visible(obs.obs_properties_get(script_props, "ctrl_grp"), expandcollapse) + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + return true end -function fade_callback() - -- if not in a transitory state, exit callback - if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then - obs.remove_current_callback() - all_sources_fade = false - end - -- the amount we want to change opacity by - local opacity_delta = 1 + text_fade_speed - -- change opacity in the direction of transitory state - if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then - local new_opacity = text_opacity - opacity_delta - if new_opacity > 0 then - text_opacity = new_opacity - else - -- completed fade out, determine next move - text_opacity = 0 - if text_status == TEXT_TRANSITION_OUT then - -- update to new lyric between fades - update_source_text() - -- begin transition back in - text_status = TEXT_TRANSITION_IN - else - text_status = TEXT_HIDDEN - end - end - elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then - local new_opacity = text_opacity + opacity_delta - if new_opacity < 100 then - text_opacity = new_opacity - else - -- completed fade in - text_opacity = 100 - text_status = TEXT_VISIBLE - end +----------------------------------------------------------------------------------------------------------------------- +-- Function tests to see of all Groups are expanded or contracted +------------------------------------------------------------------------------------------------------------------------ +function all_vis_equal(props) + if + (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "prep_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) and + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) or + not (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "mng_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) or + obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) + then + expandcollapse = not expandcollapse + local mode1, mode2 = vMode(expandecollapse) + obs.obs_property_set_description( + obs.obs_properties_get(props, "expand_all_button"), + mode1 .. "ALL GROUPS" .. mode2 + ) end - -- apply the new opacity - apply_source_opacity() end - -function prepare_song_by_index(index) - dbg_method("prepare_song_by_index") - if index <= #prepared_songs then - prepare_song_by_name(prepared_songs[index]) - end +----------------------------------------------------------------------------------------------------------------------- +-- Change state of the INFO Group +------------------------------------------------------------------------------------------------------------------------ +function change_info_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "info_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "info_showing"), + mode1 .. "SONG INFORMATION" .. mode2 + ) + all_vis_equal(props) + return true end - ---------------------------------------------------------------------------------------------------------------------- --- Function to parse and process markups within the lyrics and break the text into defined pages and verses --- The first line of song/text files can contain an optional list of meta tags that organize the files into --- user defined genre or categories for later filtering during selection - --- Currently supported markups all start with # or // - --- Display n Lines #L:n --- End Page after Line Line ### --- Blank (Pad) Line ##B or ##P --- Blank(Pad) Lines #B:n or #P:n --- External Refrain #r[ and #r] --- In-Line Refrain #R[ and #R] --- Repeat Refrain ##r or ##R --- Duplicate Line n times #D:n Line\n" --- Static Lines #S[ and #s] --- Single Static Line #S: Line --- Alternate Text #A[ and #A] --- Alt Line Repeat n Pages #A:n Line --- Comment Line // Line --- Block Comments //[ and //] --- Mark Verses ##V --- Override Title #T: text --- Optional comma delimited meta tags follow '//meta ' on 1st line" ---------------------------------------------------------------------------------------------------------------------- -function prepare_song_by_name(name) - dbg_method("prepare_song_by_name") - if name == nil then - return false +----------------------------------------------------------------------------------------------------------------------- +-- Change state of the Prepared Group +------------------------------------------------------------------------------------------------------------------------ +function change_prepared_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "mng_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "prepared_showing"), + mode1 .. "PREPARED SONGS" .. mode2 + ) + all_vis_equal(props) + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Change state of the Options Group +------------------------------------------------------------------------------------------------------------------------ +function change_options_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "disp_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "options_showing"), + mode1 .. "DISPLAY OPTIONS" .. mode2 + ) + all_vis_equal(props) + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Change state of the Source Group +------------------------------------------------------------------------------------------------------------------------ +function change_src_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "src_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description( + obs.obs_properties_get(props, "src_showing"), + mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 + ) + all_vis_equal(props) + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Change state of the Control Group +------------------------------------------------------------------------------------------------------------------------ +function change_ctrl_visible(props, prop, settings) + local pp = obs.obs_properties_get(script_props, "ctrl_grp") + local vis = not obs.obs_property_visible(pp) + obs.obs_property_set_visible(pp, vis) + local mode1, mode2 = vMode(vis) + obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) + all_vis_equal(props) + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Change state of the Fade Option +-- Fading enables or disables a number of other options like 0-100% and if backgrounds are faded +------------------------------------------------------------------------------------------------------------------------ +function change_fade_property(props, prop, settings) + local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") + obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "use100percent"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "allowBackFade"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), text_fade_enabled and not use100percent) + local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") + obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Change state of the 0-100% option +----------------------------------------------------------------------------------------------------------------------- +function change_100percent_property(props, prop, settings) + use100percent = obs.obs_data_get_bool(settings, "use100percent") + obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), not use100percent) + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Change background fading options +----------------------------------------------------------------------------------------------------------------------- +function change_back_fade_property(props, prop, settings) + allow_back_fade = obs.obs_data_get_bool(settings, "allowBackFade") + if allow_back_fade then + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_enabled) + else + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), false) + end + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Show/Hide the Help Button Text +----------------------------------------------------------------------------------------------------------------------- +function show_help_button(props, prop, settings) + dbg_method("show help") + local hb = obs.obs_properties_get(props, "show_help_button") + showhelp = not showhelp + if showhelp then + obs.obs_property_set_description(hb, help) + else + obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") end - last_prepared_song = name - -- if using transition on lyric change, first transition - -- would be reset with new song prepared - transition_completed = false - -- load song lines - local song_lines = get_song_text(name) - if song_lines == nil then - return false + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Allow specifying meta tags for filtering songs +----------------------------------------------------------------------------------------------------------------------- +function filter_songs_clicked(props, p) + local pp = obs.obs_properties_get(props, "meta") + if not obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "filter_songs_button") + obs.obs_property_set_description(mpb, "Clear Filters") -- change button function + meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + refresh_directory() + else + obs.obs_property_set_visible(pp, false) + meta_tags = "" -- clear meta tags + refresh_directory() + local mpb = obs.obs_properties_get(props, "filter_songs_button") -- + obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function end - local cur_line = 1 - local cur_aline = 1 - local recordRefrain = false - local playRefrain = false - local use_alternate = false - local use_static = false - local showText = true - local commentBlock = false - local singleAlternate = false - local refrain = {} - local arefrain = {} - lyrics = {} - verses = {} - alternate = {} - static_text = "" - alt_title = "" - local adjusted_display_lines = display_lines - local refrain_display_lines = display_lines - local alternate_display_lines = display_lines - local displaySize = display_lines - for _, line in ipairs(song_lines) do - local new_lines = 1 - local single_line = false - local comment_index = line:find("//%[") -- Look for comment block Set - if comment_index ~= nil then - commentBlock = true - line = line:sub(comment_index + 3) - end - comment_index = line:find("//]") -- Look for comment block Clear - if comment_index ~= nil then - commentBlock = false - line = line:sub(1, comment_index - 1) - new_lines = 0 + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Edit the prepared list by copying it into a Editable list where entries can be added/deleted or reordered +----------------------------------------------------------------------------------------------------------------------- +function edit_prepared_clicked(props, p) + local pp = obs.obs_properties_get(props, "edit_grp") + if obs.obs_property_visible(pp) then + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared List") + return true + end + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + local count = obs.obs_property_list_item_count(prop_prep_list) + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + if count2 > 0 then + for i = 0, count2 do + obs.obs_data_array_erase(songNames, 0) end - if not commentBlock then - local comment_index = line:find("%s*//") - if comment_index ~= nil then - line = line:sub(1, comment_index - 1) - new_lines = 0 - end - local alternate_index = line:find("#A%[") - if alternate_index ~= nil then - use_alternate = true - line = line:sub(1, alternate_index - 1) - new_lines = 0 - end - alternate_index = line:find("#A]") - if alternate_index ~= nil then - use_alternate = false - line = line:sub(1, alternate_index - 1) - new_lines = 0 - end - local static_index = line:find("#S%[") - if static_index ~= nil then - use_static = true - line = line:sub(1, static_index - 1) - new_lines = 0 - end - static_index = line:find("#S]") - if static_index ~= nil then - use_static = false - line = line:sub(1, static_index - 1) - new_lines = 0 - end - - local newcount_index = line:find("#L:") - if newcount_index ~= nil then - local iS, iE = line:find("%d+", newcount_index + 3) - local newLines = tonumber(line:sub(iS, iE)) - if use_alternate then - alternate_display_lines = newLines - elseif recordRefrain then - refrain_display_lines = newLines - else - adjusted_display_lines = newLines - refrain_display_lines = newLines - alternate_display_lines = newLines - end - line = line:sub(1, newcount_index - 1) - new_lines = 0 -- ignore line - end - local static_index = line:find("#S:") - if static_index ~= nil then - line = line:sub(static_index + 3) - static_text = line - new_lines = 0 - end - local title_index = line:find("#T:") - if title_index ~= nil then - local title_indexEnd = line:find("%s+", title_index + 1) - line = line:sub(title_indexEnd + 1) - alt_title = line - new_lines = 0 - end - local alt_index = line:find("#A:") - if alt_index ~= nil then - local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) - new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) - local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) - line = line:sub(alt_indexEnd + 1) - singleAlternate = true - end - if line:find("###") ~= nil then -- Look for single line - line = line:gsub("%s*###%s*", "") - single_line = true - end - local newcount_index = line:find("#D:") - if newcount_index ~= nil then - local newcount_indexStart, newcount_indexEnd = line:find("%d+", newcount_index + 3) - new_lines = tonumber(line:sub(newcount_indexStart, newcount_indexEnd)) - _, newcount_indexEnd = line:find("%s+", newcount_indexEnd + 1) - line = line:sub(newcount_indexEnd + 1) - end - local refrain_index = line:find("#R%[") - if refrain_index ~= nil then - if next(refrain) ~= nil then - for i, _ in ipairs(refrain) do - refrain[i] = nil - end - end - recordRefrain = true - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#r%[") - if refrain_index ~= nil then - if next(refrain) ~= nil then - for i, _ in ipairs(refrain) do - refrain[i] = nil - end - end - recordRefrain = true - showText = false - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#R]") - if refrain_index ~= nil then - recordRefrain = false - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - refrain_index = line:find("#r]") - if refrain_index ~= nil then - recordRefrain = false - showText = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - end - - refrain_index = line:find("##R") - if refrain_index == nil then - refrain_index = line:find("##r") - end - if refrain_index ~= nil then - playRefrain = true - line = line:sub(1, refrain_index - 1) - new_lines = 0 - else - playRefrain = false - end - newcount_index = line:find("#P:") - if newcount_index ~= nil then - new_lines = tonumber(line:sub(newcount_index + 3)) - line = line:sub(1, newcount_index - 1) - end - newcount_index = line:find("#B:") - if newcount_index ~= nil then - new_lines = tonumber(line:sub(newcount_index + 3)) - line = line:sub(1, newcount_index - 1) - end - local phantom_index = line:find("##P") - if phantom_index ~= nil then - line = line:sub(1, phantom_index - 1) - end - phantom_index = line:find("##B") - if phantom_index ~= nil then - line = line:gsub("%s*##B%s*", "") .. "\n" - end - local verse_index = line:find("##V") - if verse_index ~= nil then - line = line:sub(1, verse_index - 1) - new_lines = 0 - verses[#verses + 1] = #lyrics - dbg_inner("Verse: " .. #lyrics) - end - if line ~= nil then - if use_static then - if static_text == "" then - static_text = line - else - static_text = static_text .. "\n" .. line - end - else - if use_alternate or singleAlternate then - if recordRefrain then - displaySize = refrain_display_lines - else - displaySize = alternate_display_lines - end - if new_lines > 0 then - while (new_lines > 0) do - if recordRefrain then - if (cur_line == 1) then - arefrain[#refrain + 1] = line - else - arefrain[#refrain] = arefrain[#refrain] .. "\n" .. line - end - end - if showText and line ~= nil then - if (cur_aline == 1) then - alternate[#alternate + 1] = line - else - alternate[#alternate] = alternate[#alternate] .. "\n" .. line - end - end - cur_aline = cur_aline + 1 - if single_line or singleAlternate or cur_aline > displaySize then - if ensure_lines then - for i = cur_aline, displaySize, 1 do - cur_aline = i - if showText and alternate[#alternate] ~= nil then - alternate[#alternate] = alternate[#alternate] .. "\n" - end - if recordRefrain then - arefrain[#refrain] = arefrain[#refrain] .. "\n" - end - end - end - cur_aline = 1 - end - new_lines = new_lines - 1 - end - end - if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record - for _, refrain_line in ipairs(arefrain) do - alternate[#alternate + 1] = refrain_line - end - end - singleAlternate = false - else - if recordRefrain then - displaySize = refrain_display_lines - else - displaySize = adjusted_display_lines - end - if new_lines > 0 then - while (new_lines > 0) do - if recordRefrain then - if (cur_line == 1) then - refrain[#refrain + 1] = line - else - refrain[#refrain] = refrain[#refrain] .. "\n" .. line - end - end - if showText and line ~= nil then - if (cur_line == 1) then - lyrics[#lyrics + 1] = line - else - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line - end - end - cur_line = cur_line + 1 - if single_line or cur_line > displaySize then - if ensure_lines then - for i = cur_line, displaySize, 1 do - cur_line = i - if showText and lyrics[#lyrics] ~= nil then - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" - end - if recordRefrain then - refrain[#refrain] = refrain[#refrain] .. "\n" - end - end - end - cur_line = 1 - end - new_lines = new_lines - 1 - end - end - end - if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record - for _, refrain_line in ipairs(refrain) do - lyrics[#lyrics + 1] = refrain_line - end - end - end - end - end - end - if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then - for i = cur_line, displaySize, 1 do - cur_line = i - if use_alternate then - if showText and alternate[#alternate] ~= nil then - alternate[#alternate] = alternate[#alternate] .. "\n" - end - else - if showText and lyrics[#lyrics] ~= nil then - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" - end - end - if recordRefrain then - refrain[#refrain] = refrain[#refrain] .. "\n" + end + for i = 1, count do + local song = obs.obs_property_list_item_string(prop_prep_list, i) + local array_obj = obs.obs_data_create() + obs.obs_data_set_string(array_obj, "value", song) + obs.obs_data_array_push_back(songNames, array_obj) + obs.obs_data_release(array_obj) + end + obs.obs_data_set_array(script_sets, "prep_list", songNames) + obs.obs_data_array_release(songNames) + obs.obs_property_set_visible(pp, true) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Cancel Prepared Edits") + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Save changes from the Editable List back to the Prepared List +----------------------------------------------------------------------------------------------------------------------- +function save_edits_clicked(props, p) + load_source_song_directory(false) + prepared_songs = {} + local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") + obs.obs_property_list_clear(prop_prep_list) + obs.obs_property_list_add_string(prop_prep_list, "*** LIST OF PREPARED SONGS ***", "") + local songNames = obs.obs_data_get_array(script_sets, "prep_list") + local count2 = obs.obs_data_array_count(songNames) + if count2 > 0 then + for i = 0, count2 - 1 do + local item = obs.obs_data_array_item(songNames, i) + local itemName = obs.obs_data_get_string(item, "value") + if get_index_in_list(song_directory, itemName) ~= nil then + prepared_songs[#prepared_songs + 1] = itemName + obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) end + obs.obs_data_release(item) end end - lyrics[#lyrics + 1] = "" - -- pause_timer = false + obs.obs_data_array_release(songNames) + save_prepared() + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") + prepared_index = 0 + pp = obs.obs_properties_get(script_props, "edit_grp") + obs.obs_property_set_visible(pp, false) + local mpb = obs.obs_properties_get(props, "prop_manage_button") + obs.obs_property_set_description(mpb, "Edit Prepared Songs List") + + if #prepared_songs > 0 then + obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") + else + obs.obs_property_set_description(prop_prep_list, "Prepared") + end + obs.obs_properties_apply_settings(props, script_sets) + return true +end +----------------------------------------------------------------------------------------------------------------------- +-- Change transition options +----------------------------------------------------------------------------------------------------------------------- +function change_transition_property(props, prop, settings) + transition_enabled = obs.obs_data_get_bool(settings, "transition_enabled") + local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") + local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") + obs.obs_property_set_enabled(text_fade_set_prop, not transition_enabled) + obs.obs_property_set_enabled(fade_speed_prop, not transition_enabled) return true end --- finds the index of a song in the directory --- if item is not in list, then return nil -function get_index_in_list(list, q_item) - for index, item in ipairs(list) do - if item == q_item then - return index - end - end - return nil + +----------------------------------------------------------------------------------------------------------------------- +-- Reloads prepared files if storage option changes between settings and external file +----------------------------------------------------------------------------------------------------------------------- +function reLoadPrepared(props, prop, settings) +dbg_method("reLoad Prepared") + local newSaveExternal = obs.obs_data_get_bool(settings, "saveExternal") + if saveExternal ~= newSaveExternal then + save_prepared(settings) + saveExternal = obs.obs_data_get_bool(settings, "saveExternal") + load_prepared(settings) + end + return true end + -------- ---------------- ------------------------- FILE FUNCTIONS +------------------------ SCRIPT WORKING FUNCTIONS ---------------- -------- +------------------------------------------------------------------------------------------------------------------------- +-- PREPARE SONG BY NAME +-- Prepares the song with the provided name +------------------------------------------------------------------------------------------------------------------------- +function prepare_selected(name) + dbg_method("prepare_selected") + -- try to prepare song + if prepare_song_by_name(name) then + page_index = 1 + if not using_source then + prepared_index = get_index_in_list(prepared_songs, name) + else + source_song_title = name + all_sources_fade = true + end --- delete previewed song -function delete_song(name) - if testValid(name) then - path = get_song_file_path(name, ".txt") + transition_lyric_text(using_source) else - path = get_song_file_path(enc(name), ".enc") + -- hide everything if unable to prepare song + -- TODO: clear lyrics entirely after text is hidden + set_text_visibility(TEXT_HIDDEN) end - os.remove(path) - table.remove(song_directory, get_index_in_list(song_directory, name)) - source_filter = false - load_source_song_directory(false) -end --- loads the song directory -function load_source_song_directory(use_filter) - dbg_method("load_source_song_directory") - local keytext = meta_tags - if source_filter then - keytext = source_meta_tags - end - dbg_inner(keytext) - local keys = ParseCSVLine(keytext) + --update_source_text() + return true +end - song_directory = {} - local filenames = {} - local tags = {} - local dir = obs.os_opendir(get_songs_folder_path()) - -- get_songs_folder_path()) - local entry - local songExt - local songTitle - local goodEntry = true +------------------------------------------------------------------------------------------------------------------------- +-- SET SOURCE OPACITY +-- Working function to set source opacities in Settings +------------------------------------------------------------------------------------------------------------------------- +function setSourceOpacity(sourceName, fadeBackground) + dbg_method("set_Opacity") + if sourceName ~= nil and sourceName ~= "" then + if text_fade_enabled then + local settings = obs.obs_data_create() + if use100percent then -- try to honor preset maximum opacities + obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity + if fadeBackground then + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity + end + else + adj_text_opacity = text_opacity /100 + obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero + obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero + obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity + if fadeBackground then + obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity + end + end + local source = obs.obs_get_source_by_name(sourceName) + if source ~= nil then + obs.obs_source_update(source, settings) + end + obs.obs_source_release(source) + obs.obs_data_release(settings) + else + dbg_inner("use on/off") + -- do preview scene item + local sceneSource = obs.obs_frontend_get_current_preview_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source_recursive(sceneObj, sourceName) + obs.obs_source_release(sceneSource) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end + end + update_monitor() + end +end - repeat - entry = obs.os_readdir(dir) - if - entry and not entry.directory and - (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") - then - songExt = obs.os_get_path_extension(entry.d_name) - songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) - tags = readTags(songTitle) - goodEntry = true - if use_filter and #keys > 0 then -- need to check files - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - goodEntry = false -- start assuming file will not be shown - if #tags == 0 then -- check no tagged option - for k = 1, #keys do - if keys[k] == "*" then - goodEntry = true -- okay to show untagged files - break - end - end - else -- have keys and tags so compare them - for k = 1, #keys do - for t = 1, #tags do - if tags[t] == keys[k] then - goodEntry = true -- found match so show file - break - end - end - if goodEntry then -- stop outer key loop on match - break +------------------------------------------------------------------------------------------------------------------------- +-- APPLY SOURCE OPACITY +-- Uses SET SOURCE OPACITY to manage source opacities according to current options +------------------------------------------------------------------------------------------------------------------------- +function apply_source_opacity() +dbg_method("Apply Opacity") + setSourceOpacity(source_name, fade_text_back) + setSourceOpacity(alternate_source_name, fade_alternate_back) + if all_sources_fade then + setSourceOpacity(title_source_name, fade_title_back) + setSourceOpacity(static_source_name, fade_static_back) + end + if link_extras or all_sources_fade then + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count - 1 do + local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local extra_source = obs.obs_get_source_by_name(sourceName) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + setSourceOpacity(sourceName, fade_extra_back) + else -- check for filter named "Color Correction" + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") + if color_filter ~= nil and text_fade_enabled then -- update filters opacity + local filter_settings = obs.obs_data_create() + if use100percent then + obs.obs_data_set_double(filter_settings, "opacity", text_opacity/100) + else + obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) + end + obs.obs_source_update(color_filter, filter_settings) + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + else -- try to just change visibility in the scene + local sceneSource = obs.obs_frontend_get_current_preview_scene() + local sceneObj = obs.obs_scene_from_source(sceneSource) + local sceneItem = obs.obs_scene_find_source_recursive(sceneObj, sourceName) + obs.obs_source_release(sceneSource) + if text_opacity > 50 then + obs.obs_sceneitem_set_visible(sceneItem, true) + else + obs.obs_sceneitem_set_visible(sceneItem, false) + end end end end - end - if goodEntry then -- add file if valid match - if songExt == ".enc" then - song_directory[#song_directory + 1] = dec(songTitle) - else - song_directory[#song_directory + 1] = songTitle - end + obs.obs_source_release(extra_source) -- release source ptr end end - until not entry - obs.os_closedir(dir) + end end --- --- reads the first line of each lyric file, looks for the //meta comment and returns any CSV tags that exist --- -function readTags(name) - local meta = "" - local path = {} - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") - end - local file = io.open(path, "r") - if file ~= nil then - for line in file:lines() do - meta = line - break - end - file:close() - end - local meta_index = meta:find("//meta ") -- Look for meta block Set - if meta_index ~= nil then - meta = meta:sub(meta_index + 7) - return ParseCSVLine(meta) - end - return {} +------------------------------------------------------------------------------------------------------------------------- +-- GET SOURCE OPACITY (working function) +-- This function reads the current opacity levels in settings. +------------------------------------------------------------------------------------------------------------------------- +function getSourceOpacity(sourceName) + if sourceName ~= nil and sourceName ~= "" then + local source = obs.obs_get_source_by_name(sourceName) + local settings = obs.obs_source_get_settings(source) + max_opacity[sourceName]={} + max_opacity[sourceName]["opacity"] = obs.obs_data_get_int(settings, "opacity") -- text opacity + max_opacity[sourceName]["outline"] = obs.obs_data_get_int(settings, "outline_opacity") -- outline opacity + max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity + max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity + obs.obs_source_release(source) + obs.obs_data_release(settings) + end end -function ParseCSVLine(line) - local res = {} - local pos = 1 - sep = "," - while true do - local c = string.sub(line, pos, pos) - if (c == "") then - break - end - if (c == '"') then - local txt = "" - repeat - local startp, endp = string.find(line, '^%b""', pos) - txt = txt .. string.sub(line, startp + 1, endp - 1) - pos = endp + 1 - c = string.sub(line, pos, pos) - if (c == '"') then - txt = txt .. '"' - end - until (c ~= '"') - txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. txt) - table.insert(res, txt) - assert(c == sep or c == "") - pos = pos + 1 - else - local startp, endp = string.find(line, sep, pos) - if (startp) then - local t = string.sub(line, pos, startp - 1) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res, t) - pos = endp + 1 - else - local t = string.sub(line, pos) - t = string.gsub(t, "^%s*(.-)%s*$", "%1") - dbg_inner("CSV: " .. t) - table.insert(res, t) - break - end +------------------------------------------------------------------------------------------------------------------------- +-- READ SOURCE OPACITY +-- Lyrics tries to honor maximum opacities that might have been set for effects. If 0-100% is disabled then Lyrics +-- Will try to mark current maximum opacity levels of sources using getSourceOpacity and use that number +-- as the sources maximum opacity when fading sources out and back. +------------------------------------------------------------------------------------------------------------------------- +function read_source_opacity() + dbg_method("read_source_opacity") + getSourceOpacity(source_name) + getSourceOpacity(alternate_source_name) + getSourceOpacity(title_source_name) + getSourceOpacity(static_source_name) + local extra_linked_list = obs.obs_properties_get(script_props, "extra_linked_list") + local count = obs.obs_property_list_item_count(extra_linked_list) + if count > 0 then + for i = 0, count - 1 do + local sourceName = obs.obs_property_list_item_string(extra_linked_list, i) -- get extra source by name + local extra_source = obs.obs_get_source_by_name(sourceName) + if extra_source ~= nil then + source_id = obs.obs_source_get_unversioned_id(extra_source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object + getSourceOpacity(sourceName) + else -- check for filter named "Color Correction" + + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") + if color_filter ~= nil then -- update filters opacity + local filter_settings = obs.obs_source_get_settings(color_filter) + max_opacity[sourceName]={} + max_opacity[sourceName]["CC-opacity"] = obs.obs_data_get_double(filter_settings, "opacity") + obs.obs_data_release(filter_settings) + obs.obs_source_release(color_filter) + end + end + end + obs.obs_source_release(extra_source) -- release source ptr + end + end +end + +------------------------------------------------------------------------------------------------------------------------- +-- SET TEXT VISIBILITY +-- Manages visibility of the text sources from Hidden, through fading in or out, to Visible. +------------------------------------------------------------------------------------------------------------------------- +function set_text_visibility(end_status) + dbg_method("set_text_visibility") + -- if already at desired visibility, then exit + if text_status == end_status then + return + end + if end_status == TEXT_HIDE then + text_opacity = 0 + text_status = end_status + apply_source_opacity() + return + elseif end_status == TEXT_SHOW then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden + apply_source_opacity() + return + end + if text_fade_enabled then + -- if fade enabled, begin fade in or out + if end_status == TEXT_HIDDEN then + text_status = TEXT_HIDING + elseif end_status == TEXT_VISIBLE then + text_status = TEXT_SHOWING + end + --all_sources_fade = true + start_fade_timer() + else -- change visibility immediately (fade or no fade) + if end_status == TEXT_HIDDEN then + text_opacity = 0 + text_status = end_status + elseif end_status == TEXT_VISIBLE then + text_opacity = 100 + text_status = end_status + all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden end + apply_source_opacity() + --update_source_text() + all_sources_fade = false + return end - return res end -local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet +------------------------------------------------------------------------------------------------------------------------- +-- TRANSITION LYRIC TEXT +-- This function hides current lyric (with or without fading), changes to next page of lyrics, and then re-displays +-- the new page (with or without fading). +------------------------------------------------------------------------------------------------------------------------- +function transition_lyric_text(force_show) + dbg_method("transition_lyric_text") + dbg_bool("force show", force_show) + -- update the lyrics display immediately on 2 conditions + -- a) the text is hidden or hiding, and we will not force it to show + -- b) text fade is not enabled + -- otherwise, start text transition out and update the lyrics once + -- fade out transition is complete + if (text_status == TEXT_HIDDEN or text_status == TEXT_HIDING) and not force_show then + update_source_text() + -- if text is done hiding, we can cancel the all_sources_fade + if text_status == TEXT_HIDDEN then + all_sources_fade = false + end + dbg_inner("hidden") + elseif not text_fade_enabled then + dbg_custom("Instant On") + -- if text fade is not enabled, then we can cancel the all_sources_fade + all_sources_fade = false + set_text_visibility(TEXT_VISIBLE) -- does update_source_text() + update_source_text() + dbg_inner("no text fade") + else -- initiate fade out/in + dbg_custom("Transition Timer") + text_status = TEXT_TRANSITION_OUT + start_fade_timer() + end + dbg_bool("using_source", using_source) +end --- encode title/filename if it contains invalid filename characters --- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup --- -function enc(data) - return ((data:gsub( - ".", - function(x) - local r, b = "", x:byte() - for i = 8, 1, -1 do - r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0") +------------------------------------------------------------------------------------------------------------------------- +-- UPDATE SOURCE Text +-- Changes the "text" value of the sources within settings +------------------------------------------------------------------------------------------------------------------------- +function update_source_text() + dbg_method("update_source_text") + dbg_custom("Page Index: " .. page_index) + local text = "" + local alttext = "" + local next_lyric = "" + local next_alternate = "" + local static = static_text + local mstatic = static -- save static for use with monitor + local title = "" + if alt_title ~= "" then + title = alt_title + else + if not using_source then + if prepared_index ~= nil and prepared_index ~= 0 then + dbg_custom("Update from prepared: " .. prepared_index) + title = prepared_songs[prepared_index] end - return r + else + dbg_custom("Updatefrom source: " .. source_song_title) + title = source_song_title end - ) .. "0000"):gsub( - "%d%d%d?%d?%d?%d?", - function(x) - if (#x < 6) then - return "" - end - local c = 0 - for i = 1, 6 do - c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0) + end + + local source = obs.obs_get_source_by_name(source_name) + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + local stat_source = obs.obs_get_source_by_name(static_source_name) + local title_source = obs.obs_get_source_by_name(title_source_name) + + if using_source or (prepared_index ~= nil and prepared_index ~= 0) then + if #lyrics > 0 then + if lyrics[page_index] ~= nil then + text = lyrics[page_index] end - return b:sub(c + 1, c + 1) end - ) .. ({"", "==", "="})[#data % 3 + 1]) -end --- --- decode an encoded title/filename --- -function dec(data) - data = string.gsub(data, "[^" .. b .. "=]", "") - return (data:gsub( - ".", - function(x) - if (x == "=") then - return "" - end - local r, f = "", (b:find(x) - 1) - for i = 6, 1, -1 do - r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and "1" or "0") + if #alternate > 0 then + if alternate[page_index] ~= nil then + alttext = alternate[page_index] end - return r end - ):gsub( - "%d%d%d?%d?%d?%d?%d?%d?", - function(x) - if (#x ~= 8) then - return "" - end - local c = 0 - for i = 1, 8 do - c = c + (x:sub(i, i) == "1" and 2 ^ (8 - i) or 0) + + if link_text then + if string.len(text) == 0 and string.len(alttext) == 0 then + --static = "" + --title = "" end - return string.char(c) end - )) -end - -function testValid(filename) - if string.find(filename, "[\128-\255]") ~= nil then - return false - end - if string.find(filename, '[\\\\/:*?"<>|]') ~= nil then - return false - end - return true -end - --- saves previewed song, return true if new song -function save_song(name, text) - local path = {} - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") end - local file = io.open(path, "w") - if file ~= nil then - for line in text:gmatch("([^\n]+)") do - local trimmed = line:match("%s*(%S-.*%S+)%s*") - if trimmed ~= nil then - file:write(trimmed, "\n") - end + -- update source texts + if source ~= nil then + dbg_inner("Title Load") + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", text) + obs.obs_source_update(source, settings) + obs.obs_data_release(settings) + next_lyric = lyrics[page_index + 1] + if (next_lyric == nil) then + next_lyric = "" end - file:close() - if get_index_in_list(song_directory, name) == nil then - song_directory[#song_directory + 1] = name - return true + end + if alt_source ~= nil then + local settings = obs.obs_data_create() -- setup TEXT settings with opacity values + obs.obs_data_set_string(settings, "text", alttext) + obs.obs_source_update(alt_source, settings) + obs.obs_data_release(settings) + next_alternate = alternate[page_index + 1] + if (next_alternate == nil) then + next_alternate = "" end end - return false -end - --- saves preprepared songs -function save_prepared() - dbg_method("save_prepared") - local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") - for i, name in ipairs(prepared_songs) do - -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs - file:write(name, "\n") - -- end + if stat_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", static) + obs.obs_source_update(stat_source, settings) + obs.obs_data_release(settings) end - file:close() - return true -end + if title_source ~= nil then + local settings = obs.obs_data_create() + obs.obs_data_set_string(settings, "text", title) + obs.obs_source_update(title_source, settings) + obs.obs_data_release(settings) + end + -- release source references + obs.obs_source_release(source) + obs.obs_source_release(alt_source) + obs.obs_source_release(stat_source) + obs.obs_source_release(title_source) -function update_monitor() - dbg_method("update_monitor") - local tableback = "black" - local text = "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = text .. "" - text = - text .. - "
" - text = - text .. - "
" - if using_preview then - text = text .. "From Preview
" - elseif using_source then - text = text .. "From Source: " .. load_scene .. "
" + local next_prepared = "" + if using_source then + next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song + elseif prepared_index < #prepared_songs then + next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song else - text = text .. "Prepared Song: " .. prepared_index - text = - text .. - " of " .. #prepared_songs .. "
" - end - text = - text .. - "
Lyric Page: " - local pages = (#lyrics == 0) and 0 or #lyrics-1 - if page_index < #lyrics then - text = text .. page_index.. " of " .. pages .. "
" - else - text = text .. "Blank
" - end - - if #verses ~= nil and mon_verse > 0 then - text = - text .. - "
Verse: " .. mon_verse - text = text .. " of " .. #verses .. "
" - end - text = text .. "
" - if not anythingActive() then - tableback = "#440000" - end - local visbgTitle = tableback - local visbgText = tableback - if text_status == TEXT_HIDDEN or text_status == TEXT_HIDING then - visbgText = "maroon" - if link_text then - visbgTitle = "maroon" + if source_active then + next_prepared = source_song_title -- plan to go back to source loaded song + else + next_prepared = prepared_songs[1] -- plan to loop around to first prepared song end end - - text = - text .. - "
" - if mon_song ~= "" and mon_song ~= nil then - text = - text .. - "" - text = - text .. - "" - end - if mon_lyric ~= "" and mon_lyric ~= nil then - text = - text .. - "" - text = - text .. "" - end - if mon_nextlyric ~= "" and mon_nextlyric ~= nil then - text = - text .. - "" - text = text .. "" - end - if mon_alt ~= "" and mon_alt ~= nil then - text = - text .. - "" - text = - text .. - "" - end - if mon_nextalt ~= "" and mon_nextalt ~= nil then - text = - text .. - "" - text = text .. "" - end - if mon_nextsong ~= "" and mon_nextsong ~= nil then - text = - text .. - "" - text = text .. "" + mon_verse = 0 + if #verses ~= nil then --find valid page Index + for i = 1, #verses do + if page_index >= verses[i] + 1 then + mon_verse = i + end + end -- v = current verse number for this page end - text = text .. "
Song
Title
" .. mon_song .. "
Current
Page
• " .. mon_lyric .. "
Next
Page
• " .. mon_nextlyric .. "
Alt
Lyric
• " .. mon_alt .. "
Next
Alt
• " .. mon_nextalt .. "
Next
Song:
" .. mon_nextsong .. "
" - local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") - dbg_inner("write monitor file") - file:write(text) - file:close() - return true -end + mon_song = title + mon_lyric = text:gsub("\n", "
• ") + mon_nextlyric = next_lyric:gsub("\n", "
• ") + mon_alt = alttext:gsub("\n", "
• ") + mon_nextalt = next_alternate:gsub("\n", "
• ") + mon_nextsong = next_prepared --- returns path of the given song name -function get_song_file_path(name, suffix) - if name == nil then - return nil - end - return get_songs_folder_path() .. "\\" .. name .. suffix + update_monitor() end --- returns path of the lyrics songs folder -function get_songs_folder_path() - local sep = package.config:sub(1, 1) - local path = "" - if windows_os then - path = os.getenv("USERPROFILE") - else - path = os.getenv("HOME") - end - return path .. sep .. ".config" .. sep .. ".obs_lyrics" +------------------------------------------------------------------------------------------------------------------------- +-- START FADE TIMER +-- Starts a 50ms callback to "fade_callback" function to allow text opacities to gradually change up or down +-- causing the fade-in, fade-out effect. +------------------------------------------------------------------------------------------------------------------------- +function start_fade_timer() + dbgsp("started fade timer") + obs.timer_add(fade_callback, 50) end --- gets the text of a song -function get_song_text(name) - local song_lines = {} - local path = {} - if testValid(name) then - path = get_song_file_path(name, ".txt") - else - path = get_song_file_path(enc(name), ".enc") +------------------------------------------------------------------------------------------------------------------------- +-- FADE CALLBACK +-- Gradually increments or decrements target source object opacities towards the target visibility state, stopping +-- the callback timer when the target visibility is reached. +------------------------------------------------------------------------------------------------------------------------- +function fade_callback() + -- if not in a transitory state, exit callback + if text_status == TEXT_HIDDEN or text_status == TEXT_VISIBLE then + obs.remove_current_callback() + all_sources_fade = false end - local file = io.open(path, "r") - if file ~= nil then - for line in file:lines() do - song_lines[#song_lines + 1] = line + -- the amount we want to change opacity by + local opacity_delta = 1 + text_fade_speed + -- change opacity in the direction of transitory state + if text_status == TEXT_HIDING or text_status == TEXT_TRANSITION_OUT then + local new_opacity = text_opacity - opacity_delta + if new_opacity > 0 then + text_opacity = new_opacity + else + -- completed fade out, determine next move + text_opacity = 0 + if text_status == TEXT_TRANSITION_OUT then + -- update to new lyric between fades + update_source_text() + -- begin transition back in + text_status = TEXT_TRANSITION_IN + else + text_status = TEXT_HIDDEN + end + end + elseif text_status == TEXT_SHOWING or text_status == TEXT_TRANSITION_IN then + local new_opacity = text_opacity + opacity_delta + if new_opacity < 100 then + text_opacity = new_opacity + else + -- completed fade in + text_opacity = 100 + text_status = TEXT_VISIBLE end - file:close() - else - return nil end - - return song_lines + -- apply the new opacity + apply_source_opacity() end --- ------ ----------------- ------------------------- OBS DEFAULT FUNCTIONS --- -------------- --------- +------------------------------------------------------------------------------------------------------------------------- +-- PREPARE SONG BY NUMBER +-- Prepares a song by its position in the prepared lyrics list +------------------------------------------------------------------------------------------------------------------------- +function prepare_song_by_index(index) + dbg_method("prepare_song_by_index") + if index <= #prepared_songs then + prepare_song_by_name(prepared_songs[index]) + end +end --- A function named script_properties defines the properties that the user --- can change for the entire script module itself +--------------------------------------------------------------------------------------------------------------------- +-- Function to parse and process markups within the lyrics and break the text into defined pages and verses +-- The first line of song/text files can contain an optional list of meta tags that organize the files into +-- user defined genre or categories for later filtering during selection -local help = - "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. - " Markup      Syntax         Markup      Syntax \n" .. - "============ ==========   ============ ==========\n" .. - " Display n Lines    #L:n      End Page after Line   Line ###\n" .. - " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. - " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. - " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. - " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. - "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. - "Comment Line    // Line       Block Comments    //[ and //] \n" .. - "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line" +-- Currently supported markups all start with # or // -function script_properties() - dbg_method("script_properties") - editVisSet = false - script_props = obs.obs_properties_create() - obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) - ----------- - obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲", change_info_visible) - local gp = obs.obs_properties_create() - obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) - obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) - obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) - obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) - obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) - obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) - obs.obs_properties_add_group( - script_props, - "info_grp", - "Song Title (filename) and Lyrics Information", - obs.OBS_GROUP_NORMAL, - gp - ) - ------------ - obs.obs_properties_add_button( - script_props, - "prepared_showing", - "▲- HIDE PREPARED SONGS -▲", - change_prepared_visible - ) - gp = obs.obs_properties_create() - local prop_dir_list = - obs.obs_properties_add_list( - gp, - "prop_directory_list", - "Song Directory", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - table.sort(song_directory) - for _, name in ipairs(song_directory) do - obs.obs_property_list_add_string(prop_dir_list, name, name) +-- Display n Lines #L:n +-- End Page after Line Line ### +-- Blank (Pad) Line ##B or ##P +-- Blank(Pad) Lines #B:n or #P:n +-- External Refrain #r[ and #r] +-- In-Line Refrain #R[ and #R] +-- Repeat Refrain ##r or ##R +-- Duplicate Line n times #D:n Line\n" +-- Static Lines #S[ and #s] +-- Single Static Line #S: Line +-- Alternate Text #A[ and #A] +-- Alt Line Repeat n Pages #A:n Line +-- Comment Line // Line +-- Block Comments //[ and //] +-- Mark Verses ##V +-- Override Title #T: text +-- Optional comma delimited meta tags follow '//meta ' on 1st line" +--------------------------------------------------------------------------------------------------------------------- +function prepare_song_by_name(name) + dbg_method("prepare_song_by_name") + if name == nil then + return false + end + last_prepared_song = name + -- if using transition on lyric change, first transition + -- would be reset with new song prepared + transition_completed = false + -- load song lines + local song_lines = get_song_text(name) + if song_lines == nil then + return false + end + local cur_line = 1 + local cur_aline = 1 + local recordRefrain = false + local playRefrain = false + local use_alternate = false + local use_static = false + local showText = true + local commentBlock = false + local singleAlternate = false + local refrain = {} + local arefrain = {} + lyrics = {} + verses = {} + alternate = {} + static_text = "" + alt_title = "" + local adjusted_display_lines = display_lines + local refrain_display_lines = display_lines + local alternate_display_lines = display_lines + local displaySize = display_lines + for _, line in ipairs(song_lines) do + local new_lines = 1 + local single_line = false + local comment_index = line:find("//%[") -- Look for comment block Set + if comment_index ~= nil then + commentBlock = true + line = line:sub(comment_index + 3) + end + comment_index = line:find("//]") -- Look for comment block Clear + if comment_index ~= nil then + commentBlock = false + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + if not commentBlock then + local comment_index = line:find("%s*//") + if comment_index ~= nil then + line = line:sub(1, comment_index - 1) + new_lines = 0 + end + local alternate_index = line:find("#A%[") + if alternate_index ~= nil then + use_alternate = true + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + alternate_index = line:find("#A]") + if alternate_index ~= nil then + use_alternate = false + line = line:sub(1, alternate_index - 1) + new_lines = 0 + end + local static_index = line:find("#S%[") + if static_index ~= nil then + use_static = true + line = line:sub(1, static_index - 1) + new_lines = 0 + end + static_index = line:find("#S]") + if static_index ~= nil then + use_static = false + line = line:sub(1, static_index - 1) + new_lines = 0 + end + + local newcount_index = line:find("#L:") + if newcount_index ~= nil then + local iS, iE = line:find("%d+", newcount_index + 3) + local newLines = tonumber(line:sub(iS, iE)) + if use_alternate then + alternate_display_lines = newLines + elseif recordRefrain then + refrain_display_lines = newLines + else + adjusted_display_lines = newLines + refrain_display_lines = newLines + alternate_display_lines = newLines + end + line = line:sub(1, newcount_index - 1) + new_lines = 0 -- ignore line + end + local static_index = line:find("#S:") + if static_index ~= nil then + line = line:sub(static_index + 3) + static_text = line + new_lines = 0 + end + local title_index = line:find("#T:") + if title_index ~= nil then + local title_indexEnd = line:find("%s+", title_index + 1) + line = line:sub(title_indexEnd + 1) + alt_title = line + new_lines = 0 + end + local alt_index = line:find("#A:") + if alt_index ~= nil then + local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) + new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) + local alt_indexEnd = line:find("%s+", alt_indexEnd + 1) + line = line:sub(alt_indexEnd + 1) + singleAlternate = true + end + if line:find("###") ~= nil then -- Look for single line + line = line:gsub("%s*###%s*", "") + single_line = true + end + local newcount_index = line:find("#D:") + if newcount_index ~= nil then + local newcount_indexStart, newcount_indexEnd = line:find("%d+", newcount_index + 3) + new_lines = tonumber(line:sub(newcount_indexStart, newcount_indexEnd)) + _, newcount_indexEnd = line:find("%s+", newcount_indexEnd + 1) + line = line:sub(newcount_indexEnd + 1) + end + local refrain_index = line:find("#R%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r%[") + if refrain_index ~= nil then + if next(refrain) ~= nil then + for i, _ in ipairs(refrain) do + refrain[i] = nil + end + end + recordRefrain = true + showText = false + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#R]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + refrain_index = line:find("#r]") + if refrain_index ~= nil then + recordRefrain = false + showText = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + end + + refrain_index = line:find("##R") + if refrain_index == nil then + refrain_index = line:find("##r") + end + if refrain_index ~= nil then + playRefrain = true + line = line:sub(1, refrain_index - 1) + new_lines = 0 + else + playRefrain = false + end + newcount_index = line:find("#P:") + if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) + line = line:sub(1, newcount_index - 1) + end + newcount_index = line:find("#B:") + if newcount_index ~= nil then + new_lines = tonumber(line:sub(newcount_index + 3)) + line = line:sub(1, newcount_index - 1) + end + local phantom_index = line:find("##P") + if phantom_index ~= nil then + line = line:sub(1, phantom_index - 1) + end + phantom_index = line:find("##B") + if phantom_index ~= nil then + line = line:gsub("%s*##B%s*", "") .. "\n" + end + local verse_index = line:find("##V") + if verse_index ~= nil then + line = line:sub(1, verse_index - 1) + new_lines = 0 + verses[#verses + 1] = #lyrics + dbg_inner("Verse: " .. #lyrics) + end + if line ~= nil then + if use_static then + if static_text == "" then + static_text = line + else + static_text = static_text .. "\n" .. line + end + else + if use_alternate or singleAlternate then + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = alternate_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + arefrain[#refrain + 1] = line + else + arefrain[#refrain] = arefrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_aline == 1) then + alternate[#alternate + 1] = line + else + alternate[#alternate] = alternate[#alternate] .. "\n" .. line + end + end + cur_aline = cur_aline + 1 + if single_line or singleAlternate or cur_aline > displaySize then + if ensure_lines then + for i = cur_aline, displaySize, 1 do + cur_aline = i + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + if recordRefrain then + arefrain[#refrain] = arefrain[#refrain] .. "\n" + end + end + end + cur_aline = 1 + end + new_lines = new_lines - 1 + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(arefrain) do + alternate[#alternate + 1] = refrain_line + end + end + singleAlternate = false + else + if recordRefrain then + displaySize = refrain_display_lines + else + displaySize = adjusted_display_lines + end + if new_lines > 0 then + while (new_lines > 0) do + if recordRefrain then + if (cur_line == 1) then + refrain[#refrain + 1] = line + else + refrain[#refrain] = refrain[#refrain] .. "\n" .. line + end + end + if showText and line ~= nil then + if (cur_line == 1) then + lyrics[#lyrics + 1] = line + else + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line + end + end + cur_line = cur_line + 1 + if single_line or cur_line > displaySize then + if ensure_lines then + for i = cur_line, displaySize, 1 do + cur_line = i + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end + end + cur_line = 1 + end + new_lines = new_lines - 1 + end + end + end + if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record + for _, refrain_line in ipairs(refrain) do + lyrics[#lyrics + 1] = refrain_line + end + end + end + end + end end - obs.obs_property_set_modified_callback(prop_dir_list, preview_selection_made) - obs.obs_properties_add_button(gp, "prop_prepare_button", "Add Song/Text to Prepared List", prepare_song_clicked) - obs.obs_properties_add_button(gp, "prop_preview_button", "Preview Songs/Text", preview_clicked) - obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) - local gps = obs.obs_properties_create() - obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) - local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) - gps = obs.obs_properties_create() - local prepare_prop = - obs.obs_properties_add_list( - gps, - "prop_prepared_list", - "Prepared ", - obs.OBS_COMBO_TYPE_EDITABLE, - obs.OBS_COMBO_FORMAT_STRING - ) - obs.obs_property_list_add_string(prepare_prop, "*** LIST OF PREPARED SONGS ***","") - for _, name in ipairs(prepared_songs) do - obs.obs_property_list_add_string(prepare_prop, name, name) + if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then + for i = cur_line, displaySize, 1 do + cur_line = i + if use_alternate then + if showText and alternate[#alternate] ~= nil then + alternate[#alternate] = alternate[#alternate] .. "\n" + end + else + if showText and lyrics[#lyrics] ~= nil then + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + end + end + if recordRefrain then + refrain[#refrain] = refrain[#refrain] .. "\n" + end + end end - obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") - obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) - local count = obs.obs_property_list_item_count(prepare_prop) - if count > 1 then - obs.obs_property_set_description( prepare_prop, "Prepared (" .. count-1 .. ")") - end - obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) - obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) - local eps = obs.obs_properties_create() - local edit_prop = - obs.obs_properties_add_editable_list( - eps, - "prep_list", - "Prepared Songs/Text", - obs.OBS_EDITABLE_LIST_TYPE_STRINGS, - nil, - nil - ) - obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes", save_edits_clicked) - local edit_group_prop = - obs.obs_properties_add_group( - gps, - "edit_grp", - "Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", - obs.OBS_GROUP_NORMAL, - eps - ) - local saveExtProp = obs.obs_properties_add_bool(eps, "saveExternal", "Use external Prepared.dat file ") - obs.obs_property_set_modified_callback(saveExtProp, reLoadPrepared) - - obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) - obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) - ------------------ - obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) - hotkey_props = obs.obs_properties_create() - local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) - obs.obs_properties_add_button(hotkey_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked) - obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons ",obs.OBS_GROUP_NORMAL,hotkey_props) - name_hotkeys() - ------ - obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) - gp = obs.obs_properties_create() - local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "Lines to Display", 1, 50, 1) - obs.obs_property_set_long_description( - lines_prop, - "Sets default lines per page of lyric, overwritten by Markup: #L:n" - ) - local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") - obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") - local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") - obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") + lyrics[#lyrics + 1] = "" + -- pause_timer = false + return true +end - local transition_prop = - obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") - obs.obs_property_set_modified_callback(transition_prop, change_transition_property) - obs.obs_property_set_long_description( - transition_prop, - "Use with Studio Mode, duplicate sources, and OBS source transitions (beta)" - ) - - local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable Fade Transitions") - obs.obs_property_set_modified_callback(fade_prop, change_fade_property) - local fp1 = obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) - local fp2 = obs.obs_properties_add_bool(gp,"use100percent", "Use 0-100% opacity for fades") - local fp3 = obs.obs_properties_add_bool(gp,"allowBackFade", "Enable Background Fading") - obs.obs_property_set_modified_callback(fp2, change_100percent_property) - obs.obs_property_set_modified_callback(fp3, change_back_fade_property) - local oprefprop = obs.obs_properties_add_button(gp, "refreshOP", "Mark Max Opacity for Source Fades", read_source_opacity_clicked) - obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) - - ------------- - obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) - gp = obs.obs_properties_create() - - local source_prop = - obs.obs_properties_add_list( - gp, - "prop_source_list", - "Text Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - local flbprop = obs.obs_properties_add_bool(gp, "fade_text_back", "Fade Text Background") - local title_source_prop = - obs.obs_properties_add_list( - gp, - "prop_title_list", - "Title Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - local ftbprop = obs.obs_properties_add_bool(gp, "fade_title_back", "Fade Title Background") - local alternate_source_prop = - obs.obs_properties_add_list( - gp, - "prop_alternate_list", - "Alternate Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - local fabprop = obs.obs_properties_add_bool(gp, "fade_alternate_back", "Fade Alternate Background") - local static_source_prop = - obs.obs_properties_add_list( - gp, - "prop_static_list", - "Static Source", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - local fsbprop = obs.obs_properties_add_bool(gp, "fade_static_back", "Fade Static Background") - obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) - - local dlprop = obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) - xgp = obs.obs_properties_create() - local extra_linked_prop = - obs.obs_properties_add_list( - xgp, - "extra_linked_list", - "Linked Sources", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - -- initialize previously loaded extra properties from table - for _, sourceName in ipairs(extra_sources) do - obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) +------------------------------------------------------------------------------------------------------------------------- +-- GET INDEX IN LIST +-- Finds a specifically named song in the prepared list +--------------------------------------------------------------------------------------------------------------------------- +function get_index_in_list(list, q_item) + for index, item in ipairs(list) do + if item == q_item then + return index + end end - local extra_source_prop = - obs.obs_properties_add_list( - xgp, - "extra_source_list", - " Select Source:", - obs.OBS_COMBO_TYPE_LIST, - obs.OBS_COMBO_FORMAT_STRING - ) - obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") - local febprop = obs.obs_properties_add_bool(xgp, "fade_extra_back", "Fade Background for Text Sources") - local clearcall_prop = - obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) - local extra_group_prop = - obs.obs_properties_add_group(gp, "xtr_grp", "Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL, xgp) - obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) - local count = obs.obs_property_list_item_count(extra_linked_prop) - if count > 0 then - do_linked_clicked(script_props,dlprop) - obs.obs_property_set_description( - extra_linked_prop, - "Linked Sources (" .. count .. ")" - ) + return nil +end + +-------- +---------------- +------------------------ FILE FUNCTIONS +---------------- +-------- + +------------------------------------------------------------------------------------------------------------------------- +-- DELETE SONG +-- Removes a Song File from the OS file system +------------------------------------------------------------------------------------------------------------------------- +function delete_song(name) + if testValid(name) then + path = get_song_file_path(name, ".txt") else - clear_linked_clicked(script_props, clearcall_prop) + path = get_song_file_path(enc(name), ".enc") + end + os.remove(path) + table.remove(song_directory, get_index_in_list(song_directory, name)) + source_filter = false + load_source_song_directory(false) +end + +------------------------------------------------------------------------------------------------------------------------- +-- LOAD SONG DIRECTORY +-- Loads the current song directory into a song_directory table +-- optionally uses a filter based on a list of desired meta keys to include +-- If using a filter then only files that have those selected meta keys on their first line will be +-- included in the directory table +------------------------------------------------------------------------------------------------------------------------- +function load_source_song_directory(use_filter) + dbg_method("load_source_song_directory") + local keytext = meta_tags + if source_filter then + keytext = source_meta_tags end + dbg_inner(keytext) + local keys = ParseCSVLine(keytext) - local sources = obs.obs_enum_sources() - obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") - if sources ~= nil then - local n = {} - for _, source in ipairs(sources) do - local name = obs.obs_source_get_name(source) - if isValid(source) then - obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list + song_directory = {} + local filenames = {} + local tags = {} + local dir = obs.os_opendir(get_songs_folder_path()) + -- get_songs_folder_path()) + local entry + local songExt + local songTitle + local goodEntry = true + + repeat + entry = obs.os_readdir(dir) + if + entry and not entry.directory and + (obs.os_get_path_extension(entry.d_name) == ".enc" or obs.os_get_path_extension(entry.d_name) == ".txt") + then + songExt = obs.os_get_path_extension(entry.d_name) + songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) + tags = readTags(songTitle) + goodEntry = true + if use_filter and #keys > 0 then -- need to check files + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + goodEntry = false -- start assuming file will not be shown + if #tags == 0 then -- check no tagged option + for k = 1, #keys do + if keys[k] == "*" then + goodEntry = true -- okay to show untagged files + break + end + end + else -- have keys and tags so compare them + for k = 1, #keys do + for t = 1, #tags do + if tags[t] == keys[k] then + goodEntry = true -- found match so show file + break + end + end + if goodEntry then -- stop outer key loop on match + break + end + end + end end - source_id = obs.obs_source_get_unversioned_id(source) - if source_id == "text_gdiplus" or source_id == "text_ft2_source" then - n[#n + 1] = name + if goodEntry then -- add file if valid match + if songExt == ".enc" then + song_directory[#song_directory + 1] = dec(songTitle) + else + song_directory[#song_directory + 1] = songTitle + end end end - table.sort(n) - obs.obs_property_list_add_string(source_prop, "", "") - obs.obs_property_list_add_string(title_source_prop, "", "") - obs.obs_property_list_add_string(alternate_source_prop, "", "") - obs.obs_property_list_add_string(static_source_prop, "", "") - for _, name in ipairs(n) do - obs.obs_property_list_add_string(source_prop, name, name) - obs.obs_property_list_add_string(title_source_prop, name, name) - obs.obs_property_list_add_string(alternate_source_prop, name, name) - obs.obs_property_list_add_string(static_source_prop, name, name) - end - end - obs.source_list_release(sources) - - ----------------- - obs.obs_property_set_enabled(hktitletext, false) - obs.obs_property_set_visible(edit_group_prop, false) - obs.obs_property_set_visible(meta_group_prop, false) - obs.obs_property_set_visible(fp1, text_fade_enabled) - obs.obs_property_set_visible(fp2, text_fade_enabled) - obs.obs_property_set_visible(fp3, text_fade_enabled) - obs.obs_property_set_visible(flbprop, text_fade_enabled and allow_back_fade) - obs.obs_property_set_visible(ftbprop, text_fade_enabled and allow_back_fade) - obs.obs_property_set_visible(fabprop, text_fade_enabled and allow_back_fade) - obs.obs_property_set_visible(fsbprop, text_fade_enabled and allow_back_fade) - obs.obs_property_set_visible(febprop, text_fade_enabled and allow_back_fade) - obs.obs_property_set_visible(oprefprop, text_fade_enabled and not use100percent) - - - read_source_opacity() - return script_props + until not entry + obs.os_closedir(dir) end --- script_update is called when settings are changed -function script_update(settings) - text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") - text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") - display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") - source_name = obs.obs_data_get_string(settings, "prop_source_list") - alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") - static_source_name = obs.obs_data_get_string(settings, "prop_static_list") - title_source_name = obs.obs_data_get_string(settings, "prop_title_list") - ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") - link_text = obs.obs_data_get_bool(settings, "do_link_text") - link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") - use100percent = obs.obs_data_get_bool(settings, "use100percent") - allow_back_fade = obs.obs_data_get_bool(settings, "allowBackFade") - fade_text_back = obs.obs_data_get_bool(settings, "fade_text_back") and allow_back_fade - fade_title_back = obs.obs_data_get_bool(settings, "fade_title_back") and allow_back_fade - fade_alternate_back = obs.obs_data_get_bool(settings, "fade_alternate_back") and allow_back_fade - fade_static_back = obs.obs_data_get_bool(settings, "fade_static_back") and allow_back_fade - fade_extra_back = obs.obs_data_get_bool(settings, "fade_extra_back") and allow_back_fade - update_monitor() -end +------------------------------------------------------------------------------------------------------------------------- +-- READ FILE TAGS +-- Reads the first line of each lyric file, looks for the //meta comment and returns any CSV tags that exist +------------------------------------------------------------------------------------------------------------------------- --- A function named script_defaults will be called to set the default settings -function script_defaults(settings) - dbg_method("script_defaults") - obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) - obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") - obs.obs_data_set_default_bool(settings,"use100percent", true) - obs.obs_data_set_default_bool(settings,"text_fade_enabled", false) - obs.obs_data_set_default_int(settings,"text_fade_speed", 5) - if os.getenv("HOME") == nil then - windows_os = true - end -- must be set prior to calling any file functions - if windows_os then - os.execute('mkdir "' .. get_songs_folder_path() .. '"') +function readTags(name) + local meta = "" + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") else - os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') + path = get_song_file_path(enc(name), ".enc") end -end - ---verify source has an opacity setting -function isValid(source) - if source ~= nil then - local flags = obs.obs_source_get_output_flags(source) - local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) - if bit.band(flags, targetFlag) == targetFlag then - return true + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + meta = line + break end + file:close() end - return false + local meta_index = meta:find("//meta ") -- Look for meta block Set + if meta_index ~= nil then + meta = meta:sub(meta_index + 7) + return ParseCSVLine(meta) + end + return {} end --- adds an extra linked source. --- Source must be text source, or have 'Color Correction' Filter applied -function link_source_selected(props, prop, settings) - dbg_method("link_source_selected") - local extra_source = obs.obs_data_get_string(settings, "extra_source_list") - if extra_source ~= nil and extra_source ~= "" then - local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") - obs.obs_property_list_add_string(extra_linked_list, extra_source, extra_source) - obs.obs_data_set_string(script_sets, "extra_linked_list", extra_source) - obs.obs_data_set_string(script_sets, "extra_source_list", "") - obs.obs_property_set_description( - extra_linked_list, - "Linked Sources (" .. obs.obs_property_list_item_count(extra_linked_list) .. ")" - ) +------------------------------------------------------------------------------------------------------------------------- +-- PARSE CSV +-- Converts a string with CSV values A, B, C into a table of values +------------------------------------------------------------------------------------------------------------------------- +function ParseCSVLine(line) + local res = {} + local pos = 1 + sep = "," + while true do + local c = string.sub(line, pos, pos) + if (c == "") then + break + end + if (c == '"') then + local txt = "" + repeat + local startp, endp = string.find(line, '^%b""', pos) + txt = txt .. string.sub(line, startp + 1, endp - 1) + pos = endp + 1 + c = string.sub(line, pos, pos) + if (c == '"') then + txt = txt .. '"' + end + until (c ~= '"') + txt = string.gsub(txt, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. txt) + table.insert(res, txt) + assert(c == sep or c == "") + pos = pos + 1 + else + local startp, endp = string.find(line, sep, pos) + if (startp) then + local t = string.sub(line, pos, startp - 1) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + pos = endp + 1 + else + local t = string.sub(line, pos) + t = string.gsub(t, "^%s*(.-)%s*$", "%1") + dbg_inner("CSV: " .. t) + table.insert(res, t) + break + end + end end - return true + return res end --- removes linked sources -function do_linked_clicked(props, p) - dbg_method("do_link_clicked") - obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), true) - obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), false) - obs.obs_properties_apply_settings(props, script_sets) +local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- encoding alphabet - return true +------------------------------------------------------------------------------------------------------------------------- +-- Encode invalid filename +-- Encodes title/filename if it contains invalid filename characters +-- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup +--------------------------------------------------------------------------------------------------------------------------- +function enc(data) + return ((data:gsub( + ".", + function(x) + local r, b = "", x:byte() + for i = 8, 1, -1 do + r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ) .. "0000"):gsub( + "%d%d%d?%d?%d?%d?", + function(x) + if (#x < 6) then + return "" + end + local c = 0 + for i = 1, 6 do + c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0) + end + return b:sub(c + 1, c + 1) + end + ) .. ({"", "==", "="})[#data % 3 + 1]) +end + +------------------------------------------------------------------------------------------------------------------------- +-- Decode invalid filename +-- Decodes title/filename if it contains invalid filename characters +-- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup +--------------------------------------------------------------------------------------------------------------------------- +function dec(data) + data = string.gsub(data, "[^" .. b .. "=]", "") + return (data:gsub( + ".", + function(x) + if (x == "=") then + return "" + end + local r, f = "", (b:find(x) - 1) + for i = 6, 1, -1 do + r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and "1" or "0") + end + return r + end + ):gsub( + "%d%d%d?%d?%d?%d?%d?%d?", + function(x) + if (#x ~= 8) then + return "" + end + local c = 0 + for i = 1, 8 do + c = c + (x:sub(i, i) == "1" and 2 ^ (8 - i) or 0) + end + return string.char(c) + end + )) end --- removes linked sources -function clear_linked_clicked(props, p) - dbg_method("clear_linked_clicked") - local extra_linked_list = obs.obs_properties_get(props, "extra_linked_list") - - obs.obs_property_list_clear(extra_linked_list) - obs.obs_property_set_visible(obs.obs_properties_get(props, "xtr_grp"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props, "do_link_button"), true) - obs.obs_property_set_description(extra_linked_list, "Linked Sources") +------------------------------------------------------------------------------------------------------------------------- +-- TEST VALID +-- Returns True if text will work as a valid filename or otherwise false +------------------------------------------------------------------------------------------------------------------------- +function testValid(filename) + if string.find(filename, "[\128-\255]") ~= nil then + return false + end + if string.find(filename, '[\\\\/:*?"<>|]') ~= nil then + return false + end return true end --- A function named script_description returns the description shown to --- the user - -function script_description() - return description -end - -function vMode(vis) - return expandcollapse and "▲- HIDE " or "▼- SHOW ", expandcollapse and "-▲" or "-▼" -end - -function expand_all_groups(props, prop, settings) - expandcollapse = not expandcollapse - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "info_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "mng_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "disp_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "src_grp"), expandcollapse) - obs.obs_property_set_visible(obs.obs_properties_get(script_props, "ctrl_grp"), expandcollapse) - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description(obs.obs_properties_get(props, "expand_all_button"), mode1 .. "ALL GROUPS" .. mode2) - obs.obs_property_set_description( - obs.obs_properties_get(props, "info_showing"), - mode1 .. "SONG INFORMATION" .. mode2 - ) - obs.obs_property_set_description( - obs.obs_properties_get(props, "prepared_showing"), - mode1 .. "PREPARED SONGS" .. mode2 - ) - obs.obs_property_set_description( - obs.obs_properties_get(props, "options_showing"), - mode1 .. "DISPLAY OPTIONS" .. mode2 - ) - obs.obs_property_set_description( - obs.obs_properties_get(props, "src_showing"), - mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 - ) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - return true -end +------------------------------------------------------------------------------------------------------------------------- +-- SAVE SONG +-- Saves current Lyric Text in Properties as a song file, return true if new song +------------------------------------------------------------------------------------------------------------------------- -function all_vis_equal(props) - if - (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "prep_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) and - obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) or - not (obs.obs_property_visible(obs.obs_properties_get(script_props, "info_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "mng_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "disp_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "src_grp")) or - obs.obs_property_visible(obs.obs_properties_get(script_props, "ctrl_grp"))) - then - expandcollapse = not expandcollapse - local mode1, mode2 = vMode(expandecollapse) - obs.obs_property_set_description( - obs.obs_properties_get(props, "expand_all_button"), - mode1 .. "ALL GROUPS" .. mode2 - ) +function save_song(name, text) + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") + else + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "w") + if file ~= nil then + for line in text:gmatch("([^\n]+)") do + local trimmed = line:match("%s*(%S-.*%S+)%s*") + if trimmed ~= nil then + file:write(trimmed, "\n") + end + end + file:close() + if get_index_in_list(song_directory, name) == nil then + song_directory[#song_directory + 1] = name + return true + end end + return false end -function change_info_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "info_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "info_showing"), - mode1 .. "SONG INFORMATION" .. mode2 - ) - all_vis_equal(props) - return true -end +------------------------------------------------------------------------------------------------------------------------- +-- SAVE PREPARED SONGS to PREPARED.DAT +-- User has option to store the List of Prepared songs in an external dat file with songs. +------------------------------------------------------------------------------------------------------------------------- -function change_prepared_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "mng_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "prepared_showing"), - mode1 .. "PREPARED SONGS" .. mode2 - ) - all_vis_equal(props) +function save_prepared() + dbg_method("save_prepared") + local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") + for i, name in ipairs(prepared_songs) do + -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs + file:write(name, "\n") + -- end + end + file:close() return true end -function change_options_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "disp_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "options_showing"), - mode1 .. "DISPLAY OPTIONS" .. mode2 - ) - all_vis_equal(props) - return true -end -function change_src_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "src_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description( - obs.obs_properties_get(props, "src_showing"), - mode1 .. "SOURCE TEXT SELECTIONS" .. mode2 - ) - all_vis_equal(props) - return true -end +------------------------------------------------------------------------------------------------------------------------- +-- UPDATE MONITOR +-- Lyrics maintains an external Monitor.htm file in the songs directory that can be viewed externally or docked within +-- OBS as a browser option. This function keeps that files HTML text updated. +------------------------------------------------------------------------------------------------------------------------- +function update_monitor() + dbg_method("update_monitor") + local tableback = "black" + local text = "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = text .. "" + text = + text .. + "
" + text = + text .. + "
" + if using_preview then + text = text .. "From Preview
" + elseif using_source then + text = text .. "From Source: " .. load_scene .. "
" + else + text = text .. "Prepared Song: " .. prepared_index + text = + text .. + " of " .. #prepared_songs .. "
" + end + text = + text .. + "
Lyric Page: " + local pages = (#lyrics == 0) and 0 or #lyrics-1 + if page_index < #lyrics then + text = text .. page_index.. " of " .. pages .. "
" + else + text = text .. "Blank
" + end + + if #verses ~= nil and mon_verse > 0 then + text = + text .. + "
Verse: " .. mon_verse + text = text .. " of " .. #verses .. "
" + end + text = text .. "
" + if not anythingActive() then + tableback = "#440000" + end + local visbgTitle = tableback + local visbgText = tableback + if text_status == TEXT_HIDDEN or text_status == TEXT_HIDING then + visbgText = "maroon" + if link_text then + visbgTitle = "maroon" + end + end -function change_ctrl_visible(props, prop, settings) - local pp = obs.obs_properties_get(script_props, "ctrl_grp") - local vis = not obs.obs_property_visible(pp) - obs.obs_property_set_visible(pp, vis) - local mode1, mode2 = vMode(vis) - obs.obs_property_set_description(obs.obs_properties_get(props, "ctrl_showing"), mode1 .. "LYRIC CONTROLS" .. mode2) - all_vis_equal(props) + text = + text .. + "
" + if mon_song ~= "" and mon_song ~= nil then + text = + text .. + "" + text = + text .. + "" + end + if mon_lyric ~= "" and mon_lyric ~= nil then + text = + text .. + "" + text = + text .. "" + end + if mon_nextlyric ~= "" and mon_nextlyric ~= nil then + text = + text .. + "" + text = text .. "" + end + if mon_alt ~= "" and mon_alt ~= nil then + text = + text .. + "" + text = + text .. + "" + end + if mon_nextalt ~= "" and mon_nextalt ~= nil then + text = + text .. + "" + text = text .. "" + end + if mon_nextsong ~= "" and mon_nextsong ~= nil then + text = + text .. + "" + text = text .. "" + end + text = text .. "
Song
Title
" .. mon_song .. "
Current
Page
• " .. mon_lyric .. "
Next
Page
• " .. mon_nextlyric .. "
Alt
Lyric
• " .. mon_alt .. "
Next
Alt
• " .. mon_nextalt .. "
Next
Song:
" .. mon_nextsong .. "
" + local file = io.open(get_songs_folder_path() .. "/" .. "Monitor.htm", "w") + dbg_inner("write monitor file") + file:write(text) + file:close() return true end -function change_fade_property(props, prop, settings) - local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") - obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "use100percent"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "allowBackFade"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_set and allow_back_fade) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_set and allow_back_fade) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_set and allow_back_fade) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_set and allow_back_fade) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_set and allow_back_fade) - obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), text_fade_enabled and not use100percent) - local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") - obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) - return true -end +------------------------------------------------------------------------------------------------------------------------- +-- GET SONG FILE PATH +-- Working function that returns the full OS path of the given song name and suffix +------------------------------------------------------------------------------------------------------------------------- -function change_100percent_property(props, prop, settings) - use100percent = obs.obs_data_get_bool(settings, "use100percent") - obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), not use100percent) - return true +function get_song_file_path(name, suffix) + if name == nil then + return nil + end + return get_songs_folder_path() .. "\\" .. name .. suffix end -function change_back_fade_property(props, prop, settings) - allow_back_fade = obs.obs_data_get_bool(settings, "allowBackFade") - if allow_back_fade then - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_enabled) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_enabled) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_enabled) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_enabled) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_enabled) - else - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), false) - end - return true +------------------------------------------------------------------------------------------------------------------------- +-- GET SONG FOLDER PATH +-- Working function that returns the full OS path of the Song Folder +------------------------------------------------------------------------------------------------------------------------ +function get_songs_folder_path() + local sep = package.config:sub(1, 1) + local path = "" + if windows_os then + path = os.getenv("USERPROFILE") + else + path = os.getenv("HOME") + end + return path .. sep .. ".config" .. sep .. ".obs_lyrics" end -function show_help_button(props, prop, settings) - dbg_method("show help") - local hb = obs.obs_properties_get(props, "show_help_button") - showhelp = not showhelp - if showhelp then - obs.obs_property_set_description(hb, help) +------------------------------------------------------------------------------------------------------------------------- +-- GET SONG LINES +-- Reads text of the specified song file into a songs_lines table +------------------------------------------------------------------------------------------------------------------------ +function get_song_text(name) + local song_lines = {} + local path = {} + if testValid(name) then + path = get_song_file_path(name, ".txt") else - obs.obs_property_set_description(hb, "SHOW MARKUP SYNTAX HELP") + path = get_song_file_path(enc(name), ".enc") + end + local file = io.open(path, "r") + if file ~= nil then + for line in file:lines() do + song_lines[#song_lines + 1] = line + end + file:close() + else + return nil + end + + return song_lines +end + +-- ------ +---------------- +------------------------ OBS DEFAULT FUNCTIONS +-- -------------- +-------- + +-- A function named script_properties defines the properties that the user +-- can change for the entire script module. + +function script_description() + return description +end +------------------------------------------------------------------------------------------------------------------------- +-- Help Button Text +-- Text shown when user selects toggles the Help Button to see valid Lyric Markup syntax +------------------------------------------------------------------------------------------------------------------------ + +local help = + "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. + " Markup      Syntax         Markup      Syntax \n" .. + "============ ==========   ============ ==========\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line" + +------------------------------------------------------------------------------------------------------------------------- +-- OBS PROPERTIES FUNCTION (See OBS Documentation) +------------------------------------------------------------------------------------------------------------------------ +function script_properties() + dbg_method("script_properties") + editVisSet = false + script_props = obs.obs_properties_create() + obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) + ----------- + obs.obs_properties_add_button(script_props, "info_showing", "▲- HIDE SONG INFORMATION -▲", change_info_visible) + local gp = obs.obs_properties_create() + obs.obs_properties_add_text(gp, "prop_edit_song_title", "Song Title (Filename)", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gp, "show_help_button", "SHOW MARKUP SYNTAX HELP", show_help_button) + obs.obs_properties_add_text(gp, "prop_edit_song_text", "Song Lyrics", obs.OBS_TEXT_MULTILINE) + obs.obs_properties_add_button(gp, "prop_save_button", "Save Song", save_song_clicked) + obs.obs_properties_add_button(gp, "prop_delete_button", "Delete Song", delete_song_clicked) + obs.obs_properties_add_button(gp, "prop_opensong_button", "Edit Song with System Editor", open_song_clicked) + obs.obs_properties_add_button(gp, "prop_open_button", "Open Songs Folder", open_button_clicked) + obs.obs_properties_add_group( + script_props, + "info_grp", + "Song Title (filename) and Lyrics Information", + obs.OBS_GROUP_NORMAL, + gp + ) + ------------ + obs.obs_properties_add_button( + script_props, + "prepared_showing", + "▲- HIDE PREPARED SONGS -▲", + change_prepared_visible + ) + gp = obs.obs_properties_create() + local prop_dir_list = + obs.obs_properties_add_list( + gp, + "prop_directory_list", + "Song Directory", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + table.sort(song_directory) + for _, name in ipairs(song_directory) do + obs.obs_property_list_add_string(prop_dir_list, name, name) + end + obs.obs_property_set_modified_callback(prop_dir_list, load_song_from_directory) + obs.obs_properties_add_button(gp, "prop_prepare_button", "Add Song/Text to Prepared List", prepare_song_clicked) + obs.obs_properties_add_button(gp, "prop_preview_button", "Preview Songs/Text", preview_clicked) + obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) + local gps = obs.obs_properties_create() + obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) + local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) + gps = obs.obs_properties_create() + local prepare_prop = + obs.obs_properties_add_list( + gps, + "prop_prepared_list", + "Prepared ", + obs.OBS_COMBO_TYPE_EDITABLE, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_list_add_string(prepare_prop, "*** LIST OF PREPARED SONGS ***","") + for _, name in ipairs(prepared_songs) do + obs.obs_property_list_add_string(prepare_prop, name, name) + end + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") + obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) + local count = obs.obs_property_list_item_count(prepare_prop) + if count > 1 then + obs.obs_property_set_description( prepare_prop, "Prepared (" .. count-1 .. ")") + end + obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) + obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) + local eps = obs.obs_properties_create() + local edit_prop = + obs.obs_properties_add_editable_list( + eps, + "prep_list", + "Prepared Songs/Text", + obs.OBS_EDITABLE_LIST_TYPE_STRINGS, + nil, + nil + ) + obs.obs_properties_add_button(eps, "prop_save_button", "Save Changes", save_edits_clicked) + local edit_group_prop = + obs.obs_properties_add_group( + gps, + "edit_grp", + "Edit Prepared Songs - Manually entered Titles (Filenames) must be in directory", + obs.OBS_GROUP_NORMAL, + eps + ) + local saveExtProp = obs.obs_properties_add_bool(eps, "saveExternal", "Use external Prepared.dat file ") + obs.obs_property_set_modified_callback(saveExtProp, reLoadPrepared) + + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) + obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) + ------------------ + obs.obs_properties_add_button(script_props, "ctrl_showing", "▲- HIDE LYRIC CONTROLS -▲", change_ctrl_visible) + hotkey_props = obs.obs_properties_create() + local hktitletext = obs.obs_properties_add_text(hotkey_props, "hotkey-title", "", obs.OBS_TEXT_DEFAULT) + obs.obs_properties_add_button(hotkey_props, "prop_prev_button", "Previous Lyric", prev_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_button", "Next Lyric", next_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_hide_button", "Show/Hide Lyrics", toggle_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_home_button", "Reset to Song Start", home_button_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_prev_prep_button", "Previous Prepared", prev_prepared_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_next_prep_button", "Next Prepared", next_prepared_clicked) + obs.obs_properties_add_button(hotkey_props, "prop_reset_button", "Reset to First Prepared Song", reset_button_clicked) + obs.obs_properties_add_group(script_props,"ctrl_grp","Lyric Control Buttons ",obs.OBS_GROUP_NORMAL,hotkey_props) + name_hotkeys() + ------ + obs.obs_properties_add_button(script_props, "options_showing", "▲- HIDE DISPLAY OPTIONS -▲", change_options_visible) + gp = obs.obs_properties_create() + local lines_prop = obs.obs_properties_add_int_slider(gp, "prop_lines_counter", "Lines to Display", 1, 50, 1) + obs.obs_property_set_long_description( + lines_prop, + "Sets default lines per page of lyric, overwritten by Markup: #L:n" + ) + local prop_lines = obs.obs_properties_add_bool(gp, "prop_lines_bool", "Strictly ensure number of lines") + obs.obs_property_set_long_description(prop_lines, "Guarantees fixed number of lines per page") + local link_prop = obs.obs_properties_add_bool(gp, "do_link_text", "Show/Hide All Sources with Lyric Text") + obs.obs_property_set_long_description(link_prop, "Hides title and static text at end of lyrics") + + local transition_prop = + obs.obs_properties_add_bool(gp, "transition_enabled", "Transition Preview to Program on lyric change") + obs.obs_property_set_modified_callback(transition_prop, change_transition_property) + obs.obs_property_set_long_description( + transition_prop, + "Use with Studio Mode, duplicate sources, and OBS source transitions (beta)" + ) + + local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable Fade Transitions") + obs.obs_property_set_modified_callback(fade_prop, change_fade_property) + local fp1 = obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) + local fp2 = obs.obs_properties_add_bool(gp,"use100percent", "Use 0-100% opacity for fades") + local fp3 = obs.obs_properties_add_bool(gp,"allowBackFade", "Enable Background Fading") + obs.obs_property_set_modified_callback(fp2, change_100percent_property) + obs.obs_property_set_modified_callback(fp3, change_back_fade_property) + local oprefprop = obs.obs_properties_add_button(gp, "refreshOP", "Mark Max Opacity for Source Fades", read_source_opacity_clicked) + obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) + + ------------- + obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) + gp = obs.obs_properties_create() + + local source_prop = + obs.obs_properties_add_list( + gp, + "prop_source_list", + "Text Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + local flbprop = obs.obs_properties_add_bool(gp, "fade_text_back", "Fade Text Background") + local title_source_prop = + obs.obs_properties_add_list( + gp, + "prop_title_list", + "Title Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + local ftbprop = obs.obs_properties_add_bool(gp, "fade_title_back", "Fade Title Background") + local alternate_source_prop = + obs.obs_properties_add_list( + gp, + "prop_alternate_list", + "Alternate Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + local fabprop = obs.obs_properties_add_bool(gp, "fade_alternate_back", "Fade Alternate Background") + local static_source_prop = + obs.obs_properties_add_list( + gp, + "prop_static_list", + "Static Source", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + local fsbprop = obs.obs_properties_add_bool(gp, "fade_static_back", "Fade Static Background") + obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) + + local dlprop = obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) + xgp = obs.obs_properties_create() + local extra_linked_prop = + obs.obs_properties_add_list( + xgp, + "extra_linked_list", + "Linked Sources", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + -- initialize previously loaded extra properties from table + for _, sourceName in ipairs(extra_sources) do + obs.obs_property_list_add_string(extra_linked_prop, sourceName, sourceName) end - return true -end - -function filter_songs_clicked(props, p) - local pp = obs.obs_properties_get(props, "meta") - if not obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "filter_songs_button") - obs.obs_property_set_description(mpb, "Clear Filters") -- change button function - meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") - refresh_directory() + local extra_source_prop = + obs.obs_properties_add_list( + xgp, + "extra_source_list", + " Select Source:", + obs.OBS_COMBO_TYPE_LIST, + obs.OBS_COMBO_FORMAT_STRING + ) + obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") + local febprop = obs.obs_properties_add_bool(xgp, "fade_extra_back", "Fade Background for Text Sources") + local clearcall_prop = + obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) + local extra_group_prop = + obs.obs_properties_add_group(gp, "xtr_grp", "Additional Visibility Linked Sources ", obs.OBS_GROUP_NORMAL, xgp) + obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) + local count = obs.obs_property_list_item_count(extra_linked_prop) + if count > 0 then + do_linked_clicked(script_props,dlprop) + obs.obs_property_set_description( + extra_linked_prop, + "Linked Sources (" .. count .. ")" + ) else - obs.obs_property_set_visible(pp, false) - meta_tags = "" -- clear meta tags - refresh_directory() - local mpb = obs.obs_properties_get(props, "filter_songs_button") -- - obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function - end - return true -end - -function edit_prepared_clicked(props, p) - local pp = obs.obs_properties_get(props, "edit_grp") - if obs.obs_property_visible(pp) then - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared List") - return true - end - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - local count = obs.obs_property_list_item_count(prop_prep_list) - local songNames = obs.obs_data_get_array(script_sets, "prep_list") - local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2 do - obs.obs_data_array_erase(songNames, 0) - end - end - for i = 1, count do - local song = obs.obs_property_list_item_string(prop_prep_list, i) - local array_obj = obs.obs_data_create() - obs.obs_data_set_string(array_obj, "value", song) - obs.obs_data_array_push_back(songNames, array_obj) - obs.obs_data_release(array_obj) + clear_linked_clicked(script_props, clearcall_prop) end - obs.obs_data_set_array(script_sets, "prep_list", songNames) - obs.obs_data_array_release(songNames) - obs.obs_property_set_visible(pp, true) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Cancel Prepared Edits") - return true -end --- removes prepared songs -function save_edits_clicked(props, p) - load_source_song_directory(false) - prepared_songs = {} - local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") - obs.obs_property_list_clear(prop_prep_list) - obs.obs_property_list_add_string(prop_prep_list, "*** LIST OF PREPARED SONGS ***", "") - local songNames = obs.obs_data_get_array(script_sets, "prep_list") - local count2 = obs.obs_data_array_count(songNames) - if count2 > 0 then - for i = 0, count2 - 1 do - local item = obs.obs_data_array_item(songNames, i) - local itemName = obs.obs_data_get_string(item, "value") - if get_index_in_list(song_directory, itemName) ~= nil then - prepared_songs[#prepared_songs + 1] = itemName - obs.obs_property_list_add_string(prop_prep_list, itemName, itemName) + local sources = obs.obs_enum_sources() + obs.obs_property_list_add_string(extra_source_prop, "List of Valid Sources", "") + if sources ~= nil then + local n = {} + for _, source in ipairs(sources) do + local name = obs.obs_source_get_name(source) + if isValid(source) then + obs.obs_property_list_add_string(extra_source_prop, name, name) -- add source to extra list end - obs.obs_data_release(item) + source_id = obs.obs_source_get_unversioned_id(source) + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then + n[#n + 1] = name + end + end + table.sort(n) + obs.obs_property_list_add_string(source_prop, "", "") + obs.obs_property_list_add_string(title_source_prop, "", "") + obs.obs_property_list_add_string(alternate_source_prop, "", "") + obs.obs_property_list_add_string(static_source_prop, "", "") + for _, name in ipairs(n) do + obs.obs_property_list_add_string(source_prop, name, name) + obs.obs_property_list_add_string(title_source_prop, name, name) + obs.obs_property_list_add_string(alternate_source_prop, name, name) + obs.obs_property_list_add_string(static_source_prop, name, name) end end - obs.obs_data_array_release(songNames) - save_prepared() - obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") - prepared_index = 0 - pp = obs.obs_properties_get(script_props, "edit_grp") - obs.obs_property_set_visible(pp, false) - local mpb = obs.obs_properties_get(props, "prop_manage_button") - obs.obs_property_set_description(mpb, "Edit Prepared Songs List") - - if #prepared_songs > 0 then - obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") - else - obs.obs_property_set_description(prop_prep_list, "Prepared") - end - obs.obs_properties_apply_settings(props, script_sets) - return true + obs.source_list_release(sources) + + ----------------- + obs.obs_property_set_enabled(hktitletext, false) + obs.obs_property_set_visible(edit_group_prop, false) + obs.obs_property_set_visible(meta_group_prop, false) + obs.obs_property_set_visible(fp1, text_fade_enabled) + obs.obs_property_set_visible(fp2, text_fade_enabled) + obs.obs_property_set_visible(fp3, text_fade_enabled) + obs.obs_property_set_visible(flbprop, text_fade_enabled and allow_back_fade) + obs.obs_property_set_visible(ftbprop, text_fade_enabled and allow_back_fade) + obs.obs_property_set_visible(fabprop, text_fade_enabled and allow_back_fade) + obs.obs_property_set_visible(fsbprop, text_fade_enabled and allow_back_fade) + obs.obs_property_set_visible(febprop, text_fade_enabled and allow_back_fade) + obs.obs_property_set_visible(oprefprop, text_fade_enabled and not use100percent) + + + read_source_opacity() + return script_props end -function change_transition_property(props, prop, settings) - transition_enabled = obs.obs_data_get_bool(settings, "transition_enabled") - local text_fade_set_prop = obs.obs_properties_get(props, "text_fade_enabled") - local fade_speed_prop = obs.obs_properties_get(props, "text_fade_speed") - obs.obs_property_set_enabled(text_fade_set_prop, not transition_enabled) - obs.obs_property_set_enabled(fade_speed_prop, not transition_enabled) - return true +------------------------------------------------------------------------------------------------------------------------- +-- SCRIPT UPDATE (See OBS Documentation) +------------------------------------------------------------------------------------------------------------------------ +-- script_update is called when settings are changed +function script_update(settings) + text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") + text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") + display_lines = obs.obs_data_get_int(settings, "prop_lines_counter") + source_name = obs.obs_data_get_string(settings, "prop_source_list") + alternate_source_name = obs.obs_data_get_string(settings, "prop_alternate_list") + static_source_name = obs.obs_data_get_string(settings, "prop_static_list") + title_source_name = obs.obs_data_get_string(settings, "prop_title_list") + ensure_lines = obs.obs_data_get_bool(settings, "prop_lines_bool") + link_text = obs.obs_data_get_bool(settings, "do_link_text") + link_extras = obs.obs_data_get_bool(settings, "link_extra_with_text") + use100percent = obs.obs_data_get_bool(settings, "use100percent") + allow_back_fade = obs.obs_data_get_bool(settings, "allowBackFade") + fade_text_back = obs.obs_data_get_bool(settings, "fade_text_back") and allow_back_fade + fade_title_back = obs.obs_data_get_bool(settings, "fade_title_back") and allow_back_fade + fade_alternate_back = obs.obs_data_get_bool(settings, "fade_alternate_back") and allow_back_fade + fade_static_back = obs.obs_data_get_bool(settings, "fade_static_back") and allow_back_fade + fade_extra_back = obs.obs_data_get_bool(settings, "fade_extra_back") and allow_back_fade + update_monitor() end +------------------------------------------------------------------------------------------------------------------------- +-- SCRIPT DEFAULTS Sets Default Values used by the UI (See OBS Documentation) +------------------------------------------------------------------------------------------------------------------------ +function script_defaults(settings) + dbg_method("script_defaults") + obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) + obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") + obs.obs_data_set_default_bool(settings,"use100percent", true) + obs.obs_data_set_default_bool(settings,"text_fade_enabled", false) + obs.obs_data_set_default_int(settings,"text_fade_speed", 5) + if os.getenv("HOME") == nil then + windows_os = true + end -- must be set prior to calling any file functions + if windows_os then + os.execute('mkdir "' .. get_songs_folder_path() .. '"') + else + os.execute('mkdir -p "' .. get_songs_folder_path() .. '"') + end +end --- reloads prepared songs if source , settings or file, is changed -function reLoadPrepared(props, prop, settings) -dbg_method("reLoad Prepared") - local newSaveExternal = obs.obs_data_get_bool(settings, "saveExternal") - if saveExternal ~= newSaveExternal then - save_prepared(settings) - saveExternal = obs.obs_data_get_bool(settings, "saveExternal") - load_prepared(settings) - end - return true +------------------------------------------------------------------------------------------------------------------------- +-- Working function to return if a source is Valid to be included (excludes non-visible sources like Audio) +------------------------------------------------------------------------------------------------------------------------ +function isValid(source) + if source ~= nil then + local flags = obs.obs_source_get_output_flags(source) + local targetFlag = bit.bor(obs.OBS_SOURCE_VIDEO, obs.OBS_SOURCE_CUSTOM_DRAW) + if bit.band(flags, targetFlag) == targetFlag then + return true + end + end + return false end +----------------------------------------------------------------------------------------------------------------------- +-- This is a FILE function to read prepared songs from an external file, but also a SETTINGS retrieval function +----------------------------------------------------------------------------------------------------------------------- function load_prepared(settings) dbg_method("Load Prepared") prepared_songs = {} @@ -2577,13 +2780,14 @@ dbg_method("Load Prepared") end end +----------------------------------------------------------------------------------------------------------------------- +-- This is a FILE function to write prepared songs from an external file, but also a SETTINGS storage function +----------------------------------------------------------------------------------------------------------------------- function save_prepared(settings) if saveExternal then -- saves preprepared songs in prepared.dat file local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") for i, name in ipairs(prepared_songs) do - -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs file:write(name, "\n") - -- end end file:close() else -- saves prepared songs in settings array @@ -2599,7 +2803,10 @@ function save_prepared(settings) end end --- A function named script_save will be called when the script is saved + +----------------------------------------------------------------------------------------------------------------------- +-- Save Script Settings (See OBS Documentation) +----------------------------------------------------------------------------------------------------------------------- function script_save(settings) dbg_method("script_save") save_prepared() @@ -2649,9 +2856,10 @@ function script_save(settings) save_prepared(settings) end --- a function named script_load will be called on startup and mostly handles loading hotkey data to OBS --- sets callback to obs_frontend Event Callback --- + +----------------------------------------------------------------------------------------------------------------------- +-- Load Script Settings (See OBS Documentation) +----------------------------------------------------------------------------------------------------------------------- function script_load(settings) dbg_method("script_load") hotkey_n_id = obs.obs_hotkey_register_frontend("lyric_next_hotkey", "Advance Lyrics", next_lyric) @@ -2729,6 +2937,9 @@ function script_load(settings) obs.obs_frontend_add_event_callback(on_event) -- Setup Callback for event capture end +----------------------------------------------------------------------------------------------------------------------- +-- This function "tries" to ensure sources are not abandoned with 0% opacity +----------------------------------------------------------------------------------------------------------------------- function script_unload() -- not sure this is working as expected all_sources_fade = true text_opacity = 100 @@ -3131,6 +3342,7 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) +print(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page dbg_bool("Active:", source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS From 3520e9a2831acf1fe0928a43d8ba5716f27c3c5d Mon Sep 17 00:00:00 2001 From: wzaggle Date: Wed, 27 Oct 2021 00:44:46 -0600 Subject: [PATCH 079/105] Update lyrics+.lua More Internal Documentation --- lyrics+.lua | 179 +++++++++++++++++++++++++++------------------------- 1 file changed, 94 insertions(+), 85 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 1b31d70..140f8f4 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -1,3 +1,10 @@ +-- __ _ __ _______ ______ ____ ____ ___ +-- / / | | / / / ___ / /_ __/ / ___\ / ___\ __/ /_ +-- / / | |_/ / / /__/ / / / / / \ \ /_ __/ +-- / /___ |_ _/ / __ | __/ /__ | |__ __\ \ /__/ +-- /______/ /_/ /_/ |_|/______/ |____\ /____/ +-- +-- --- Copyright 2020 amirchev/wzaggle -- Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +35,6 @@ -- Alternate Source --> The text source that will contain the Pages of Alternate song lyrics -- Static Source -----> The text source that will contain any Static Text that will be shown along with lyrics - --------------------------------------------------------------------------------------------------------------------- -- NOTES ON INTERNAL DOCUMENTATION @@ -165,6 +171,25 @@ the active window. --]] transition_enabled = false transition_completed = false +------------------------------------------------------------------------------------------------------------------------- +-- Help Button Text +-- Text shown when user selects toggles the Help Button to see valid Lyric Markup syntax +------------------------------------------------------------------------------------------------------------------------ + +help = + "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. + " Markup      Syntax         Markup      Syntax \n" .. + "============ ==========   ============ ==========\n" .. + " Display n Lines    #L:n      End Page after Line   Line ###\n" .. + " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. + " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. + " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. + " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. + "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. + "Comment Line    // Line       Block Comments    //[ and //] \n" .. + "Mark Verses     ##V        Override Title     #T: text\n\n" .. + "Optional comma delimited meta tags follow '//meta ' on 1st line" + -- SIMPLE DEBUGGING/PRINT MECHANISM DEBUG = true -- on switch for entire debugging mechanism DEBUG_METHODS = true -- print method names @@ -1085,7 +1110,7 @@ end -------- ---------------- ------------------------- SCRIPT WORKING FUNCTIONS +------------------------ SCRIPT WORKING FUNCTIONS (These form the bulk of the Effort) ---------------- -------- ------------------------------------------------------------------------------------------------------------------------- @@ -1544,6 +1569,7 @@ function prepare_song_by_index(index) end --------------------------------------------------------------------------------------------------------------------- +-- PREPARE SONG BY NAME -- Function to parse and process markups within the lyrics and break the text into defined pages and verses -- The first line of song/text files can contain an optional list of meta tags that organize the files into -- user defined genre or categories for later filtering during selection @@ -1605,49 +1631,49 @@ function prepare_song_by_name(name) for _, line in ipairs(song_lines) do local new_lines = 1 local single_line = false - local comment_index = line:find("//%[") -- Look for comment block Set + local comment_index = line:find("//%[") -- Look for Comment Block Set if comment_index ~= nil then commentBlock = true line = line:sub(comment_index + 3) end - comment_index = line:find("//]") -- Look for comment block Clear + comment_index = line:find("//]") -- Look for Comment Block Clear if comment_index ~= nil then commentBlock = false line = line:sub(1, comment_index - 1) new_lines = 0 end if not commentBlock then - local comment_index = line:find("%s*//") + local comment_index = line:find("%s*//") -- Single line comment if comment_index ~= nil then line = line:sub(1, comment_index - 1) new_lines = 0 end - local alternate_index = line:find("#A%[") + local alternate_index = line:find("#A%[") -- Alternate Block set if alternate_index ~= nil then use_alternate = true line = line:sub(1, alternate_index - 1) new_lines = 0 end - alternate_index = line:find("#A]") + alternate_index = line:find("#A]") -- Alternate Block clear if alternate_index ~= nil then use_alternate = false line = line:sub(1, alternate_index - 1) new_lines = 0 end - local static_index = line:find("#S%[") + local static_index = line:find("#S%[") -- Static Block Set if static_index ~= nil then use_static = true line = line:sub(1, static_index - 1) new_lines = 0 end - static_index = line:find("#S]") + static_index = line:find("#S]") -- Static Block Clear if static_index ~= nil then use_static = false line = line:sub(1, static_index - 1) new_lines = 0 end - local newcount_index = line:find("#L:") + local newcount_index = line:find("#L:") -- Lines per Page if newcount_index ~= nil then local iS, iE = line:find("%d+", newcount_index + 3) local newLines = tonumber(line:sub(iS, iE)) @@ -1663,20 +1689,20 @@ function prepare_song_by_name(name) line = line:sub(1, newcount_index - 1) new_lines = 0 -- ignore line end - local static_index = line:find("#S:") + local static_index = line:find("#S:") -- Single Static if static_index ~= nil then line = line:sub(static_index + 3) static_text = line new_lines = 0 end - local title_index = line:find("#T:") + local title_index = line:find("#T:") -- Set Title if title_index ~= nil then local title_indexEnd = line:find("%s+", title_index + 1) line = line:sub(title_indexEnd + 1) alt_title = line new_lines = 0 end - local alt_index = line:find("#A:") + local alt_index = line:find("#A:") -- Single Alternate if alt_index ~= nil then local alt_indexStart, alt_indexEnd = line:find("%d+", alt_index + 3) new_lines = tonumber(line:sub(alt_indexStart, alt_indexEnd)) @@ -1684,18 +1710,18 @@ function prepare_song_by_name(name) line = line:sub(alt_indexEnd + 1) singleAlternate = true end - if line:find("###") ~= nil then -- Look for single line + if line:find("###") ~= nil then -- End of Page line = line:gsub("%s*###%s*", "") single_line = true end - local newcount_index = line:find("#D:") + local newcount_index = line:find("#D:") -- Duplicate Lines if newcount_index ~= nil then local newcount_indexStart, newcount_indexEnd = line:find("%d+", newcount_index + 3) new_lines = tonumber(line:sub(newcount_indexStart, newcount_indexEnd)) _, newcount_indexEnd = line:find("%s+", newcount_indexEnd + 1) line = line:sub(newcount_indexEnd + 1) end - local refrain_index = line:find("#R%[") + local refrain_index = line:find("#R%[") -- Start In-Line Refrain if refrain_index ~= nil then if next(refrain) ~= nil then for i, _ in ipairs(refrain) do @@ -1707,7 +1733,7 @@ function prepare_song_by_name(name) line = line:sub(1, refrain_index - 1) new_lines = 0 end - refrain_index = line:find("#r%[") + refrain_index = line:find("#r%[") -- Start External Refrain if refrain_index ~= nil then if next(refrain) ~= nil then for i, _ in ipairs(refrain) do @@ -1719,14 +1745,14 @@ function prepare_song_by_name(name) line = line:sub(1, refrain_index - 1) new_lines = 0 end - refrain_index = line:find("#R]") + refrain_index = line:find("#R]") -- End In-Line Refrain if refrain_index ~= nil then recordRefrain = false showText = true line = line:sub(1, refrain_index - 1) new_lines = 0 end - refrain_index = line:find("#r]") + refrain_index = line:find("#r]") -- End External Refrain if refrain_index ~= nil then recordRefrain = false showText = true @@ -1734,9 +1760,9 @@ function prepare_song_by_name(name) new_lines = 0 end - refrain_index = line:find("##R") + refrain_index = line:find("##R") -- Repeat Refrain if refrain_index == nil then - refrain_index = line:find("##r") + refrain_index = line:find("##r") -- Repeat Refrain end if refrain_index ~= nil then playRefrain = true @@ -1745,25 +1771,25 @@ function prepare_song_by_name(name) else playRefrain = false end - newcount_index = line:find("#P:") + newcount_index = line:find("#P:") -- Add Blank Lines if newcount_index ~= nil then new_lines = tonumber(line:sub(newcount_index + 3)) line = line:sub(1, newcount_index - 1) end - newcount_index = line:find("#B:") + newcount_index = line:find("#B:") -- Add Blank Lines if newcount_index ~= nil then new_lines = tonumber(line:sub(newcount_index + 3)) line = line:sub(1, newcount_index - 1) end - local phantom_index = line:find("##P") + local phantom_index = line:find("##P") -- Single Blank Line if phantom_index ~= nil then line = line:sub(1, phantom_index - 1) end - phantom_index = line:find("##B") + phantom_index = line:find("##B") -- Single Blank Line if phantom_index ~= nil then line = line:gsub("%s*##B%s*", "") .. "\n" end - local verse_index = line:find("##V") + local verse_index = line:find("##V") -- Mark Start of Verse if verse_index ~= nil then line = line:sub(1, verse_index - 1) new_lines = 0 @@ -1771,14 +1797,14 @@ function prepare_song_by_name(name) dbg_inner("Verse: " .. #lyrics) end if line ~= nil then - if use_static then + if use_static then -- Text goes to Static Source if static_text == "" then static_text = line else static_text = static_text .. "\n" .. line end else - if use_alternate or singleAlternate then + if use_alternate or singleAlternate then -- Text goes to Alternate Source if recordRefrain then displaySize = refrain_display_lines else @@ -1824,26 +1850,26 @@ function prepare_song_by_name(name) end end singleAlternate = false - else - if recordRefrain then - displaySize = refrain_display_lines + else -- Text goes to Refrain or Verse Lyrics + if recordRefrain then + displaySize = refrain_display_lines -- display lines controlled by Refrain Size else - displaySize = adjusted_display_lines + displaySize = adjusted_display_lines -- display lines controlled by Lyric Size end if new_lines > 0 then while (new_lines > 0) do - if recordRefrain then + if recordRefrain then -- Recording Refrain ? if (cur_line == 1) then - refrain[#refrain + 1] = line + refrain[#refrain + 1] = line -- Start new page else - refrain[#refrain] = refrain[#refrain] .. "\n" .. line + refrain[#refrain] = refrain[#refrain] .. "\n" .. line -- Add to page end end if showText and line ~= nil then if (cur_line == 1) then - lyrics[#lyrics + 1] = line + lyrics[#lyrics + 1] = line -- Start new page else - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" .. line -- Add to Page end end cur_line = cur_line + 1 @@ -1852,10 +1878,10 @@ function prepare_song_by_name(name) for i = cur_line, displaySize, 1 do cur_line = i if showText and lyrics[#lyrics] ~= nil then - lyrics[#lyrics] = lyrics[#lyrics] .. "\n" + lyrics[#lyrics] = lyrics[#lyrics] .. "\n" -- pad new lines end if recordRefrain then - refrain[#refrain] = refrain[#refrain] .. "\n" + refrain[#refrain] = refrain[#refrain] .. "\n" -- pad new lines end end end @@ -1867,14 +1893,14 @@ function prepare_song_by_name(name) end if playRefrain == true and not recordRefrain then -- no recursive call of Refrain within Refrain Record for _, refrain_line in ipairs(refrain) do - lyrics[#lyrics + 1] = refrain_line + lyrics[#lyrics + 1] = refrain_line -- add play refrain line end end end end end end - if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then + if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then -- pad lines for i = cur_line, displaySize, 1 do cur_line = i if use_alternate then @@ -1891,8 +1917,7 @@ function prepare_song_by_name(name) end end end - lyrics[#lyrics + 1] = "" - -- pause_timer = false + lyrics[#lyrics + 1] = "" -- Add blank page at end of lyrics return true end @@ -1925,8 +1950,8 @@ function delete_song(name) else path = get_song_file_path(enc(name), ".enc") end - os.remove(path) - table.remove(song_directory, get_index_in_list(song_directory, name)) + os.remove(path) -- delete from OS + table.remove(song_directory, get_index_in_list(song_directory, name)) -- delete from table source_filter = false load_source_song_directory(false) end @@ -2016,7 +2041,7 @@ end function readTags(name) local meta = "" local path = {} - if testValid(name) then + if testValid(name) then -- get the full file path path = get_song_file_path(name, ".txt") else path = get_song_file_path(enc(name), ".enc") @@ -2024,13 +2049,13 @@ function readTags(name) local file = io.open(path, "r") if file ~= nil then for line in file:lines() do - meta = line + meta = line -- read one line and stop break end file:close() end local meta_index = meta:find("//meta ") -- Look for meta block Set - if meta_index ~= nil then + if meta_index ~= nil then meta = meta:sub(meta_index + 7) return ParseCSVLine(meta) end @@ -2205,9 +2230,7 @@ function save_prepared() dbg_method("save_prepared") local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") for i, name in ipairs(prepared_songs) do - -- if not scene_load_complete or i > 1 then -- don't save scene prepared songs file:write(name, "\n") - -- end end file:close() return true @@ -2334,7 +2357,6 @@ end -- GET SONG FILE PATH -- Working function that returns the full OS path of the given song name and suffix ------------------------------------------------------------------------------------------------------------------------- - function get_song_file_path(name, suffix) if name == nil then return nil @@ -2364,17 +2386,17 @@ end function get_song_text(name) local song_lines = {} local path = {} - if testValid(name) then + if testValid(name) then -- get file path path = get_song_file_path(name, ".txt") else path = get_song_file_path(enc(name), ".enc") end - local file = io.open(path, "r") + local file = io.open(path, "r") -- open the file if file ~= nil then for line in file:lines() do - song_lines[#song_lines + 1] = line + song_lines[#song_lines + 1] = line -- read lines into song_lines table end - file:close() + file:close() -- close file else return nil end @@ -2394,24 +2416,7 @@ end function script_description() return description end -------------------------------------------------------------------------------------------------------------------------- --- Help Button Text --- Text shown when user selects toggles the Help Button to see valid Lyric Markup syntax ------------------------------------------------------------------------------------------------------------------------- -local help = - "▪▪▪▪▪ MARKUP SYNTAX HELP ▪▪▪▪▪▲- CLICK TO CLOSE -▲▪▪▪▪▪\n\n" .. - " Markup      Syntax         Markup      Syntax \n" .. - "============ ==========   ============ ==========\n" .. - " Display n Lines    #L:n      End Page after Line   Line ###\n" .. - " Blank (Pad) Line  ##B or ##P     Blank(Pad) Lines   #B:n or #P:n\n" .. - " External Refrain   #r[ and #r]      In-Line Refrain    #R[ and #R]\n" .. - " Repeat Refrain   ##r or ##R    Duplicate Line n times   #D:n Line\n" .. - " Static Lines    #S[ and #s]      Single Static Line    #S: Line \n" .. - "Alternate Text   #A[ and #A]    Alt Line Repeat n Pages  #A:n Line \n" .. - "Comment Line    // Line       Block Comments    //[ and //] \n" .. - "Mark Verses     ##V        Override Title     #T: text\n\n" .. - "Optional comma delimited meta tags follow '//meta ' on 1st line" ------------------------------------------------------------------------------------------------------------------------- -- OBS PROPERTIES FUNCTION (See OBS Documentation) @@ -2731,7 +2736,7 @@ function script_defaults(settings) end ------------------------------------------------------------------------------------------------------------------------- --- Working function to return if a source is Valid to be included (excludes non-visible sources like Audio) +-- Working function to return if a source is Valid to be included as extra (excludes non-visible sources like Audio) ------------------------------------------------------------------------------------------------------------------------ function isValid(source) if source ~= nil then @@ -2953,6 +2958,7 @@ end --------- Return true if sourcename given is showing anywhere or on in the Active scene ------ --- +-- Function returns true if SourceName is Showing anywhere in Preview or Active function isShowing(sourceName) local source = obs.obs_get_source_by_name(sourceName) local showing = false @@ -2962,7 +2968,7 @@ function isShowing(sourceName) obs.obs_source_release(source) return showing end - +-- Function returns true if SourceName is visible in the Active Window function isActive(sourceName) local source = obs.obs_get_source_by_name(sourceName) local active = false @@ -2972,45 +2978,45 @@ function isActive(sourceName) obs.obs_source_release(source) return active end - +-- Function returns true if ANY of the sources are showing in OBS function anythingShowing() return isShowing(source_name) or isShowing(alternate_source_name) or isShowing(title_source_name) or isShowing(static_source_name) end - +-- Function returns true if Lyric Source is showing function sourceShowing() return isShowing(source_name) end - +-- Function returns true if Alternate Source is showing function alternateShowing() return isShowing(alternate_source_name) end - +-- Function returns true if Title Source is showing function titleShowing() return isShowing(title_source_name) end - +-- Function returns true if Static Source is showing function staticShowing() return isShowing(static_source_name) end - +-- Function returns true if ANY of the sources are ACTIVE in OBS function anythingActive() return isActive(source_name) or isActive(alternate_source_name) or isActive(title_source_name) or isActive(static_source_name) end - +-- Function returns true if Lyric Source is Active function sourceActive() return isActive(source_name) end - +-- Function returns true if Alternate Source is Active function alternateActive() return isActive(alternate_source_name) end - +-- Function returns true if Title Source is Active function titleActive() return isActive(title_source_name) end - +-- Function returns true if Static Source is Active function staticActive() return isActive(static_source_name) end @@ -3031,6 +3037,7 @@ end ---------------------------------------------------------------------------------------------------------- function get_hotkeys(hotkey_array, prefix, leader) +--Hotkey Translation Table local Translate = { ["NUMLOCK"] = "NumLock", ["NUMSLASH"] = "Num/", @@ -3070,6 +3077,7 @@ function get_hotkeys(hotkey_array, prefix, leader) item = obs.obs_data_array_item(hotkey_array, 0) local key = string.sub(obs.obs_data_get_string(item, "key"), 9) +-- Globally change any NUM to Num or MOUSE to Mouse if Translate[key] ~= nil then key = Translate[key] elseif string.sub(key, 1, 3) == "NUM" then @@ -3080,7 +3088,7 @@ function get_hotkeys(hotkey_array, prefix, leader) obs.obs_data_release(item) local val = prefix - if key ~= nil and key ~= "" then + if key ~= nil and key ~= "" then -- Add key modifiers Ctrl, Alt, Shift and Command val = val .. " " .. leader .. " " if obs.obs_data_get_bool(item, "control") then val = val .. "Ctrl + " @@ -3483,6 +3491,7 @@ end obs.obs_register_source(source_def) +-- Base64 Lyrics+ Icon description = [[
OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian
From 9d2dbed9aeb86e3b360370a14c9762dbe725fba5 Mon Sep 17 00:00:00 2001 From: wzaggle Date: Thu, 28 Oct 2021 10:07:59 -0600 Subject: [PATCH 080/105] Update lyrics+.lua Not finding any more issues after these few ultra minor nit-picks. I will keep looking until we are ready to release. I am sure there is something left. --- lyrics+.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 140f8f4..0167bbc 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -2857,7 +2857,6 @@ function script_save(settings) end obs.obs_data_set_array(settings, "extra_link_sources", extra_sources_array) obs.obs_data_array_release(extra_sources_array) - save_prepared(settings) end @@ -2893,7 +2892,7 @@ function script_load(settings) hotkey_p_p_id = obs.obs_hotkey_register_frontend("previous_prepared_hotkey", "Prepare Previous", prev_prepared) hotkey_save_array = obs.obs_data_get_array(settings, "previous_prepared_hotkey") - hotkey_p_p_key = get_hotkeys(hotkey_save_array, "Previous Prepared", "............") + hotkey_p_p_key = get_hotkeys(hotkey_save_array, "Previous Prepared", " ............") obs.obs_hotkey_load(hotkey_p_p_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) @@ -3284,7 +3283,7 @@ source_def.get_properties = function(data) obs.obs_properties_add_list( source_props, "songs", - "Song Directory", + "Song Directory", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) From 54cca875f74572f836ac8e86c9734a00e2535a1e Mon Sep 17 00:00:00 2001 From: amirchev Date: Sun, 31 Oct 2021 02:07:34 -0700 Subject: [PATCH 081/105] some nil checks for prepared_index --- lyrics+.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 0167bbc..04377f2 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -740,8 +740,8 @@ function read_source_opacity_clicked(props, p) end --------------------------------------------------------------------------------------------------------------------------- -- ADD LINKED Source Callback --- adds an extra linked source to the linked sources list. +-------------------------------------------------------------------------------------------------------------------------- +-- Callback to add an extra linked source to the linked sources list. -- Source must be text source, or have 'Color Correction' Filter applied ------------------------------------------------------------------------------------------------------------------------ function link_source_selected(props, prop, settings) @@ -1148,7 +1148,7 @@ function setSourceOpacity(sourceName, fadeBackground) dbg_method("set_Opacity") if sourceName ~= nil and sourceName ~= "" then if text_fade_enabled then - local settings = obs.obs_data_create() + local settings = obs.obs_data_create() if use100percent then -- try to honor preset maximum opacities obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero @@ -1172,7 +1172,7 @@ function setSourceOpacity(sourceName, fadeBackground) obs.obs_source_release(source) obs.obs_data_release(settings) else - dbg_inner("use on/off") + dbg_inner("use on/off") -- do preview scene item local sceneSource = obs.obs_frontend_get_current_preview_scene() local sceneObj = obs.obs_scene_from_source(sceneSource) @@ -1476,7 +1476,7 @@ function update_source_text() local next_prepared = "" if using_source then next_prepared = prepared_songs[prepared_index] -- plan to go to current prepared song - elseif prepared_index < #prepared_songs then + elseif prepared_index ~= nil and prepared_index < #prepared_songs then next_prepared = prepared_songs[prepared_index + 1] -- plan to go to next prepared song else if source_active then @@ -2266,7 +2266,11 @@ function update_monitor() elseif using_source then text = text .. "From Source: " .. load_scene .. "" else - text = text .. "Prepared Song: " .. prepared_index + local indexText = "N/A" + if prepared_index ~= nil then + indexText = prepared_index + end + text = text .. "Prepared Song: " .. indexText text = text .. " of " .. #prepared_songs .. "" From 777ff44e678af8b7272909677373643ab1764f92 Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:17:25 -0700 Subject: [PATCH 082/105] Update README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbe5d32..581b3bb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ # OBS-Lyrics Manage and display lyrics to any text source in your OBS scene. -## How to use +## Table of Contents +1. [Basic usage](#basic-usage) +2. [Random facts](#random-facts) +3. [Notation](#notation) +4. [The UI](#the-ui) +5. [That's it](#thats-it) + +## Basic usage 1. Download the script and open it with OBS. 2. Add some songs to the script and save them. 3. Select the text source for displaying the lyrics. Set the amount of lines to display, default is 2. Optionally, you can setup hotkeys to control the lyrics display. @@ -10,7 +17,7 @@ Manage and display lyrics to any text source in your OBS scene. 6. Advance lyrics as needed using the buttons or appropriate hotkeys. You can also advance to the next prepared song using hotkeys. 7. When you're finished with the current song, hide the lyrics and select the next song from the "Prepared Songs" list. -## Things to know +## Random facts - To display a specific song when a scene is activated, add a "Source" to the scene by clicking the + sign in the scene, adding a "Prepare Lyric" source, and selecting the song to open. - Use "Home" hotkey to return to the beginning of your prepared songs, perhaps after practicing the songs. - Continue clicking `Advance lyrics` after the end of a song to begin the next prepared song. @@ -182,6 +189,7 @@ Now hit the refrain again! ##R ``` +## The UI ### Song Title (filename) and Lyrics Information ![Title Lyrics](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Title%20Lyrics.gif) From 7d33c7139c70c283cf663292d284f49593720c0d Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:26:24 -0700 Subject: [PATCH 083/105] Update README.md --- README.md | 58 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 581b3bb..7c22687 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,15 @@ Manage and display lyrics to any text source in your OBS scene. - Prepared songs are stored in the Settings for the scene collection unless the option to use an external Prepared.dat file is selected in the Edit Prepared Songs subgroup. ## Notation -### Mark songs with with 'meta' tags for filtering on future selection (`//meta tag1, tag2, ... , tag n`) +### Mark songs with with 'meta' tags for filtering on future selection +(`//meta tag1, tag2, ... , tag n`) Using //meta tags on the __1st line__ of lyrics allows song files to be labeled as belonging to different genre. Example genre are Hymn, Contemporary, Gospel, Country, Blues, Spritual, Rock, Chant, Reggae, Metal, or HipHop. However, any tag can be used to organize and cross organize Lyric/Text files into categories. Other meta groups could be Call/Response or Scripture. Meta tags must match exactly, so the tag __*hymn*__ is different from the tag __*Hymn*__. Try it: ``` //meta Hymn, Blues, Spiritual ``` -### Single blank line/padding (`##P` or `##B`) +### Single blank line/padding +(`##P` or `##B`) Use on any line that you want to keep as an empty line (for line padding, etc.) Try it: ``` @@ -40,7 +42,8 @@ This is line 1 ##B This is line 3 ``` -### Multiple blank lines/padding (`#B:3` or `#P:3`) +### Multiple blank lines/padding +(`#B:3` or `#P:3`) Use on any line to create 3 empty lines (you may use any number) Try it: ``` @@ -48,7 +51,8 @@ This is line 1 #B:2 This is line 4!! ``` -### End the current page (`###`) +### End the current page +(`###`) Append `###` to the end of any line to end the current page with this line. Try it: ``` @@ -56,13 +60,15 @@ This line will show first This line will be the last one regardless of page size ### This line will be the only one on the 2nd page ### ``` -### Repeat line (`#D:3`) +### Repeat line +(`#D:3`) Duplicate a line multiple times. Try it: ``` #D3: Sing this line 3 times!!! ``` -### Set number of lines to be displayed per page (`#L:3`) +### Set number of lines to be displayed per page +(`#L:3`) Change the amount of lines displayed at one time throughout the same song. Try it: ``` @@ -74,7 +80,8 @@ But in the chorus, it needs to show all three! ``` -### Another way would be to use a page break + +Another way would be to use a page break Try it: ``` #L:3 @@ -84,13 +91,15 @@ But in the chorus, it needs to show all three! ``` -### Comment out text (`//`) +### Comment out text +(`//`) Use `//` to write a comment that will not display to your viewers. Try it: ``` We sing to you God //long pause/guitar solo after this ``` -### Comment out text (`//`) +### Comment out text +(`//`) Use `//` to write a comment that will not display to your viewers. Try it: ``` @@ -99,11 +108,16 @@ Try it: Note 3rd verse of this song is not Public Domain //] ``` -### Define refrain and show it right away (`#R[` and `#R]`) +### Define refrain and show it right away +(`#R[` and `#R]`) Use this notation to define a refrain that will be displayed right away as well. -### Define refrain but DON'T show it right away (`#r[` and `#r]`) + +### Define refrain but DON'T show it right away +(`#r[` and `#r]`) Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. -### Play refrain (`##R`) + +### Play refrain +(`##R`) Use this annotation to show where a refrain should be inserted. See above. Try it: ``` @@ -122,7 +136,8 @@ it will also continue with three lines per verse. Now hit the refrain again! ##R ``` -### Static Text (`#S[` and `#S]`) +### Static text +(`#S[` and `#S]`) Use this anotation to define a block of text lines shown in the selected Static Source that remain constant during the scene (no paging). Try it: ``` @@ -131,19 +146,22 @@ The song Amazing Grace was written by John Newton who was a former Slave Trader #S] ``` -### Single static text line (`#S: line`) +### Single static text line +(`#S: line`) Use this to define a simple single line of Static text Try it: ``` #S: The song Amazing Grace was written by John Newton who was a former Slave Trader ``` -### Override Title/filename (`#T: new title`) -Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename +### Override title/filename +(`#T: new title`) +Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename. Try it: ``` #T: How Great Thou Art (주하나님지으신모든세계) ``` -### Alternate Text Block (`#A[` and `#A]`) +### Alternate text block +(`#A[` and `#A]`) Use this annotation to mark additional verses or text to show and page in the selected Alternate Source. Note: The page length will be governed by text in the main block if it exists and its Text Source exists in the scene. The alternate block should have the same number of lines per page as the main block if both are used. @@ -159,13 +177,15 @@ Alguna vez estuve perdido, pero ahora me he encontrado Estuve ciego pero ahora veo #A] ``` -### Single Line Alternate Text repeated for n pages (`#A:n line`) +### Single line alternate text repeated for `n` pages +(`#A:n line`) Use this annotation to include a simple single line of Alternate Text to be used for n pages. Try it: ``` #A:2 This alaternate line shows for the next two pages of Lyrics. ``` -### Mark Verses (`##V`) +### Mark verses +(`##V`) Use this annotation to mark where new verses start. Verse number will be displayed in the monitor. Try it: From 26913a8a6f9ec8af33b8c63b4630e5f3093267ea Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:37:26 -0700 Subject: [PATCH 084/105] Update README.md --- README.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7c22687..496ffc0 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,23 @@ Manage and display lyrics to any text source in your OBS scene. 1. [Basic usage](#basic-usage) 2. [Random facts](#random-facts) 3. [Notation](#notation) + 1. [Mark songs with with 'meta' tags for filtering on future selection](#mark-songs-with-with-meta-tags-for-filtering-on-future-selection) + 2. [Single blank line/padding](#single-blank-linepadding) + 3. [Multiple blank lines/padding](#multiple-blank-linespadding) + 4. [End the current page](#end-the-current-page) + 5. [Repeat line](#repeat-line) + 6. [Set number of lines to be displayed per page](#set-number-of-lines-to-be-displayed-per-page) + 7. [Comment out line of text](#comment-out-line-of-text) + 8. [Comment out block of text](#comment-out-block-of-text) + 9. [Define refrain and show it right away](#define-refrain-and-show-it-right-away) + 10. [Define refrain but DON'T show it right away](#define-refrain-but-dont-show-it-right-away) + 11. [Play refrain](#play-refrain) + 12. [Static text](#static-text) + 13. [Single static text line](#single-static-text-line) + 14. [Override title/filename](override-titlefilename) + 15. [Alternate text block](#alternate-text-block) + 16. [Single line alternate text repeated for `n` pages](#single-line-alternate-text-repeated-for-n-pages) + 17. [Mark verses](#mark-verses) 4. [The UI](#the-ui) 5. [That's it](#thats-it) @@ -91,16 +108,16 @@ But in the chorus, it needs to show all three! ``` -### Comment out text +### Comment out line of text (`//`) Use `//` to write a comment that will not display to your viewers. Try it: ``` We sing to you God //long pause/guitar solo after this ``` -### Comment out text -(`//`) -Use `//` to write a comment that will not display to your viewers. +### Comment out block of text +(`//[` and `//]`) +Use these blocks to write a comment that will not display to your viewers. Try it: ``` //[ From 5e888c199c4a5dd4770d3135c8d233e44c420680 Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:40:04 -0700 Subject: [PATCH 085/105] Update README.md --- README.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 496ffc0..6da365f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Manage and display lyrics to any text source in your OBS scene. 5. Once you are ready to display the lyrics, select the song you'd like to display from the "Prepared Songs" list and click "Show/Hide Lyrics" button or the appropriate hotkey. 6. Advance lyrics as needed using the buttons or appropriate hotkeys. You can also advance to the next prepared song using hotkeys. 7. When you're finished with the current song, hide the lyrics and select the next song from the "Prepared Songs" list. - +[Top](#table-of-contents) ## Random facts - To display a specific song when a scene is activated, add a "Source" to the scene by clicking the + sign in the scene, adding a "Prepare Lyric" source, and selecting the song to open. - Use "Home" hotkey to return to the beginning of your prepared songs, perhaps after practicing the songs. @@ -41,7 +41,7 @@ Manage and display lyrics to any text source in your OBS scene. - Ensure a constant number of lines displayed using the checkbox, e.g., if the song ends and only one line is left, lyrics will be padded with blank lines to ensure you hava a minimum number of lines. (See Display Options) - A Monitor.htm file is created with current/next song, lyrics and alternate lyrics that can be docked in OBS with custom browser docks. Use Open Songs Folder button, open Monitor.htm with browser, copy url and paste it into an OBS custom browser dock. - Prepared songs are stored in the Settings for the scene collection unless the option to use an external Prepared.dat file is selected in the Edit Prepared Songs subgroup. - +[Top](#table-of-contents) ## Notation ### Mark songs with with 'meta' tags for filtering on future selection (`//meta tag1, tag2, ... , tag n`) @@ -50,6 +50,7 @@ Try it: ``` //meta Hymn, Blues, Spiritual ``` +[Top](#table-of-contents) ### Single blank line/padding (`##P` or `##B`) Use on any line that you want to keep as an empty line (for line padding, etc.) @@ -59,6 +60,7 @@ This is line 1 ##B This is line 3 ``` +[Top](#table-of-contents) ### Multiple blank lines/padding (`#B:3` or `#P:3`) Use on any line to create 3 empty lines (you may use any number) @@ -68,6 +70,7 @@ This is line 1 #B:2 This is line 4!! ``` +[Top](#table-of-contents) ### End the current page (`###`) Append `###` to the end of any line to end the current page with this line. @@ -77,6 +80,7 @@ This line will show first This line will be the last one regardless of page size ### This line will be the only one on the 2nd page ### ``` +[Top](#table-of-contents) ### Repeat line (`#D:3`) Duplicate a line multiple times. @@ -84,6 +88,7 @@ Try it: ``` #D3: Sing this line 3 times!!! ``` +[Top](#table-of-contents) ### Set number of lines to be displayed per page (`#L:3`) Change the amount of lines displayed at one time throughout the same song. @@ -108,6 +113,7 @@ But in the chorus, it needs to show all three! ``` +[Top](#table-of-contents) ### Comment out line of text (`//`) Use `//` to write a comment that will not display to your viewers. @@ -115,6 +121,7 @@ Try it: ``` We sing to you God //long pause/guitar solo after this ``` +[Top](#table-of-contents) ### Comment out block of text (`//[` and `//]`) Use these blocks to write a comment that will not display to your viewers. @@ -125,14 +132,15 @@ Try it: Note 3rd verse of this song is not Public Domain //] ``` +[Top](#table-of-contents) ### Define refrain and show it right away (`#R[` and `#R]`) Use this notation to define a refrain that will be displayed right away as well. - +[Top](#table-of-contents) ### Define refrain but DON'T show it right away (`#r[` and `#r]`) Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. - +[Top](#table-of-contents) ### Play refrain (`##R`) Use this annotation to show where a refrain should be inserted. See above. @@ -153,6 +161,7 @@ it will also continue with three lines per verse. Now hit the refrain again! ##R ``` +[Top](#table-of-contents) ### Static text (`#S[` and `#S]`) Use this anotation to define a block of text lines shown in the selected Static Source that remain constant during the scene (no paging). @@ -163,6 +172,7 @@ The song Amazing Grace was written by John Newton who was a former Slave Trader #S] ``` +[Top](#table-of-contents) ### Single static text line (`#S: line`) Use this to define a simple single line of Static text @@ -170,6 +180,7 @@ Try it: ``` #S: The song Amazing Grace was written by John Newton who was a former Slave Trader ``` +[Top](#table-of-contents) ### Override title/filename (`#T: new title`) Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename. @@ -177,6 +188,7 @@ Try it: ``` #T: How Great Thou Art (주하나님지으신모든세계) ``` +[Top](#table-of-contents) ### Alternate text block (`#A[` and `#A]`) Use this annotation to mark additional verses or text to show and page in the selected Alternate Source. @@ -194,6 +206,7 @@ Alguna vez estuve perdido, pero ahora me he encontrado Estuve ciego pero ahora veo #A] ``` + [Top](#table-of-contents) ### Single line alternate text repeated for `n` pages (`#A:n line`) Use this annotation to include a simple single line of Alternate Text to be used for n pages. @@ -201,6 +214,7 @@ Try it: ``` #A:2 This alaternate line shows for the next two pages of Lyrics. ``` +[Top](#table-of-contents) ### Mark verses (`##V`) @@ -225,38 +239,38 @@ it will also continue with three lines per verse. Now hit the refrain again! ##R ``` - +[Top](#table-of-contents) ## The UI ### Song Title (filename) and Lyrics Information ![Title Lyrics](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Title%20Lyrics.gif) The song Title is also used as a filename to store the lyrics. If the text of the title is not a valid OS filename then the filename will be encoded to create a valid filename. Providing a valid filename for this field instead of a song Title the actual Title can be included using the ##T markup. Song lyrics can be added in the dialog, saved, and deleted. Songs can also be opened and edited with the default system text editor. - +[Top](#table-of-contents) ### Manage Prepared Songs/Text ![image-20211022010733511](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Manage%20Prepared.gif) Songs saved in the Song Title and Lyrics Information can be selected in the Manage Prepared section to be added to the Prepared Songs/Text list. Selecting a song from this Prepared List loads the contents of the Song/Text into the selected Text Sources. If songs are marked with //meta tags, they can be filtered by specifying one or more tags and refreshing the directory. Prepared songs can be edited as a list where they can be individually ordered or deleted. *(New songs can be typed into the edit list manually if they exist in the directory exactly as typed)* - +[Top](#table-of-contents) ### Lyric Control Buttons Control Buttons perform the seven different functions of the Lyrics Script. Additionally, Hot Keys can be assigned within OBS to perform these same functions. ![img](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Lyric%20Control%20Buttons.gif) - +[Top](#table-of-contents) ### Display Options ![image-20211021232744449](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Display%20Options.gif) Enabling Fade Transitions will offer additional options to cause lyrics and other sources to fade to transparent before changing to a different page and fading back to opaque. The Use 0-100% option is set by default. Unchecking this option will cause Lyrics to restore faded sources back to their "marked" original opacity levels if specific graphic effects have been applied to text. Background color fading is optional and can be further configured per text source if enabled. - +[Top](#table-of-contents) ### Text Sources in Scenes ![image-20211021234545740](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Text%20Sources.gif) Lyrics will modify the text content of existing text sources within OBS and a given scene. These Text, Title, Alternate and Static text sources are defined in the Text Sources in Scenes section. New text sources added to OBS while the script properties window is open, can be included by clicking the Refresh All Sources button. Additional visual sources can be added and linked to show/hide/fade with the Title and Static text sources if desired, such as with a background image for Lyrics, etc. Optionally, these sources can be faded with the Lyrics and Alternate text. - +[Top](#table-of-contents) ### Lyrics Monitor Browser Dock ![image-20211021235826735](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/monitor.gif) @@ -272,7 +286,7 @@ A Lyrics Monitor Page updated in HTML is available in the Songs Folder as Monito - The Next Prepared Song/Text file Note: Red backgrounds in the Monitor Page indicate lyrics are not currently visible, or the selected text sources do not exist in the current Active scene. - +[Top](#table-of-contents) ## That's it Please post any bugs or feature requests here or to the OBS forum. From a3b528a39634e2a471da5a97e080540c9be27ace Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:43:10 -0700 Subject: [PATCH 086/105] Update README.md --- README.md | 63 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6da365f..54493c5 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ Manage and display lyrics to any text source in your OBS scene. 4. Select a song from the song directory and click "Prepare Song". Do that to as many songs as you will need for the session. 5. Once you are ready to display the lyrics, select the song you'd like to display from the "Prepared Songs" list and click "Show/Hide Lyrics" button or the appropriate hotkey. 6. Advance lyrics as needed using the buttons or appropriate hotkeys. You can also advance to the next prepared song using hotkeys. -7. When you're finished with the current song, hide the lyrics and select the next song from the "Prepared Songs" list. -[Top](#table-of-contents) +7. When you're finished with the current song, hide the lyrics and select the next song from the "Prepared Songs" list. + +[Back to Top](#table-of-contents) ## Random facts - To display a specific song when a scene is activated, add a "Source" to the scene by clicking the + sign in the scene, adding a "Prepare Lyric" source, and selecting the song to open. - Use "Home" hotkey to return to the beginning of your prepared songs, perhaps after practicing the songs. @@ -41,7 +42,8 @@ Manage and display lyrics to any text source in your OBS scene. - Ensure a constant number of lines displayed using the checkbox, e.g., if the song ends and only one line is left, lyrics will be padded with blank lines to ensure you hava a minimum number of lines. (See Display Options) - A Monitor.htm file is created with current/next song, lyrics and alternate lyrics that can be docked in OBS with custom browser docks. Use Open Songs Folder button, open Monitor.htm with browser, copy url and paste it into an OBS custom browser dock. - Prepared songs are stored in the Settings for the scene collection unless the option to use an external Prepared.dat file is selected in the Edit Prepared Songs subgroup. -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ## Notation ### Mark songs with with 'meta' tags for filtering on future selection (`//meta tag1, tag2, ... , tag n`) @@ -50,7 +52,7 @@ Try it: ``` //meta Hymn, Blues, Spiritual ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Single blank line/padding (`##P` or `##B`) Use on any line that you want to keep as an empty line (for line padding, etc.) @@ -60,7 +62,7 @@ This is line 1 ##B This is line 3 ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Multiple blank lines/padding (`#B:3` or `#P:3`) Use on any line to create 3 empty lines (you may use any number) @@ -70,7 +72,7 @@ This is line 1 #B:2 This is line 4!! ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### End the current page (`###`) Append `###` to the end of any line to end the current page with this line. @@ -80,7 +82,7 @@ This line will show first This line will be the last one regardless of page size ### This line will be the only one on the 2nd page ### ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Repeat line (`#D:3`) Duplicate a line multiple times. @@ -88,7 +90,7 @@ Try it: ``` #D3: Sing this line 3 times!!! ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Set number of lines to be displayed per page (`#L:3`) Change the amount of lines displayed at one time throughout the same song. @@ -113,7 +115,7 @@ But in the chorus, it needs to show all three! ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Comment out line of text (`//`) Use `//` to write a comment that will not display to your viewers. @@ -121,7 +123,7 @@ Try it: ``` We sing to you God //long pause/guitar solo after this ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Comment out block of text (`//[` and `//]`) Use these blocks to write a comment that will not display to your viewers. @@ -132,15 +134,17 @@ Try it: Note 3rd verse of this song is not Public Domain //] ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Define refrain and show it right away (`#R[` and `#R]`) Use this notation to define a refrain that will be displayed right away as well. -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ### Define refrain but DON'T show it right away (`#r[` and `#r]`) Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ### Play refrain (`##R`) Use this annotation to show where a refrain should be inserted. See above. @@ -161,7 +165,7 @@ it will also continue with three lines per verse. Now hit the refrain again! ##R ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Static text (`#S[` and `#S]`) Use this anotation to define a block of text lines shown in the selected Static Source that remain constant during the scene (no paging). @@ -172,7 +176,7 @@ The song Amazing Grace was written by John Newton who was a former Slave Trader #S] ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Single static text line (`#S: line`) Use this to define a simple single line of Static text @@ -180,7 +184,7 @@ Try it: ``` #S: The song Amazing Grace was written by John Newton who was a former Slave Trader ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Override title/filename (`#T: new title`) Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename. @@ -188,7 +192,7 @@ Try it: ``` #T: How Great Thou Art (주하나님지으신모든세계) ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Alternate text block (`#A[` and `#A]`) Use this annotation to mark additional verses or text to show and page in the selected Alternate Source. @@ -206,7 +210,7 @@ Alguna vez estuve perdido, pero ahora me he encontrado Estuve ciego pero ahora veo #A] ``` - [Top](#table-of-contents) + [Back to Top](#table-of-contents) ### Single line alternate text repeated for `n` pages (`#A:n line`) Use this annotation to include a simple single line of Alternate Text to be used for n pages. @@ -214,7 +218,7 @@ Try it: ``` #A:2 This alaternate line shows for the next two pages of Lyrics. ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Mark verses (`##V`) @@ -239,38 +243,42 @@ it will also continue with three lines per verse. Now hit the refrain again! ##R ``` -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ## The UI ### Song Title (filename) and Lyrics Information ![Title Lyrics](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Title%20Lyrics.gif) The song Title is also used as a filename to store the lyrics. If the text of the title is not a valid OS filename then the filename will be encoded to create a valid filename. Providing a valid filename for this field instead of a song Title the actual Title can be included using the ##T markup. Song lyrics can be added in the dialog, saved, and deleted. Songs can also be opened and edited with the default system text editor. -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ### Manage Prepared Songs/Text ![image-20211022010733511](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Manage%20Prepared.gif) Songs saved in the Song Title and Lyrics Information can be selected in the Manage Prepared section to be added to the Prepared Songs/Text list. Selecting a song from this Prepared List loads the contents of the Song/Text into the selected Text Sources. If songs are marked with //meta tags, they can be filtered by specifying one or more tags and refreshing the directory. Prepared songs can be edited as a list where they can be individually ordered or deleted. *(New songs can be typed into the edit list manually if they exist in the directory exactly as typed)* -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ### Lyric Control Buttons Control Buttons perform the seven different functions of the Lyrics Script. Additionally, Hot Keys can be assigned within OBS to perform these same functions. ![img](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Lyric%20Control%20Buttons.gif) -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ### Display Options ![image-20211021232744449](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Display%20Options.gif) Enabling Fade Transitions will offer additional options to cause lyrics and other sources to fade to transparent before changing to a different page and fading back to opaque. The Use 0-100% option is set by default. Unchecking this option will cause Lyrics to restore faded sources back to their "marked" original opacity levels if specific graphic effects have been applied to text. Background color fading is optional and can be further configured per text source if enabled. -[Top](#table-of-contents) +[Back to Top](#table-of-contents) ### Text Sources in Scenes ![image-20211021234545740](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Text%20Sources.gif) Lyrics will modify the text content of existing text sources within OBS and a given scene. These Text, Title, Alternate and Static text sources are defined in the Text Sources in Scenes section. New text sources added to OBS while the script properties window is open, can be included by clicking the Refresh All Sources button. Additional visual sources can be added and linked to show/hide/fade with the Title and Static text sources if desired, such as with a background image for Lyrics, etc. Optionally, these sources can be faded with the Lyrics and Alternate text. -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ### Lyrics Monitor Browser Dock ![image-20211021235826735](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/monitor.gif) @@ -286,7 +294,8 @@ A Lyrics Monitor Page updated in HTML is available in the Songs Folder as Monito - The Next Prepared Song/Text file Note: Red backgrounds in the Monitor Page indicate lyrics are not currently visible, or the selected text sources do not exist in the current Active scene. -[Top](#table-of-contents) + +[Back to Top](#table-of-contents) ## That's it Please post any bugs or feature requests here or to the OBS forum. @@ -294,3 +303,5 @@ Feel free to make pull requests for any features you implement yourself, I'll be amirchev and DC Strato with significant contributions from taxilian + +[Back to Top](#table-of-contents) From 40bbd004eb8b203e1d1d12312ec02d6e60d5dbd0 Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:44:12 -0700 Subject: [PATCH 087/105] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54493c5..76b2da9 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Manage and display lyrics to any text source in your OBS scene. 11. [Play refrain](#play-refrain) 12. [Static text](#static-text) 13. [Single static text line](#single-static-text-line) - 14. [Override title/filename](override-titlefilename) + 14. [Override title/filename](#override-titlefilename) 15. [Alternate text block](#alternate-text-block) 16. [Single line alternate text repeated for `n` pages](#single-line-alternate-text-repeated-for-n-pages) 17. [Mark verses](#mark-verses) From 9aff44a28ea701c11010fa51b5ea504c870d0017 Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:45:00 -0700 Subject: [PATCH 088/105] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 76b2da9..9673f8c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Manage and display lyrics to any text source in your OBS scene. 1. [Basic usage](#basic-usage) 2. [Random facts](#random-facts) 3. [Notation](#notation) - 1. [Mark songs with with 'meta' tags for filtering on future selection](#mark-songs-with-with-meta-tags-for-filtering-on-future-selection) + 1. [Mark songs with with `meta` tags for filtering on future selection](#mark-songs-with-with-meta-tags-for-filtering-on-future-selection) 2. [Single blank line/padding](#single-blank-linepadding) 3. [Multiple blank lines/padding](#multiple-blank-linespadding) 4. [End the current page](#end-the-current-page) @@ -45,7 +45,7 @@ Manage and display lyrics to any text source in your OBS scene. [Back to Top](#table-of-contents) ## Notation -### Mark songs with with 'meta' tags for filtering on future selection +### Mark songs with with `meta` tags for filtering on future selection (`//meta tag1, tag2, ... , tag n`) Using //meta tags on the __1st line__ of lyrics allows song files to be labeled as belonging to different genre. Example genre are Hymn, Contemporary, Gospel, Country, Blues, Spritual, Rock, Chant, Reggae, Metal, or HipHop. However, any tag can be used to organize and cross organize Lyric/Text files into categories. Other meta groups could be Call/Response or Scripture. Meta tags must match exactly, so the tag __*hymn*__ is different from the tag __*Hymn*__. Try it: From 0f13b94f4c4ea2d2115a30b8314b3e41fa032d2e Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:46:06 -0700 Subject: [PATCH 089/105] Update README.md --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 9673f8c..07c0350 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Manage and display lyrics to any text source in your OBS scene. ## Notation ### Mark songs with with `meta` tags for filtering on future selection (`//meta tag1, tag2, ... , tag n`) + Using //meta tags on the __1st line__ of lyrics allows song files to be labeled as belonging to different genre. Example genre are Hymn, Contemporary, Gospel, Country, Blues, Spritual, Rock, Chant, Reggae, Metal, or HipHop. However, any tag can be used to organize and cross organize Lyric/Text files into categories. Other meta groups could be Call/Response or Scripture. Meta tags must match exactly, so the tag __*hymn*__ is different from the tag __*Hymn*__. Try it: ``` @@ -55,6 +56,7 @@ Try it: [Back to Top](#table-of-contents) ### Single blank line/padding (`##P` or `##B`) + Use on any line that you want to keep as an empty line (for line padding, etc.) Try it: ``` @@ -65,6 +67,7 @@ This is line 3 [Back to Top](#table-of-contents) ### Multiple blank lines/padding (`#B:3` or `#P:3`) + Use on any line to create 3 empty lines (you may use any number) Try it: ``` @@ -75,6 +78,7 @@ This is line 4!! [Back to Top](#table-of-contents) ### End the current page (`###`) + Append `###` to the end of any line to end the current page with this line. Try it: ``` @@ -85,6 +89,7 @@ This line will be the only one on the 2nd page ### [Back to Top](#table-of-contents) ### Repeat line (`#D:3`) + Duplicate a line multiple times. Try it: ``` @@ -93,6 +98,7 @@ Try it: [Back to Top](#table-of-contents) ### Set number of lines to be displayed per page (`#L:3`) + Change the amount of lines displayed at one time throughout the same song. Try it: ``` @@ -118,6 +124,7 @@ all three! [Back to Top](#table-of-contents) ### Comment out line of text (`//`) + Use `//` to write a comment that will not display to your viewers. Try it: ``` @@ -126,6 +133,7 @@ We sing to you God //long pause/guitar solo after this [Back to Top](#table-of-contents) ### Comment out block of text (`//[` and `//]`) + Use these blocks to write a comment that will not display to your viewers. Try it: ``` @@ -137,16 +145,19 @@ Try it: [Back to Top](#table-of-contents) ### Define refrain and show it right away (`#R[` and `#R]`) + Use this notation to define a refrain that will be displayed right away as well. [Back to Top](#table-of-contents) ### Define refrain but DON'T show it right away (`#r[` and `#r]`) + Used in the same way as `#R[` and `#R]`, but the refrain is not shown in the beginning. It will only be displayed when `##R` or `##r` is called. [Back to Top](#table-of-contents) ### Play refrain (`##R`) + Use this annotation to show where a refrain should be inserted. See above. Try it: ``` @@ -168,6 +179,7 @@ Now hit the refrain again! [Back to Top](#table-of-contents) ### Static text (`#S[` and `#S]`) + Use this anotation to define a block of text lines shown in the selected Static Source that remain constant during the scene (no paging). Try it: ``` @@ -179,6 +191,7 @@ who was a former Slave Trader [Back to Top](#table-of-contents) ### Single static text line (`#S: line`) + Use this to define a simple single line of Static text Try it: ``` @@ -187,6 +200,7 @@ Try it: [Back to Top](#table-of-contents) ### Override title/filename (`#T: new title`) + Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename. Try it: ``` @@ -195,6 +209,7 @@ Try it: [Back to Top](#table-of-contents) ### Alternate text block (`#A[` and `#A]`) + Use this annotation to mark additional verses or text to show and page in the selected Alternate Source. Note: The page length will be governed by text in the main block if it exists and its Text Source exists in the scene. The alternate block should have the same number of lines per page as the main block if both are used. @@ -213,6 +228,7 @@ Estuve ciego pero ahora veo [Back to Top](#table-of-contents) ### Single line alternate text repeated for `n` pages (`#A:n line`) + Use this annotation to include a simple single line of Alternate Text to be used for n pages. Try it: ``` From 62060a26b1ca20fec27b0090c6d16754aab6e5cc Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:47:14 -0700 Subject: [PATCH 090/105] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 07c0350..d5317af 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Manage and display lyrics to any text source in your OBS scene. ## Table of Contents 1. [Basic usage](#basic-usage) -2. [Random facts](#random-facts) +2. [Useful facts](#useful-facts) 3. [Notation](#notation) 1. [Mark songs with with `meta` tags for filtering on future selection](#mark-songs-with-with-meta-tags-for-filtering-on-future-selection) 2. [Single blank line/padding](#single-blank-linepadding) @@ -35,7 +35,7 @@ Manage and display lyrics to any text source in your OBS scene. 7. When you're finished with the current song, hide the lyrics and select the next song from the "Prepared Songs" list. [Back to Top](#table-of-contents) -## Random facts +## Useful facts - To display a specific song when a scene is activated, add a "Source" to the scene by clicking the + sign in the scene, adding a "Prepare Lyric" source, and selecting the song to open. - Use "Home" hotkey to return to the beginning of your prepared songs, perhaps after practicing the songs. - Continue clicking `Advance lyrics` after the end of a song to begin the next prepared song. From 066ecd3d598425a4da3549baaf7d61e96a526106 Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Sun, 31 Oct 2021 02:48:07 -0700 Subject: [PATCH 091/105] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5317af..39cdaf7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OBS-Lyrics -Manage and display lyrics to any text source in your OBS scene. +An OBS Lua script for managing and displaying lyrics to any text source in your OBS scene. ## Table of Contents 1. [Basic usage](#basic-usage) From 12fad73adf7ef23dc5b1767ab7d54398b03cca5a Mon Sep 17 00:00:00 2001 From: wzaggle Date: Sun, 31 Oct 2021 14:02:32 -0600 Subject: [PATCH 092/105] A few small updates to keep monitor accurate as possible Update Monitor in Set Opacity does not seem necessary. --- lyrics+.lua | 55 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 0167bbc..e85db7f 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -75,7 +75,7 @@ verses = {} -- MISC FLAGS AND TABLES page_index = 0 -- current page of lyrics being displayed -prepared_index = 0 -- TODO: avoid setting prepared_index directly, use prepare_selected +prepared_index = 1 -- TODO: avoid setting prepared_index directly, use prepare_selected song_directory = {} -- holds list of current songs from song directory TODO: Multiple Song Books (Directories) prepared_songs = {} -- holds pre-prepared list of songs to use @@ -303,7 +303,6 @@ function next_prepared(pressed) if using_source then using_source = false dbg_custom("do current prepared") - print("INDEX: " .. prepared_index) prepare_selected(prepared_songs[prepared_index]) -- if source load song showing then goto curren prepared song return end @@ -1132,11 +1131,11 @@ function prepare_selected(name) transition_lyric_text(using_source) else -- hide everything if unable to prepare song - -- TODO: clear lyrics entirely after text is hidden - set_text_visibility(TEXT_HIDDEN) + -- Read current text in sources so monitor is correct + get_text() + all_sources_fade = true + set_text_visibility(TEXT_HIDE) end - - --update_source_text() return true end @@ -1184,7 +1183,7 @@ function setSourceOpacity(sourceName, fadeBackground) obs.obs_sceneitem_set_visible(sceneItem, false) end end - update_monitor() +-- update_monitor() end end @@ -1309,14 +1308,15 @@ function set_text_visibility(end_status) if text_status == end_status then return end + print("text_status: " .. text_status) if end_status == TEXT_HIDE then text_opacity = 0 - text_status = end_status + text_status = TEXT_HIDDEN apply_source_opacity() return elseif end_status == TEXT_SHOW then text_opacity = 100 - text_status = end_status + text_status = TEXT_VISIBLE all_sources_fade = true -- prevent orphaned title/static if link is removed when hidden apply_source_opacity() return @@ -2236,6 +2236,41 @@ function save_prepared() return true end +-- GET_TEXT Function +-- Gets current ACTUAL text contained in the text sources. This allows the monitor to display whatever is +-- really there when starting up. +-- +function get_text() + local source = obs.obs_get_source_by_name(source_name) + if source ~= nil then + local settings = obs.obs_source_get_settings(source) + if settings ~= nil then + mon_lyric = obs.obs_data_get_string(settings,"text"):gsub("\n", "
• ") + obs.obs_data_release(settings) + end + obs.obs_source_release(source) + end + local alt_source = obs.obs_get_source_by_name(alternate_source_name) + if alt_source ~= nil then + local settings = obs.obs_source_get_settings(alt_source) + if settings ~= nil then + mon_alt = obs.obs_data_get_string(settings,"text"):gsub("\n", "
• ") + obs.obs_data_release(settings) + end + obs.obs_source_release(alt_source) + end + local title_source = obs.obs_get_source_by_name(title_source_name) + if title_source ~= nil then + local settings = obs.obs_source_get_settings(title_source) + if settings ~= nil then + mon_song = obs.obs_data_get_string(settings,"text") + obs.obs_data_release(settings) + end + obs.obs_source_release(title_source) + end +end + + ------------------------------------------------------------------------------------------------------------------------- -- UPDATE MONITOR @@ -2244,6 +2279,7 @@ end ------------------------------------------------------------------------------------------------------------------------- function update_monitor() dbg_method("update_monitor") + get_text() -- read actual text in sources local tableback = "black" local text = "" text = text .. "" @@ -3349,7 +3385,6 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) -print(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page dbg_bool("Active:", source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS From d15dc263e91987eb5cdd2d48addc0bae376d8239 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 31 Oct 2021 14:11:12 -0600 Subject: [PATCH 093/105] Update README.md OVerride is ONLY for title. When used, the Title/filename in properties becomes the filename, and the ##T text becomes the Title. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 39cdaf7..7eea21e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ An OBS Lua script for managing and displaying lyrics to any text source in your 11. [Play refrain](#play-refrain) 12. [Static text](#static-text) 13. [Single static text line](#single-static-text-line) - 14. [Override title/filename](#override-titlefilename) + 14. [Override title](#override-titlefilename) 15. [Alternate text block](#alternate-text-block) 16. [Single line alternate text repeated for `n` pages](#single-line-alternate-text-repeated-for-n-pages) 17. [Mark verses](#mark-verses) @@ -198,7 +198,7 @@ Try it: #S: The song Amazing Grace was written by John Newton who was a former Slave Trader ``` [Back to Top](#table-of-contents) -### Override title/filename +### Override title (`#T: new title`) Use this to specifically define the song title. This is useful if title has special characters, not valid as a filename. From 86d2f9d802fbf062548d2cad2bc973b80b1c4bd3 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 31 Oct 2021 14:13:37 -0600 Subject: [PATCH 094/105] Update README.md ##T should be #T: --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7eea21e..55665dc 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ Now hit the refrain again! ![Title Lyrics](https://github.com/amirchev/OBS-Lyrics/blob/cleanup/images/Title%20Lyrics.gif) -The song Title is also used as a filename to store the lyrics. If the text of the title is not a valid OS filename then the filename will be encoded to create a valid filename. Providing a valid filename for this field instead of a song Title the actual Title can be included using the ##T markup. Song lyrics can be added in the dialog, saved, and deleted. Songs can also be opened and edited with the default system text editor. +The song Title is also used as a filename to store the lyrics. If the text of the title is not a valid OS filename then the filename will be encoded to create a valid filename. Providing a valid filename for this field instead of a song Title the actual Title can be included using the #T: markup. Song lyrics can be added in the dialog, saved, and deleted. Songs can also be opened and edited with the default system text editor. [Back to Top](#table-of-contents) ### Manage Prepared Songs/Text From 00d5551f8fb24f87d39d51183ca2d31599ecf6af Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 1 Nov 2021 02:47:45 -0600 Subject: [PATCH 095/105] Create hymns.zip --- hymns/hymns.zip | 1 + 1 file changed, 1 insertion(+) create mode 100644 hymns/hymns.zip diff --git a/hymns/hymns.zip b/hymns/hymns.zip new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/hymns/hymns.zip @@ -0,0 +1 @@ + From aa36072e0a142ed7f602661f0c2c7cfde6e2c950 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 1 Nov 2021 02:50:24 -0600 Subject: [PATCH 096/105] Delete hymns.zip --- hymns/hymns.zip | 1 - 1 file changed, 1 deletion(-) delete mode 100644 hymns/hymns.zip diff --git a/hymns/hymns.zip b/hymns/hymns.zip deleted file mode 100644 index 8b13789..0000000 --- a/hymns/hymns.zip +++ /dev/null @@ -1 +0,0 @@ - From df18381114e3fbbec02e0b66ba4454126ec2ddbc Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 1 Nov 2021 02:53:41 -0600 Subject: [PATCH 097/105] Create ... --- hymns/... | 1 + 1 file changed, 1 insertion(+) create mode 100644 hymns/... diff --git a/hymns/... b/hymns/... new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/hymns/... @@ -0,0 +1 @@ + From 69f3a7a02aa72d345bd8cff9e6ed6477826fcf6c Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 1 Nov 2021 02:54:17 -0600 Subject: [PATCH 098/105] Add files via upload --- hymns/hymns.zip | Bin 0 -> 217359 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hymns/hymns.zip diff --git a/hymns/hymns.zip b/hymns/hymns.zip new file mode 100644 index 0000000000000000000000000000000000000000..0880f737f847c59fb50d20df9b09ac522cf9f6c8 GIT binary patch literal 217359 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 NRb-k-P&xVd6 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 Q5)-m8&*NEe8mHd#KgIMl_y7O^ literal 0 HcmV?d00001 From c11ea120b9b3d8a65f0882bbce1002532c353658 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 1 Nov 2021 15:56:14 -0600 Subject: [PATCH 099/105] Delete hymns.zip --- hymns/hymns.zip | Bin 217359 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 hymns/hymns.zip diff --git a/hymns/hymns.zip b/hymns/hymns.zip deleted file mode 100644 index 0880f737f847c59fb50d20df9b09ac522cf9f6c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217359 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 NRb-k-P&xVd6 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 Q5)-m8&*NEe8mHd#KgIMl_y7O^ From 5656a1fe6b5e8782518c5ff44544e4df950c732d Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Mon, 1 Nov 2021 15:56:41 -0600 Subject: [PATCH 100/105] Add files via upload --- hymns/FWS.zip | Bin 0 -> 27152 bytes hymns/UMH.zip | Bin 0 -> 215734 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 hymns/FWS.zip create mode 100644 hymns/UMH.zip diff --git a/hymns/FWS.zip b/hymns/FWS.zip new file mode 100644 index 0000000000000000000000000000000000000000..f0f5e69186eed87d8fd6b82bddd097a298e09b73 GIT binary patch literal 27152 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�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 OS>_j7n2*1o_0xaechJ57 literal 0 HcmV?d00001 diff --git a/hymns/UMH.zip b/hymns/UMH.zip new file mode 100644 index 0000000000000000000000000000000000000000..78781da102a4006a759b942e9efc9ce78642d839 GIT binary patch literal 215734 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

-SR)6^tQ{Hi{b`VWOj~DFTAGP$M(RN`IK1 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

" + text = text .. "From Source: " .. load_scene .. "" else local indexText = "N/A" if prepared_index ~= nil then @@ -2313,14 +2310,14 @@ function update_monitor() end text = text .. - "
Lyric Page: " - local pages = (#lyrics == 0) and 0 or #lyrics-1 + "
Lyric Page: " + local pages = (#lyrics == 0) and 0 or #lyrics-1 if page_index < #lyrics then text = text .. page_index.. " of " .. pages .. "
" else text = text .. "Blank
" end - + if #verses ~= nil and mon_verse > 0 then text = text .. @@ -2451,19 +2448,20 @@ end -------- -- A function named script_properties defines the properties that the user --- can change for the entire script module. +-- can change for the entire script module. function script_description() return description end - + ------------------------------------------------------------------------------------------------------------------------- -- OBS PROPERTIES FUNCTION (See OBS Documentation) ------------------------------------------------------------------------------------------------------------------------ function script_properties() dbg_method("script_properties") editVisSet = false + use_meta_tags = false script_props = obs.obs_properties_create() obs.obs_properties_add_button(script_props, "expand_all_button", "▲- HIDE ALL GROUPS -▲", expand_all_groups) ----------- @@ -2509,7 +2507,7 @@ function script_properties() obs.obs_properties_add_button(gp, "filter_songs_button", "Filter Titles by Meta Tags", filter_songs_clicked) local gps = obs.obs_properties_create() obs.obs_properties_add_text(gps, "prop_edit_metatags", "Filter MetaTags", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_button(gps, "dir_refresh", "Refresh Directory", refresh_directory_button_clicked) + obs.obs_properties_add_button(gps, "dir_refresh", "Filter by Tags (Could take time to complete)", refresh_directory_button_clicked) local meta_group_prop = obs.obs_properties_add_group(gp, "meta", " Filter Songs/Text", obs.OBS_GROUP_NORMAL, gps) gps = obs.obs_properties_create() local prepare_prop = @@ -2524,12 +2522,12 @@ function script_properties() for _, name in ipairs(prepared_songs) do obs.obs_property_list_add_string(prepare_prop, name, name) end - obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") obs.obs_property_set_modified_callback(prepare_prop, prepare_selection_made) local count = obs.obs_property_list_item_count(prepare_prop) if count > 1 then obs.obs_property_set_description( prepare_prop, "Prepared (" .. count-1 .. ")") - end + end obs.obs_properties_add_button(gps, "prop_clear_button", "Clear All Prepared Songs/Text", clear_prepared_clicked) obs.obs_properties_add_button(gps, "prop_manage_button", "Edit Prepared List", edit_prepared_clicked) local eps = obs.obs_properties_create() @@ -2553,7 +2551,7 @@ function script_properties() ) local saveExtProp = obs.obs_properties_add_bool(eps, "saveExternal", "Use external Prepared.dat file ") obs.obs_property_set_modified_callback(saveExtProp, reLoadPrepared) - + obs.obs_properties_add_group(gp, "prep_grp", "Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gps) obs.obs_properties_add_group(script_props, "mng_grp", "Manage Prepared Songs/Text", obs.OBS_GROUP_NORMAL, gp) ------------------ @@ -2589,21 +2587,21 @@ function script_properties() transition_prop, "Use with Studio Mode, duplicate sources, and OBS source transitions (beta)" ) - - local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable Fade Transitions") + + local fade_prop = obs.obs_properties_add_bool(gp, "text_fade_enabled", "Enable Fade Transitions") obs.obs_property_set_modified_callback(fade_prop, change_fade_property) local fp1 = obs.obs_properties_add_int_slider(gp, "text_fade_speed", "Fade Speed", 1, 10, 1) local fp2 = obs.obs_properties_add_bool(gp,"use100percent", "Use 0-100% opacity for fades") - local fp3 = obs.obs_properties_add_bool(gp,"allowBackFade", "Enable Background Fading") - obs.obs_property_set_modified_callback(fp2, change_100percent_property) - obs.obs_property_set_modified_callback(fp3, change_back_fade_property) + local fp3 = obs.obs_properties_add_bool(gp,"allowBackFade", "Enable Background Fading") + obs.obs_property_set_modified_callback(fp2, change_100percent_property) + obs.obs_property_set_modified_callback(fp3, change_back_fade_property) local oprefprop = obs.obs_properties_add_button(gp, "refreshOP", "Mark Max Opacity for Source Fades", read_source_opacity_clicked) obs.obs_properties_add_group(script_props, "disp_grp", "Display Options", obs.OBS_GROUP_NORMAL, gp) - + ------------- obs.obs_properties_add_button(script_props, "src_showing", "▲- HIDE SOURCE TEXT SELECTIONS -▲", change_src_visible) gp = obs.obs_properties_create() - + local source_prop = obs.obs_properties_add_list( gp, @@ -2612,7 +2610,7 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - local flbprop = obs.obs_properties_add_bool(gp, "fade_text_back", "Fade Text Background") + local flbprop = obs.obs_properties_add_bool(gp, "fade_text_back", "Fade Text Background") local title_source_prop = obs.obs_properties_add_list( gp, @@ -2621,7 +2619,7 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - local ftbprop = obs.obs_properties_add_bool(gp, "fade_title_back", "Fade Title Background") + local ftbprop = obs.obs_properties_add_bool(gp, "fade_title_back", "Fade Title Background") local alternate_source_prop = obs.obs_properties_add_list( gp, @@ -2630,7 +2628,7 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - local fabprop = obs.obs_properties_add_bool(gp, "fade_alternate_back", "Fade Alternate Background") + local fabprop = obs.obs_properties_add_bool(gp, "fade_alternate_back", "Fade Alternate Background") local static_source_prop = obs.obs_properties_add_list( gp, @@ -2639,9 +2637,9 @@ function script_properties() obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) - local fsbprop = obs.obs_properties_add_bool(gp, "fade_static_back", "Fade Static Background") + local fsbprop = obs.obs_properties_add_bool(gp, "fade_static_back", "Fade Static Background") obs.obs_properties_add_button(gp, "prop_refresh", "Refresh All Sources", refresh_button_clicked) - + local dlprop = obs.obs_properties_add_button(gp, "do_link_button", "Add Additional Linked Sources", do_linked_clicked) xgp = obs.obs_properties_create() local extra_linked_prop = @@ -2665,8 +2663,8 @@ function script_properties() obs.OBS_COMBO_FORMAT_STRING ) obs.obs_property_set_modified_callback(extra_source_prop, link_source_selected) - obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") - local febprop = obs.obs_properties_add_bool(xgp, "fade_extra_back", "Fade Background for Text Sources") + obs.obs_properties_add_bool(xgp, "link_extra_with_text", "Show/Hide Sources with Lyrics Text") + local febprop = obs.obs_properties_add_bool(xgp, "fade_extra_back", "Fade Background for Text Sources") local clearcall_prop = obs.obs_properties_add_button(xgp, "linked_clear_button", "Clear Linked Sources", clear_linked_clicked) local extra_group_prop = @@ -2674,7 +2672,7 @@ function script_properties() obs.obs_properties_add_group(script_props, "src_grp", "Text Sources in Scenes", obs.OBS_GROUP_NORMAL, gp) local count = obs.obs_property_list_item_count(extra_linked_prop) if count > 0 then - do_linked_clicked(script_props,dlprop) + do_linked_clicked(script_props,dlprop) obs.obs_property_set_description( extra_linked_prop, "Linked Sources (" .. count .. ")" @@ -2717,15 +2715,15 @@ function script_properties() obs.obs_property_set_visible(meta_group_prop, false) obs.obs_property_set_visible(fp1, text_fade_enabled) obs.obs_property_set_visible(fp2, text_fade_enabled) - obs.obs_property_set_visible(fp3, text_fade_enabled) + obs.obs_property_set_visible(fp3, text_fade_enabled) obs.obs_property_set_visible(flbprop, text_fade_enabled and allow_back_fade) obs.obs_property_set_visible(ftbprop, text_fade_enabled and allow_back_fade) obs.obs_property_set_visible(fabprop, text_fade_enabled and allow_back_fade) obs.obs_property_set_visible(fsbprop, text_fade_enabled and allow_back_fade) obs.obs_property_set_visible(febprop, text_fade_enabled and allow_back_fade) obs.obs_property_set_visible(oprefprop, text_fade_enabled and not use100percent) - - + + read_source_opacity() return script_props end @@ -2733,7 +2731,7 @@ end ------------------------------------------------------------------------------------------------------------------------- -- SCRIPT UPDATE (See OBS Documentation) ------------------------------------------------------------------------------------------------------------------------ --- script_update is called when settings are changed +-- script_update is called when settings are changed (Every KEYSTROKE Included) function script_update(settings) text_fade_enabled = obs.obs_data_get_bool(settings, "text_fade_enabled") text_fade_speed = obs.obs_data_get_int(settings, "text_fade_speed") @@ -2752,7 +2750,7 @@ function script_update(settings) fade_alternate_back = obs.obs_data_get_bool(settings, "fade_alternate_back") and allow_back_fade fade_static_back = obs.obs_data_get_bool(settings, "fade_static_back") and allow_back_fade fade_extra_back = obs.obs_data_get_bool(settings, "fade_extra_back") and allow_back_fade - update_monitor() + source_meta_tags = obs.obs_data_get_string(settings, "prop_edit_metatags") end ------------------------------------------------------------------------------------------------------------------------- @@ -2763,8 +2761,8 @@ function script_defaults(settings) obs.obs_data_set_default_int(settings, "prop_lines_counter", 2) obs.obs_data_set_default_string(settings, "hotkey-title", "Button Function\t\tAssigned Hotkey Sequence") obs.obs_data_set_default_bool(settings,"use100percent", true) - obs.obs_data_set_default_bool(settings,"text_fade_enabled", false) - obs.obs_data_set_default_int(settings,"text_fade_speed", 5) + obs.obs_data_set_default_bool(settings,"text_fade_enabled", false) + obs.obs_data_set_default_int(settings,"text_fade_speed", 5) if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -2820,7 +2818,7 @@ dbg_method("Load Prepared") end obs.obs_data_array_release(prepared_songs_array) end - if #prepared_songs > 0 then + if #prepared_songs > 0 then prepared_index = 1 end end @@ -2829,6 +2827,7 @@ end -- This is a FILE function to write prepared songs from an external file, but also a SETTINGS storage function ----------------------------------------------------------------------------------------------------------------------- function save_prepared(settings) + if #prepared > 0 then if saveExternal then -- saves preprepared songs in prepared.dat file local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") for i, name in ipairs(prepared_songs) do @@ -2846,6 +2845,7 @@ function save_prepared(settings) obs.obs_data_set_array(settings, "prepared_songs_list", prepared_songs_array) obs.obs_data_array_release(prepared_songs_array) end + end end @@ -2960,7 +2960,7 @@ function script_load(settings) local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") local count = obs.obs_data_array_count(extra_sources_array) if count > 0 then - for i = 0, count do + for i = 0, count-1 do local item = obs.obs_data_array_item(extra_sources_array, i) local sourceName = obs.obs_data_get_string(item, "value") if sourceName ~= "" then @@ -2970,7 +2970,7 @@ function script_load(settings) end end obs.obs_data_array_release(extra_sources_array) - + if os.getenv("HOME") == nil then windows_os = true end -- must be set prior to calling any file functions @@ -2985,9 +2985,6 @@ end -- This function "tries" to ensure sources are not abandoned with 0% opacity ----------------------------------------------------------------------------------------------------------------------- function script_unload() -- not sure this is working as expected - all_sources_fade = true - text_opacity = 100 - apply_source_opacity() end @@ -3279,7 +3276,6 @@ end -- function source_refresh_button_clicked(props, p) dbg_method("source_refresh_button") - source_filter = true dbg_inner("tags: " .. source_meta_tags) load_source_song_directory(true) table.sort(song_directory) @@ -3316,8 +3312,7 @@ end -- Standard OBS get Properties function for OBS source dialog -- source_def.get_properties = function(data) - source_filter = true - load_source_song_directory(true) + load_source_song_directory(false) local source_props = obs.obs_properties_create() local source_dir_list = obs.obs_properties_add_list( @@ -3390,11 +3385,9 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page - dbg_bool("Active:", source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes - dbg_inner("Scene Change") obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change end end @@ -3407,7 +3400,7 @@ function load_source_song(source, preview) if not preview or (preview and obs.obs_data_get_bool(settings, "source_activate_in_preview")) then local song = obs.obs_data_get_string(settings, "songs") using_source = true - using_preview = false + using_preview = false load_source = source all_sources_fade = true -- fade title and source the first time set_text_visibility(TEXT_HIDE) -- if this is a transition turn it off so it can fade in @@ -3531,6 +3524,6 @@ obs.obs_register_source(source_def) -- Base64 Lyrics+ Icon description = - [[ -
OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian
+ [[ +
OBS Lyrics+ Manages lyrics & other paged text
Ver: 2.0 • Authors: Amirchev & DC Strato
with contributions from Taxilian -
Lyrics+ Help ]] From d6cf4536ff9d0561fa6d9b1af0ef7d77989c0b0b Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 5 Dec 2021 21:06:36 -0700 Subject: [PATCH 104/105] Update lyrics+.lua Reverted Rename to older stable version. --- lyrics+.lua | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 3582414..f8b022f 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -81,6 +81,7 @@ song_directory = {} -- holds list of current songs from song directory TODO: Mu prepared_songs = {} -- holds pre-prepared list of songs to use extra_sources = {} -- holder for extra sources settings max_opacity = {} -- record maximum opacity settings for sources +loadLyric_items = {} link_text = false -- true if Title and Static should fade with text only during hide/show link_extras = false -- extras fade with text always when true, only during hide/show when false @@ -1179,13 +1180,14 @@ function setSourceOpacity(sourceName, fadeBackground) local sceneSource = obs.obs_frontend_get_current_preview_scene() local sceneObj = obs.obs_scene_from_source(sceneSource) local sceneItem = obs.obs_scene_find_source_recursive(sceneObj, sourceName) - obs.obs_source_release(sceneSource) + --obs.obs_source_release(sceneSource) if text_opacity > 50 then obs.obs_sceneitem_set_visible(sceneItem, true) else obs.obs_sceneitem_set_visible(sceneItem, false) end end +-- update_monitor() end end @@ -1256,9 +1258,8 @@ function getSourceOpacity(sourceName) max_opacity[sourceName]["outline"] = obs.obs_data_get_int(settings, "outline_opacity") -- outline opacity max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity - obs.obs_data_release(settings) obs.obs_source_release(source) - + obs.obs_data_release(settings) end end @@ -2751,6 +2752,7 @@ function script_update(settings) fade_static_back = obs.obs_data_get_bool(settings, "fade_static_back") and allow_back_fade fade_extra_back = obs.obs_data_get_bool(settings, "fade_extra_back") and allow_back_fade source_meta_tags = obs.obs_data_get_string(settings, "prop_edit_metatags") +-- update_monitor() end ------------------------------------------------------------------------------------------------------------------------- @@ -2827,7 +2829,6 @@ end -- This is a FILE function to write prepared songs from an external file, but also a SETTINGS storage function ----------------------------------------------------------------------------------------------------------------------- function save_prepared(settings) - if #prepared > 0 then if saveExternal then -- saves preprepared songs in prepared.dat file local file = io.open(get_songs_folder_path() .. "/" .. "Prepared.dat", "w") for i, name in ipairs(prepared_songs) do @@ -2845,7 +2846,6 @@ function save_prepared(settings) obs.obs_data_set_array(settings, "prepared_songs_list", prepared_songs_array) obs.obs_data_array_release(prepared_songs_array) end - end end @@ -2960,7 +2960,7 @@ function script_load(settings) local extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") local count = obs.obs_data_array_count(extra_sources_array) if count > 0 then - for i = 0, count-1 do + for i = 0, count do local item = obs.obs_data_array_item(extra_sources_array, i) local sourceName = obs.obs_data_get_string(item, "value") if sourceName ~= "" then @@ -3194,9 +3194,9 @@ function rename_source() local settings = obs.obs_source_get_settings(source) -- Get settings for this Prepare_Lyric source local index = obs.obs_data_get_string(settings, "index") -- Get index for this source (set earlier) if loadLyric_items[index] == nil then - loadLyric_items[index] = 1 -- First time to find this source so mark with 1 + loadLyric_items[index] = "x" -- First time to find this source so mark with x else - loadLyric_items[index] = loadLyric_items[index] + 1 -- Found this source again so increment + loadLyric_items[index] = "*" -- Found this source again so mark with * end obs.obs_data_release(settings) -- release memory end @@ -3217,13 +3217,11 @@ function rename_source() local song = obs.obs_data_get_string(settings, "songs") -- Get the current song name to load local index = obs.obs_data_get_string(settings, "index") -- get index if (song ~= nil) then - local name = "Load lyrics for: " .. song .. "" -- use index for compare + local name = t - i .. ". Load lyrics for: " .. song .. "" -- use index for compare -- Mark Duplicates if index ~= nil then - if loadLyric_items[index] > 1 then - name = - '' .. - name .. " " .. loadLyric_items[index] .. "" + if loadLyric_items[index] == "*" then + name = '' .. name .. " * " end if (c_name ~= name) then obs.obs_source_set_name(source, name) @@ -3303,9 +3301,9 @@ end function source_selection_made(props, prop, settings) dbg_method("source_selection") local name = obs.obs_data_get_string(settings, "songs") - saved = false -- mark properties changed using_source = true prepare_selected(name) + saved = false -- mark properties changed return true end @@ -3385,9 +3383,11 @@ end -- on_event setup when source load, detects when a scenes content changes or when the scene list changes, ignores other events function on_event(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then -- scene changed so update HTML monitor page + dbg_bool("Active:", source_active) obs.timer_add(update_source_callback, 100) -- delay updating source text until all sources have been removed by OBS end if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- scene list is different so rename sources to reflect changes + dbg_inner("Scene Change") obs.timer_add(rename_callback, 1000) -- delay until OBS has completed list change end end From 4f826214af04b052532f4faf8b8afa908854f3e8 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Sun, 5 Dec 2021 22:28:30 -0700 Subject: [PATCH 105/105] Update lyrics+.lua Fix the dumb memory leak! --- lyrics+.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index f8b022f..cdfc647 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -2952,8 +2952,6 @@ function script_load(settings) script_sets = settings source_name = obs.obs_data_get_string(settings, "prop_source_list") - extra_sources_array = obs.obs_data_get_array(settings, "extra_link_sources") - -- load previously defined extra sources from settings array into table -- script_properties function will take them from the table and restore them as UI properties --

*R!R~IuT$#tgpD42Q6V*;{Jb%U?t$=N8#Di~ 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 literal 0 HcmV?d00001 From 14a1253bcf3398f458b768057cfc5666fbda633b Mon Sep 17 00:00:00 2001 From: Aleksandr Mirchev Date: Tue, 2 Nov 2021 03:14:14 -0700 Subject: [PATCH 101/105] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55665dc..b09ebdf 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ An OBS Lua script for managing and displaying lyrics to any text source in your 11. [Play refrain](#play-refrain) 12. [Static text](#static-text) 13. [Single static text line](#single-static-text-line) - 14. [Override title](#override-titlefilename) + 14. [Override title](#override-title) 15. [Alternate text block](#alternate-text-block) 16. [Single line alternate text repeated for `n` pages](#single-line-alternate-text-repeated-for-n-pages) 17. [Mark verses](#mark-verses) From b56a053d0a121f041f61c2b2a655b5a1b6d76087 Mon Sep 17 00:00:00 2001 From: DCStrato <76598794+DCStrato@users.noreply.github.com> Date: Tue, 2 Nov 2021 22:13:49 -0600 Subject: [PATCH 102/105] Add files via upload Baptist Hymns --- hymns/NNBH.zip | Bin 0 -> 222705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hymns/NNBH.zip diff --git a/hymns/NNBH.zip b/hymns/NNBH.zip new file mode 100644 index 0000000000000000000000000000000000000000..4a1e72ee61fffab3bd351286d803e5eb6760f400 GIT binary patch literal 222705 zcmce;RaabF*R@S>hXBC?g}b}EySr1ky9Rf6cXuZ^1ef5h!Gb#^ND}fE+4p`n-}eJ@ zu&Om^tqy9gYxdEH%&8~?0f_+y1_ld8-(alrslqUP?hP221Oylu7MKkfKnn;^0Xn%E z*||6a?9BiY_NMeM9xf`P8eqksFM+;=iQGDCVrpoKYGi20Xy^|{2rx5+V$l){5IriO z&mc}9nOBH9k@FggHi(TN3Jk!ZrR%@SQ_2lUcw49rD=Q^9m<}+87ZiT;Sv??pj|mG* zO%(|Y#vLKhXy$*9ZQ>`pbwq?8OLR?TP&8!Z$Y zySsi#a)m*zj5XgS{CreFLAj#k?c?Aulg9aeNYqhJWAHF}e|3C|)dukp{3C{t?!poDnigz!_M!W&aNEE^9g-CoxMdf=WI^*q={3OjFRB z2cA5du0L>FW5x77kX~d$DQF8gw<~U5V2RB~TBi-DFl{1f5sn#Nqx(!#U98Cd zJIPNIz4vvq#@~0<=rewJm7+Cb#g>0VNw;;e_*!j{+p!yUII7%7GO}6eg&_e~huCV3 z<&AjkNCa0^Zj(ps*_i6BfoHfIIJu1hIPQC;hQgtKZnB-Px?bj(G_d}!~ zOu3jYG9qFHP%?t5o`L_{UBwlAY^!;o=;)qb=S>cotGt`%^MnycvPOk@){|MWzH&$^7EWoS* z2LoG%1Op@bhcG!ya|;(wfVjPrixbe<86f2hP;hnnTk^IK|DU{tsCL@nF`=G89T8$W z>>L>L#1SV%^+oJZ1`xh6I^$2e`V3>5qy4tLjq15ra6-=6{OTs7E`55)h5B_9=gH!( zMODQ4?LtOAr2RML_*#d|=jepV7`3SgF5>Nmopa^&E%@+9p6zWhLyBfwN)fCQ_3}70 zL3S~%tx`x(?=rtT@=Mqn!)WF4xZ9{yf5Au z>P_r|5C~1ruqm$@;>w>LG{We^hOVMnBmG6;-N$KR=alJ1j^#Z66|0EG=q+2Kg$xS3 zKN1T^w0ZX8rjQH&bc(gLF_cyWYIx8%Po^AXJoWzKTh_d^s5){f^$!lYP7T(dI8of>!)`?hL+BopdJqSu15a>H&D^EX-4u}; zD0(>pVV|k$GW`XTkaRGS?BC7+R^KOmi-GKU++w2${-o(At%L@*eS`VrJfl*Hwi6lk ze$0kjn7$fE0__gWy+)E)#d=)4;CI0FnnAxMI$E{kX{2MJ#Fm(8X)iRm&9BAwRkH{$e#)#Zc2;` z;8*BS?vfQZS|y-Lc8Lbiq{{E|8ZyUbOI1DdFB%b7Rx0{~6IZE^X*b{wm&k^cs5GImN@bKVp=(y_{gz-l-U3SZxIHdN=P-#>zR6 ztiK&EaXkDD!#UQ3r!FpoCtv&d!=<{)SZM?6eWu3jqr~4kFt*1Qjc8&b9(q#m5QfQmz|?GCtV*rJ}{2oeXF<@5Dd*C&h>vI4aYZ zTvB|A_@uq+#q!}>fwR)8i?8;t%9vH5khi@G-;aR^xFpeMT@81)4FAz^2vS*RtxC9K z5Df;PIF9y*24hQ8AV9;?#R4D){2LMc=9R4aDo@0MTw@pBl|cO{5!T0bEH4M-s8)*v zu=NX~PZt@jN0KqLxQX}!eg<<}P?$%1q&m3B=A2#a0hbBh4}l_XR0AI_AfCLqb|`uS zKIZZFS^H1Nut3Jq>pTbn8@QJ6JQLznevPajydj_@4|KnA*6^c^ddEanSoYq9GKc$) zU#Jz*r6eAzo+c{kCvJ3_AXNYnE@koi4M)~RXD-7gV?sJ8L!xlQF_bwa&YU{1pn%jL zu_mcB3aTkX`lx-Ia|*NixT+4liF_)(UR2M%TKE*pz)?i`aP4U)8M!3?5yUOmW;v(* zGqqdV$R4B5>hyls+1vP@;CoM-%D&(6-;-f0M(;HD^=c!UYwzP)IYMIzhJs^zCU-)R z7Ix5H?3j86k1;pPgdj3F)IJyK1#$@jn3cH)P^pwG(u77l6Sn(tf?4TJ1`%U8m5;az zx3v*x8g3_=nZJpYQno_pwqC2#X!;mP(x5#edr{%5MQKZEWmAfXe8jcbubHxAJw3%v zbPcF1Q~Z4{v}88DRehkQgQsP-9KSEUrVOD($f|-yC{glmXidR&VzAvH&Bzd|u{Kt; zLgAQLX`utdV-wnO$r(|4QM6x1JL-t)GmSLuSmBQ4COH*vFxhWube7=N6Y5`+Gxq;M z_##QF!fAo(AP6l$l0@6a5Nl$=;laYy~y{j`o1N8sDL8aI$X$py- zkRA}k2)z&K89>o%tDw^t92KuLq*K+?bn#{8l7cor82wzF`0>8jQR$sy%EagUFR3yc zV%Kd2_ckiSzLyZ~yH|rnv<{B0u_}hZ=)v(&Xsvo{?`jep8omCi=RA3{=^@#=lZo1g z-#2Ga4EX4KvI4C`UtMkxEPF_@z@~qOw%#tSZcLXY=~<)mS39rdCvy3qQ2Y=wu97VH z?18)5#K2Wad%L^x%LnyXNqTX5c7+dS0S%@=)V>-M#S1%wdSO7EZvL4R&{AAUhDDOY z1-!v$=1RZN8`yuV9|GUL2<->cmM*`Py*`)=KA@3g`aIpU2P3HqmnXip}SHTL38^dE4&yTELH*b2ht%X+$X zBRf-osHG`D&J&I7ih(I$dQ0c#96 zMkC3X-qj+NeGiq`Sn$C&u2RY4l=S3c)BAF3b7Pw;lmHL6Q3i)Wg<*gB7OS@({-e9V z)jN-?WI^MreZ(#DuKBT$@ECJ;*A@>028{B!%_qN^AsBv6^5L52uG-6!N|5yqo{f0^ zGT0-_y4HIVJkQ*CDvID^ppj>E2hD&uP!^A~sw`f}-WUK>?lvcrmKP;U7+w;dw7snJ z^-;m4>xd3AUO)1Z_lsz)FP;->t5q$E3J{GLAqodqrE_Aso|(6&cn_UcCa$!=EY2m( zIWg3YLNT0MrpUe{KhmSi^H7Vt;(b@!MKoN#@8WT)w8G z@WxW>%EF}Gh$y{+ylXbWWb7nSRAKJ3uUz;Iwr7Skca=4VS3-<*I(BHV!%PT1;Y^~S zM}n3tKC^qlWoC?^D zzP(C8uRzE!1cQl8fRK>`g)YKBs$pXTkg&0L@}vdG8o8R%{=rAm-p=0XZzu-jD`0$; zCts=`Juss@sG|i~3}T%|F!#yTN6fT30c;l2vV+?Fq_g?So*#ZFIiWSD<80vpMOv+=WeRGC`=Bj4HD zZwRdEE7?R?yF2ss51G^OU0K`D;2)v3KAZD|=8#!!W8oAE&h@!tbyhHI#^lfixch>C z2nllCf*pE1gTW1CMFplvW0R1hlC?@V`#>h^W{6cY_L2$ROjdZs`DM2&L&qKML=<7_ z(*{MQGy|y?!vnVa0+ee9le8mxzbB*Nctkh*tlyot9|x*i#W~V`$L6__GADxU&X*A@ zXGO;U)G<~0N!k@cW=S-_xJBr_UTdV<2^P_3E1WBi0L9A@x5ncLZ-t=}(=Rjn7K%Tk zCNRD3h(6`&YsFN35oG!@X&xt$5va&Ex935YWUiW#4}R)S8=|Ou(dj0jzs6U0ZOm5b zE*)!v#1(i^PV*+me=$sBnG34TTO^Km!4d42SluSO%A8>Cq^7d!u;qAa7**S1c4h2))_=JtfMY?9hb} zyX%^NUsd8e!BJrh5@jE~fx>u4T1gx`Rm;Hxbi-erul;^`Z44xMx?{LlbqrVV57;EF7{pG~59A~?#RFKaJUY9P(6v=V_mllq zznr=s%btdi>@;Jphl4f`eD&U`{)TZ)c6`K0!FN^H;F-#I*|y-U)Knjjjy3+NsF8*{ z_a?{U!W=#&f_P1k4uz&nfetr?$p{bnOmtn?57*_FYIxM|F$*sydB%hGqb4dxUEE|` z{mFOIW2RRw8g2$_@80BQX{d8YclRCTA|-7Y2|O0@%f#`BN24^Z4&<)u6?LSTKcbN2On9Kyt z;gY!Ugu-g28_-)-ETZ?z&_hT|W`y23-?DY5+B9|Rr@CBm-4BKh$`M52fL z<;5L6y~SQh(Pe3zZFqAIxA_@$&YeuEi>Id?VZ_qX9G{GR#`l;A(%28Ugm@iqJ8)Ll zcy-Ry5%RC=4Sr#LxE4e?gP5o2+Ni0$k=6S3IdYm-zFN9mSv|qLbLl-5JeaKeE)A{@ z*m&sba!^TAGnwCHlkK{aq2Iw{PpG`mI=kYfTvSF&xm_@MpFpumW;$~8wCp%rE%CHM zWvkEWH|N~*JPIsnE-oB|z>6Byg^tMfz-BV5I|M0JpQUcbqJ_9K3_E zf|xKY{u^Zx2%%*MFdjH`)H=VYhafS7pPptk|6p@SF5kyAw^CCwF68j3Q&dF{6AVsHf*-Q5ZSPxjQFhv!z% zv6FY_-#%KhRj@o@%=S@#X<(p`uJPa}k;Ue%JLo)u(Hz#su9mXlF|E?cD;?GnqCtq! zU+!2q7(Ee3aeEiM)!y*|u97`6gNa=lLI7&G)F?RR+Mc2VR}4HkS1#BzKeXF!s+W-& zhRb;^A$0OFC}>(-*aylMalB9cVsMhq9x7c2itabqe}v+J%yyd0nQ;Y(P!tfM)c;DD zqLY!OGw{W2YuUTfzW6Or&wv(0tfezR{-x^nHz-wmC1Wv(lk%sGs3G?WH^?NepU6ML z9)&gric#%qGz%LavnH)o?p4^#Q`%2A40O+Q8mA0CPi3&MaPgIdq{Z4T@Mof0a=FN_ zdVZKolC@;-EEW)5UYd*2AZHXSaDOa6a?IDUq}H}ZKN_=frHvZ1;tv34IDM>ROXBb{ zK{-JrM7h<{BK`WTx#~-0qTEqjJ#I{ti?LFkVKqU7{Sk@OmcJe@Gj)jXHkZ5hD8tRq z;PMT*T^b}~htUZ2Ni{;It!ZAZ(yB^c`tfx3Vf)PzK4%tRxI{gsmQ!0NhOhrIu30yh zYNWPUwl$O(NvuTu*g_Gm#k3W~65vfH=~rB*=m$1l>#0Y>>3Ud9I@NtHQG8ZQ-uHwd zos>~biZFXTqi`+g3d1F#jCw0!xm;^TcN4bv+O%;=Lm$)7+j^W3O5_JnW}DW<`PegI zNV<#i-Kf4uD^kBhJewt{-5Eg+>9l!YgK=$@;jPyQG;eD z@O~1coQn=v_$eS>nF^80%n0ks>1d%W6aHleJj%>qR$4bjq8o$pBV6VO@c-a?DJQhj zUEIQhEW8}3X!}nl2l8Yup;5!g^OcL21sa(;|E2oE^(*ZwQR#fqz851ezYQ|-%~Web z1d*-&Sr}4Eq(aOpb19>tXi&;1N0;7|_gtP=rdnNxjwo^T431Z|LllcByt_JpQ`imKM*6*R za5=ZnVM<>73!;IhIEBxw2HJuR&W@3@{3=j5uwq{%(?|XN7j-+@qc4z%b85SAw_g+A z9*>@FVlFKR#|jp^D3##EK`s}T+F4`W6-tjOW{kP?vq~>7O`+g4dk8h;tDDU*zIR+9k%h727( z)W?N%IgJF^Zej+JZ`tE>`%xLB$VJzm!F36lK`!)7kxi(!DH%y_8>N|vHd3TypDh7f zQRxG1WI3bcwBx&?rl1GJ%Bo#Qha#ww81t#?uK05P(6;v{D`S!$F2_CQn~AKkm-GaQ z?)c@~aRoMnZ-q?#kRN*SzvNEM9^3HMNmy$oqmCKhR%ZpAs8C*nS0Jdx5;%Ee+B{AP8O3 ze>D*#es<>00AZ&W{ri(k2m_5=T`bL9|4Pihc*Si@0+YhPiyBHkq9~m7DJu)U6G^8m zbP@E=4_vLOiy#}pT9q&Q={*&rO^KfLzW2Vnb7Rs>)61@2(Uz6Sn2l9_^~Pa01VMMw z-Vts?C(6}U_g(!}X(dVWyk`7K;i@b9G&$i{(Jm*gwBHU2{MS$yV3~E+ji!{d>2)3@dQHd9u?!UUerAB~hZP8_VHX^8b$Y0gQ(lS-9kTFugxC ze0a2A5d)HPq|SWkHG?-C7cEI*N_S10#1QQ{6z|xlvWkOMjma6=26+>{bK&}3y#M5i z6%-4X>7mP_Kn?LZ&_njmd$;{xkI4w6T28b8NuZI_pC%J1LV5!KN+KEbIt+g9gzpzb z4cXDZdXHQj0|#5WA|Dkmtjfs1_-PjDjO?>~d{?)%HeS^&7GYWKtn?k{Y_W;5OBBlf z=Z>~P_{oA#$ei2)J=OFUQgkvAu~H5>P#gszz)q@*(os$c`{!+e{tsfGjkU!x;-<{< zj64h-ve8Dlia9$WTZ%pA%dsDDLS}UisCA}yApTo9BGAfF$ua7Lfp$zbXvYBlteh=C z3Sb0Kv9~d`{3i)|k$n*-duQjr;T`l|En0{YpZ!a@2I)bdt3L>fO(+X7ga^_#fE0Se ztUlp$vI}#BBJU2Rn&JoF%M~!oQu@aIdpUjmUG=sRcaCVHxHf~Q{oUH1yI2O>PZJbA zkUig426jdpkVv1xrkBR5t`hQ$%pUS>C^#`$%oPtpiX&9s12=*f?fGNhTyE`CrcW2A zr9NINS=Xb(Eo2VP-8cizf5p#;<$i3WESp>k(H--+Awtt^Pi#4L`Ceps7$5l|{>r#n zd6`Nrr~67his>Ul<60MubBQj|{0U2gcH$$v7xR}XHudKg``x#m#XX{~2jwgLr)3vE zZHLTKS5B_oxT#ar%#{#qU5q^AB(r* zY4=q-7a0UhVD*OC5(pR#knN=S2N)x-mkRGoX%|QYkhFIPsJH`xE`RKTioL7dUn%qP zyyE7c%;-;l>h5Q`tul^Gq0eYG4xg#_6szlFq3!SjhC8co#>}hHeoBN}G_F zDe0YPTxy+@F@++zjw=+W*k5-+y*+iHte4amlh<+V?;};FZ=6G4EZF0jHT^;w5L5tf zs0it;7vOx9Ivs^`8vm;_rO$`#sTT?Jd3DYx6v8 z3jC*AEFcWK+2Er@L29oJdf@)S5NP)|1S;?qX!FY&4m9J_Y7Wx0lw-8?N>}BdCuKf2 zfX~B!c=UodD;0`?JnH^{VXR-6Qe(y>95)r-&eoWRT(72pq?&Bh#9YD$XwS-kG3bN;!6bu zLZOd+5keTFRhg5_VP_fKprdr|d#|ezK;~0@v=u*65mp%v$ z1si?GP19Xvo=fRF8>$TRIjGnv-)v4Z6eldUpRZ%BF#sV79Y&#L*ajEO zQ856w$mip3iwQb|=$0;i%ot}^4^6Bfp zUq$ zh2)ZW`Ti^zQfmwJbEfqPIhYFMN{MBOs*vB7@0A+n!1VaATgUb~-3jwt9EI+ccsDEA z*OM*z9`ZM=ewIpbl}%Jl-vu0zTXd)duxHqL^6S`WAe?QF*nRu5wF~mSGc)gsKU!&xR|8sF#CLg7zl`Y~?WJZjE7HWn zNG}q_b2RlBxdmZZwfjrA0v4KmIgWOJpg@CGmqk&aG-0VU;HtgMmZa(}n3wdxUCM3D_P~e5i@? z)kSQ{8~Lizq4iE;48283t6EOQ_<6LFJzJlGm##zsy#ywJd`k0+cvhddij{3}tqKHj zetcoK3u7cLnNQ?lz7rNs5VSYrW5?ac{BejPgZXGK<-5+NZ*@S*(jr`cD-k6$!!*eg zdVEcAHhE9gd&~wWiIzNwMd-J(3yg*`^U)?a>P#wibAvW>r(FBYq@?17yV-ZBn>Yuz zDoLH0qkOvw3Hdk*o~`Mmf*vBcX{9^oFlF>=MR{o0{vyE+Rc&(l)c40{2)#8$9M4#3 z*4YBXm2TjwB#)Xi^)6J&n#ao8{q8i39Yl+*O7ZXF_0Y^?J+ASKp26?OqxHP6Wzj`Zd zl8*8G;k2!bo?VvOkw zMtGRmU%Ey&Hf1Qo<$smTBGAdMT@5JFzNd$rrtPk+s>%p)lv;&y zVr6Aqb0#L!{UPMri0P)^n3Xb_XdzXtHv?Z^9f)f!F`GD4r#Yh`KR$yo!xm+GpjYfW zf_5!-?I%&p(Mps9zqpu=6k13=Gf^*htYF>{pS(gA;770#jjLLC+wo*&YIUVW7gsB! z$RmRc11^Uf*DjfR$y`4bGBxtKp7ZIIj-G*-;;It~Zv`>62MW{wC%x^JnE zwd^<(wO6Yi#A!7Xy9Vqh(%%NVWG@??7E6?UFzb`R56JLVV_{lE#pFLdtp@OJYAbfG zSmjK~tOsDV{pc}=3p?qVTdA9MJ|RHRg{QYkaMc|?9PgFA{ro%O1nWQHx&c8pCq)=u z1cFQyRD=B|HFpBOl=VS1SZ52MDZtY1A6IB&|96fBz`P3GFWpIyZgqpE{1C?ru%y-C z@l!>_ge-(;IGa!4iI9^_(R4U-7gsEkd(F>W()tVPPlg)U?63n7I*}-!7a1Aa0W()2U2Z2%4Cq;xM5mrcz*mk<86E8I?c?OOuKfu>cf~>{U zM#(lU$9cjwuTQ?(`T-K|Yn;M_)t70+4r$hS_$Lz(F@kZ-@o{hZV~dJVFQOuIjF{Qc z@-g`o(&8e24;hT1@Kx%xk-r{Ekoy$c8WU_ zKQs$x4owelHnD}c3zTyz(J1PPV^J9sVPPQr-jOF&ELN5IExWV!da5z|$e@YtJ=t9V zi=rfn*a|i_C!P718~x#7x74*VSwAIA9g+KMw48dZB~p$`3?$j5b~5aKHYmM$w7?>a zk1&bJgM&oD=RPYp@ot~>SBJin^AE*{TLm?VtHzkF5KYe-LaJ_|&U%sMR6otz@M&;; zF*A>=>ZXgr6A|u^T3D6H3(t4vElIhYG>s~n9#cE1`z`d7cKzX16ubn%1Oy+V`vrn2 z1*BO2z~l^Ye;E#xurV^Vdl|x(1KPc`ycKNzO1}Q)6-*(C3UTv{s6&?tH}Azy3ex7( z-UO)1>pj=3fYaEvWS4y-Wjb9+IAJy4#4(PsM~_;gsyfyX z4%4RO&lch1+7%9q{ND0*y!(c{q%%!%J}l^+7R)!zDy8SkA9b07)m5y@%gyaVeCjvZ z)njZa4xxx68~`8GH}+{~Fd;hYbM$U&%D!j-*jGgY*Er*-O>>$GgP>Md*D~)+?$71u z{0wwVqWuKjnnDf@@s0=s#W7L@cnSNAT={^TZ0q}71%I?y4a_Z51e_H%B!7&S4xxB1 zyy*y2ESV7SO9?WZgHZ+y4b=46?76#N0LUxYP!hz2IDdX~_QqvK$GaRgj( z2K+19Vxd*c%($G9z*_o8b6+V)-;09w##_vGGJkut;d){&hF`LghaDYQIIt>9>{$(G z-B}j9Tyz-NWyn9$ZrLoW6tT0n2|0@y?R~e%S?_IvP7_odiUOdULW*lYq&Sw6GKq^V zrL;hKXzyXDC2a4SZj^%~POkXD3?c`9do|Qd3Ce%{D`C`=L70+)F#V_R zC~n-+HX@0PWQ^_pvZ=}$fRct8P3CRpW^O(`?T953cWE`s4y<75{t$XDC3!#A^TVR?5V^*z#H!59Rk5jJE}Y7 z3W~Hze@z5)w<5da#>!G6{|}41(~W-e=5aIU&*s?O>Z{Px`o>qAP6y>uwh=Yw+h_6d z5IVF+($I(=NC}W~OCO7Z5Tx@K7W0_wqNR)59g^FjmGLkgST ztEO59O)eC^&(8D55HDpn3RxUZnC1+e@)YZ0*BXAu0T39^Yrb(^j2)?Q$z3uNw zr&~p9;W>J`quV=X)y^vLcy&={K5RZ=e{v$vPV8K3V}Q1_QMzCIj0SW6%p<|`J;;a_ zUxl`|qpj@c#O13_z)R5VeQR^v0|JN`1Q5!f0*VVL!L@NS`a2#B_7yCyi9#>lJ_ICd zgam6M^fFb-$QE3)()BVjQG=DRM95nn><#-Xvnx``e$y)jYjnyCQZ{}#z4vDH4A&XH zVR(xY=)|qghsm4!90Wh1YHJ0y>8_}D)mN3Sbup_5vr=p(Mn2r$`1_>|aqN1D+eE-6 z!CR!O>WXmDBI>OVB_L~`v9`FLe$#M zfLNz*Qo~|%*AfI(6ufU?`cr5xI4&o4v8a$&i0<}@qX){fBb=9(nr55nqjZLb*&O&w zc`o@*X{!$k*d=DkMssfGSxk0!N1DoqQu9Noq9oku&4&)1wo-;BqZ#xy-Uz7pzB5uA zy@z_+67(*|*o7&Nomj=aK3NG)<_fM;I`JqBGP1P0f&HBYw8@ZC?5Iokw^g~%>59*; zTFb!eZI#u8ne+yy3J3!##5b(!yd&z=HNS-{s^{E7^YB|sMXWZXJJE?~QWJ0=+6`Kq zuBObQVjx|7^dekN8uS>%yUQbLR+ePZg`dc9FG)MLsP7L%>6=n!A%0N1i&J=7#_GrC zSgxffHjad(sY|XaXO908kfM1;_|J>og0T9b`uv3klt8Y6uzLGHto~|W1nw1BKo!vB z&!2J}A-3t&F1h49vTGPX>g}N5T8R)h#(Rs1L}dO=xUM9mZ>K*z)MxXxNH-IFeByjM zx{6_9_%^TO^~UcwzXkj%?K;-!%C9D_#OMg*Dk!4RKbRV4~6oYES7irR=Xq$ zk{f2+PK~qZn3=H$_ZsAPA|AE7PfqOPwY2HVI_)-fW?2p_S9MBc%`HwLPv1W@eRU913QsPI;ldB@Gy zOU5&E3zw`nT51(#cC(B>kXGk1ttEv-p&)j|QV{#SsxJQkQCuKz_y7ZkLd#g_f|jFsHU!7XiR|}v7jAhMFt11Lxsj;i$r+7QJgcFkLjQ=}zD}$g-6k%k zN-2A-Y6g>PuFz)RKF0Wzp`a+?qTxfNU~t617G|Z7K`koO>bn`TqQ36Q7D;a{#!RV; zZ*tP!WPyvMROTI-o=|Q{at{hM@kH5RZ;|#C!3TVyesWQ|8<-^xqL>^X5j-pWzRTGT zoppbOslsK3HK51_(Y)EeSpMz+CYvM?lZS3At%wjC|4Uai{r$jK-YRj}6h}{weFBEr z@Bt)8wzyc^Tr3M#L(^q=`H10Y4$ZkzmINoH_rUBQ9Q}6m{m9fyUI`5E*hZBh>5VqO zZg|E9xfgSvCH5O7>In9TQp42JK7U>{zfTh5&gpMD#>do~coZorCGALk#xHXLS8w@{ zcavkvk+=Em_c_h))%fKFh}7^_Bu@uW7%u?P`tA>@?x4XVP{u0+kTU|+TK-A{|M#nc zimQ^G0yqYldo&uF!QFsB-SE* zK}G3}pJsmEFz_uRPVCPW*pab|676W4 zM3l+qC6iLbLw-a0v|2O{-zkpwb@f1aNlw_bO0yPdtCnY2M^e1F(b??rwpTugVwW|3|+5T#(R> zH!8pjSf&;0)Jx4>0vA!KJ~h(vR<~KhBu)w-UB3Df*KAU~?hiU4a2avQcXI-7P~_6) ze0ZiZJh0M$(0Lqn**G+LXzc@b5kB@6lzMGQNWm%ZB`D{q4?T_8)iCnmgRV%ZHMkOH zdW-ne_TEyhD=f|L(|vi4IpSOx{Av#DW_a>@jVv`sMPVyFvLGgW?k~-rqRBVS)cU@5 zDaVZQQ6z!Nb{a!i&2w)<+b!P15K>3VQrTAHOQ9c4J5hBg*_XMT-LFV|-_Ydv0em~v zL)={XWkr{+KGH)-&2FRT-nZ%Dnakbrk#Jcd7mgLJIn`GJuEd12aP=0iqQHuktYvU2 ze7d@2Sv~%#qf{~}UQ%5`;$u_mnvdp(oM|nn%Cqz`+W2qfF(^S6OVt6SXG-wP&h$&J z?S5Bqa`8tc3a~fgw|h@^(?OyL>Qa81&uyAp3M?Djo~>OsG)Z7R|P)hrEfU4vBml z)3mKYomC(rE#nH+V0fn269s)~>pwew-|-&7$?&xvaWo$?Py%(mI~! zu4(R-p?(=7si~8Q$OY|aLeL8Tsi6N^VNg|5$l2M|$;i$GNc+#}isW-G7d-E?PGLDJ~bl&gTj@p^Hb62aua5HHCp=fUl6xKN9T1pw^Lj5E)I6 zA2WO|l{7RQCM^AtVpnBC&v``=zukl?9HT9r-d{l*qDuyIw_#>ro5;M|NRkD22an@d z)~a^dW}_9X)?rb8; z(*eIK7t8*h)5f)dTus=mlbr%}9Ra$X%hc*H?GDPQ>Ds`g0Yuoa!DJ<7A#OoJ)weFs z55nt-=0Q*cZ<`bv0v&gk3TV&bDpVS>2504U=z8(^epI(8Wm8`{3T4GBn+Iz6&_Cqh zlJP5M36+{%5i`8kZ-@=N)LTOKwl^5ete5vQ4osJyk*&T543TjbS#>NolmVXVnlX5y z>Wy?z++6FWB;Fs%oubh#Lhrh@l6F}vL%~Zws+>GsD%()GO zF)5>>ETfOAOV=^aMC2`)?x<_F_tF=fFHf=ah0WX^GyiD1VjZk>XeI0VEAxz@Mc|dM z{RMIhN#_37tRR?dK`{Nf#N=O?gh8Y5py{>0b$;jlYkm?0UPe8KK%<_>D8M6SN{qqy z1_+GuaX~fcK?gZ{Kh=?1A2_vMzBE-z^uz?sv!pMbd0RQ=EW94ehC*SjshATgCS%0| zF)f|_Kc5KF`ugi(4>cLrA{$CW_b5GO&=*@)ihDVKh7nY$@(Da$RO>93`FVuUgnE7T z`EdQEdC>19!KMMo5S>MItB2oR=s-}cz6`gDU(*B}DdZ?_1)};C0WGuZS`<2P?a zo6|lyI*deIp5v@W#~5P}Lh4!7<~Vkc+KP$%XlioEX+0?qr9xyao$vJt{L1h>J~I0F zcgz0wuFhJ3*10)ec0^G9JptoxJuLpd2t1oj3 zf8MF87Oj+eLvK$~ zOc*?$4gdE^ZCBTM1Gq2_c$?tiM)>(;)20Ghf~> z*-#B(yCbLAZA;^?9+n8^{LpwKPRKR?wZUp+_}bt-iYlPw7F?pXq!3;JQPkU<%7ey2 znaFn>%a{bWj$VZDKub@`)o5XnW_k2mxy$?TLq(b*#Es5OR-f=jv9Q%90=s5eo*u=D zs?3g<8fH$O*h!IEKR4S%w4=x^xY6ZC-QX+de?!GV?tY$@Uu=M zGvxKiU*-YuHFPWMGp>!b#K=#iZ)_vCWl=O|Q>&xc$$7uxLJaq(P<`OBJr?i6CnGy4!gW=WTYerybQDqfd*QTQLeVd)V2$gcow1XVPH>)#ND)cGpZ|i0*VWt z*6%5+}Jzk!6s zG?8dW@F0eBW?OYmXGrKh}U%k$wBpnuykJmn>x9MEq`_DYr@md5tcpW+{ zffZxPSYloh9$|_EK7$?xEQurEI1!5op(i8WLUfUwn>moFq3Yl;X zCo3uE&|kk?;!#v#LavEmc>m1(;?2_2Q5d$ZqkTh^k$zNR|9)NUd7e<9EUQzegH#)O zhtC$zuXsUnOMTy3J43rPY1NDg0yXM<*KcN(rdv8(czLB7v$r&fA^3*yQ=pwe4{cgcy{m?9%*klNf?*j5veN!M)zFxU-JKSi*>UaRGAcl)M zG40>7pwHB&ZqB7aU(xlF8|)GhJxs zJ|)3t_95FhKhu3;lh^&adZJuCNEy|r*;rfKmUsy}_N;w&YlCU=4723v#<~>gKRa5> z#wNstD@NG@&sn7OHWh}cxMN|X$hcj44Sl((syH`-*; zd5!cQ+G+k1LSse1rEGZCtGi2H+{RvIbUm2QEiE&fSJoPaJCPCz@_|Cx2GCVyFGIM5u%@Fq}PCB zpQ|tTN?0Vw9T*lAS!hz-_YWReSa*C<`PTDTX$UpQM7X`>y>}{G^IyxRL1lk#$A$8YhC7Sg@xfnEE#X@cVf6|*P*Z8t}@yl%`GS@o!XRaSzi zp5a(oiiir$QXQ%HWAR5}E)$PuW1b2yIOPflf<-i`pIs8m(-KsI@K1TIefc8PFRtPeg3+_NyEn5qLND89)@#81+_4wJI4jaQR)tVmc{`CsA{!1vXcz zRWUZ;6b~=_jk;h|bttU@ZO|E8?zEk3^<0Zh;>oU>$3%b|xZIM5uG&UnOixW0E3)#Q z8E$m0O};%$AN;M>n_uQY`nd6GBXwU~=MNcA44($A48HRr;j~?nI`hL|PLl0$v$oP_ zdzFw^nUXnLbPFQ`3el=`AOZfB#YP{i(w6$17j+E7DF{@@Dpkcc#udqX)4S1KH>`yl zpnnXZ8#`=LcYcLS@k=^5p`4^M<1oExJlu0rB-p;|d!Fg)uDhsHv0-%6>giUabj@h3 z8&;8yJX+yOH>dTlASF^P9VKz#(@ZxUI{bgUUGqbqecRruWh@)Zwry+Kw(Z)oZClH3 zY1J~8vFuv4Y^?XH`+1(b_aE3#_4D;TJ|~akIN4YCNHjFc!#su=Rk*&G6F^9-No%?) zIMz{#4KK|X;1_(E{ULCf2$TBgqCoBl(kT$E6wJVAr`jufdHZU@wga?Ky7uLi0YFq6 z2A;r+a_+?i5`h16AbXB(|EsJ-f#~*k=C8-E=C6^l0&+|IurJHhta=DqCyGL?#$rEV zbSQgYhwNqcRK$4ut<*z{CvTo8vs^OS(EhQj-Q94QC8+3Tdqt?PdY)ME;n?>Zv2!;b z-jQ^j6$mxRl52Rq$cl-OaLoYp(m?CB4_|h9Zfp^(E63>gPevec<;x#^09>TVdXU_~@Zm%%<8AIYJ?6 z${po81ah(iTbtiR4Kchpx(3bCa5>b8@z*N%zvR+JVtCZba4De2JTjJQ82gunB2W*L z-m|$nD(_9NmR2`3T@cvF1?}95_hvVZ$}Ov$3uGE0@|r9PlT-~7<5yq!jAiY1sR}eG z`6fvFZ|EfZ*k=79k#On#~$=Wa{xHEg?Rdp zuPihWfq=R8@xAA><5&(*WxWW2gv=bwf&T1yUZW3Kk3EA%(HWqui9Tz9^qtIY|B60C z@Kvo9sUTxJ1sI6}>{<*82wHPWayv+}u#hmNy$>I$nyr7=xGH;+rD>C$ecK&fK%w*n zJodPbM?Gy#=;+LgC#M>?c48QpBfOoK8{2-RTkY*Hd$thVZOshnB6&NrC|*h53#7U* z_AG1@*nd^~-9*)SYCr!6U*B2ox;*q3i;qoH&X4+-k|z-O|9_J9}5Sp)*R4GFU8t%{piSTTbkr$vIRY<}uL^$Nx zY9H@HWV2A?YM@y9!zSL9k7kFiP@_W#m=X^z;dALsVG~C>@c8SHrAw zR8UC$?n4;v6&lFWQig+^b(G1>(7W4!+94_iOFx{}puknn5u{bmK>WDO?n$$4vd7ss zj>qAE`^EgNXLOdS8G*|{=nwj&_dADu==y-$BvW%yT#hn2gLf`Rkb(ivI;;;~+i?8b z) z-fD)ct8mnxiX=3JYy|z{bPLS`XX?uQJ~exPeb%wm_GvmwKuAXpZ*MK}>t|}DWsyF@ zQ|9m9X5O?7w>c?v{!nTw@7+5RX~-r&*t}K&Qvt|SSB_rK1CYrKAoGRm_8(*_7#kX! zy8v1J*%#jz9{)zFoW!hQ}@vp&%`MB}L$2=4T-J(Gq+iw>(<8wH9fS`R) zHKM{X&uD=hO(-!_(Dz{sB%c2yNUkvtRkPnIV${$FEtgq#&sTf1BwiAiGH0us*=&s} zG^FMYZArZWD3Xw1DYLpNse&@~LaYkU?|>@K3FJKuyy8LBeP?lnl+54kB^w=`g!0B$ zTr*Hb^eoYvT1sC@?OX1Hr7(JBPn1eXwV;8fs`e7uju z-P=E8#GLU(>$6xzZNJXK|15~?4;YIy_At$Xtw`A0%gX47^hAolwvpmm0ar&{(LU+= zZQvW&2}4cnH_0{76@(V^RK#%~kJn(MIh!j+h6u&;<|vP(lX`-Xb=Sq{ex4T#P6e~4 z&Mnj*?aBap7LQKp$om%T-yzf$kQeVq)=mlmENKB)dLg9!2TRY<$8-B5Rds5R z2I8&lIAt!BikyrNfmk(mejG+~_)V`ridk5trE6r4y)zT3b8aO)P~JUqiz}ZP8CbkG z-|b%-18vKcRjDK^tcrvguxQxbYex2>%7C!u!wyqghekJw+dqi$z2>}lBLU6J2c`?_ z0Sa%~9jDh8Y|KJbRGeH8nD2VfV?O|*o7es0$@(@^V<&4Tjb1&Eb@-6OrVe*lf5c}{ zdzt&lI6t-UO9BEZiQEFJ=o%v$a}cj-&HBw+x!$jZwqb_jRd2ShzOE<(8wl2Pt?d9=p7 zc|Lpm)v)=~UcZE4u5PKL!e%=HWu=Q;a$M1&S+7*q-9yVBJinC7}YY6AU zwW~_Y+V7++zUgWdmD<9U0Iy$B80K=Ra7nLK`z4cIpkvxB!Jb#x$Z_UlXLiJDw+}i* z?Of2OS*C?8QOHcj-=k=QT|)U?g|{A+;^M8~?vusYx(KrE*SvroDv8it-D%8Kt3g+$ zCrQ7n!vrbj{psDJgiXW%Jgu`^??aw7!05wQbKffm*n2lG(?@cb`T0;d%pU|%1p3CV zi$;0yr6fh=Wr?IhFz>gg4sFOnSdua>SjR57*Wv|$XcmejkQFk#-;z~N`f-mEe+^zL z%Jf06A~LqCK_D_nZw$*OQuXc`{=RTwoo(fvYWB z`{aXF>cQ-v%a!9Vh-h{gpynXUMKedx48}8&B*ep^jCyKWCLyIs_!(@$lY8fwdb;R8 zT5|}wewNg-c8aKFE=jKVu=uu09JhyvT(!(UgDXmb@{>hvHOEc0#QY(qtM_}kp$Ohi zNA*4|UC^O4yz@5kn-+l+DwH40V^6OvfN+5abu}Kap$)_+Um)|nI0_3{+5TfK^|u0( z`m5Jdr~vF<06RQj_wpHnPP4G$qB_eW7hmtUz-g8X(@}jL^596z;Wt~8aJ3qO=_%>6 zxa4H9K0`N@O1+>(GogUp#J1~&+?TI<8c$py@nFJD_Hkgq&oGay%+g;SK(XR#r&t)W~x8@Pah zWbAexmgT$^ojAtjFtO*4-{i@+6Cm;>vhaj{Gys>=xeuWC+xR*BfFzkYiSdat#ORzi zvw-lmOOuW8nrrQvzjQ$tjuk#uw}sW5k<{6Q-Juq_szSa6?XbII`!|xleU7Bbg&jt~ z55EOO(|<}HTWesh`M-DyL{drHXYrBkUo#jde1(KSg+W z{`>~2XPG$OC~Q-pQE?iOOIC}^uWfD9FxejF6KT2qvNO>k3=WxTbnHX>9)``)<7FQK zx~9dMw%&FrX|UN1o7@fm(Qd4Au6vjQ2WyW?vjIg0U_`Y zZ0$`(cTyk2H@PL|pyf&wtxqa3DG&WP6URHNEzGHKUmrh{4S$$!SAOhIXC_R{e}BRW z#+V3F$1Sx=;T?V9f|W1TG=Fc>as6r$iv$27A2GDc7XZZm{|AJ)EwE=(v@>@wccKPd z5P`%hZ0_=xm>R;b0TK7Fj2z))T*Q8ybZ<@C+MiWu9F_seh+Bf}04t1He8KO@`-mLf z4g@Sq_~Wtn^~J!j)I*ehr`&>x1g2|nSG;B-*`FYi$`0L4kQ-F+N;~wWx6zuRaa2n| zJew+48-SjiyAq9Zrq{z+r_HS!xxeS;3*9wUgSUx!cwyQY9=F4oMXA)JFOKLCw7@=D zFTyGz6AMo_ZJucuhw}Sq9oGF3agI1Qj|L~BiZ!G9oM2Dsa9T2__||iyrg7fE{YF~#dc(p%3}?=upqta!<1%zhZw!O` zC!mot{`5=E7RKUk`%R97yJ)*jCg>2{owJWEcnqSv?QX zj9oeiC+Z26ef!HL{?(RnOkei28xPFsNe-wrPB&xqm@*EBp}yPG=EPfmj-2moK<(Lz z=s1^2TATRUUdV}32UYKSFeKr6SQ9^BEXFeYDtv$fXu(0SwQUE`@+{%OeO}fIy@ZdK z?VEtre{7ll)>#>nS2*#GlL5A~FQ<&~)wW`!((<%x-X2kCP87}0;$S(M&X`cc0jG={ zV4v<|9hyi1H{bKk?eEcLRBn3vYh}OU!=V}#(IcwP(6h|p*qU_Lkk+uBoBi4g-?DZ% zR>&ZGx#rS@_ewRy5V#+fJq6O}tfO)eY7LJ??>nkzfTd5ZDXUveT+<8N%f6Qmj)C0x zDn-qMjb1|;4iQg%CTYZ7pEW=6p2Vq1_(x-Y*%aS_)@%SchBo%+C?pcR`G5s|aejzq zHbt#$!PSiQ7JyPo`r(1lM)-Wf9u$S~*ptjYL%Y=%)kz^Cfu0$?7&2{~=n`*z)KIA- zGDXw1=TH$bL`XYa4$J!&ZmYD}U~3MPhDzUrUb)Bv?A_F=q~=-t(GGuxk*sm<b0!F!3}^$dW0( zJ$vIe9mlnN->g28?DZ8%inz*;A)2D`fVKS9O6@!fSQqr0q1+#>s)t_4xk<#vdm*Ps z?E;*%yQr=^OQYC<65V(j4l4|5OjC;A(Mo}JeNYXzBSQRDOMF+~}nJ%M(( zzc6F3Rw^92RiSMAuqVE!yC%P88Oeu-NmkRh$5+aL?0lu|+Qi0oJ=P1sl0_Jq(5i2R zqIJmjW+(aN?ue%ir1_eMc5VC?ESmQ#WQT7s(Ilj0_y>%+q+Y9UY4#lW@MTY+BRXsE zCWw5UgS{3rcfpjfwG3N_cpCc5XN4*YqwjTkG=*~Qujb`My+Dy3L zfD#mF$q>FsucL|IJcGu_Qm4y`kfBdr5VISEs9J}PSJ+#@$do~9N9w1My-|{UIHYbg zQin@!AtcfFb~)HQxS&`ogcELG>%zdll&zD3?y01#3Ymv zQ;Gb-VE0I3=rC#@r(Hfea5(Sr3xpJCh4-u-5B=f@eGqMw%wSuwu)qFk8QUM3nnph@ z!I-&!@$Q>5qWOt0=t4?oekZ4xj=IXNDwZvsIX9GU7S}NMDU^CONTl+G5?XeGKAR$# zY=VQ11_A0yLNEL#3t0bs!y8MVYGX6{zH&2bcE*(kWUlXrhb_**@Fa<*XWSqiawIVG z?vpT4Uj^!?LaIn`&h0fIqx_`~>&Y1>+?y*e0tqQ6x*tKmFirVzZjYj_H^`xkVK==d zHtz#KrhGyVb_IfE!v9aLFTqmU*2a`Z#Ldpw5a?F_Aw&KR5MRFnaiGHRf3z8VuxmoJ zp;KV*K$_XEeYqoWCJ9_sG}ML7E2|6Q_K&%Y)@OOP3s7B6T}*(y>czyPcTfb7n}0im z&|2-IN4N9^r(pm|0j2UH7kApi3syQ*i0GgqPJapE#=@L^ZADEA zBviq4vag;1MuGs0{4>se33h;g$qOI(Wu)=napd0sh}o;08W{U*kPrAwfLy?X-sDK_ z6PP>L%Q^f0DX{S@K?H4Q*pM_5wD%`57O-6{G$=SNee>3#>*spYcK{=3c-1UpKZ_%( z&fm+-Q*-&8VKvP)xj}es_|n=D9A*yApT@@E{t~d#uJEx{`7-t0=Nx}~AMctt)UL4C z#!*p~?yj^%34djuZhzmf9x|aTe=T-50HRxQi^8X*jOz-q1s16vFMbT(oKQKv@o_F| zIVDCE5ij}64UCw^x8ARnWtprd{Th(Y`8KJsUWW(!YZ$Rn#COaxFvIALlGV6O8oU0e zVQV%Fu0~NZuuL6v?R+B<7t%YWT6a_LcLe6=nLb}TH_$Ef25emPJN6t>%oix4R5D=p z$`zs3(ixKC+DYNUB;Eu_AN+u7Db!W|1{Mzm1L6U%Vy>8EVBcp~N8dBoDOLWi5zG}j zJ}Po#z^LFMAW>bBT|OVuA2Cw!DYsy8ZIM2OUBL{=g9GyrGxIc$LzY?+Jagi9rQ&tB zz6S&niL3>6HvlBf07zbp!~Utd0O_=_xuc=2jiarVzSCbBCtkdYACYl#mH^qQ_Za+B zaMNfmx^2s&5X_H?*2N$N4|9sck2U=@q2&xj9DS=Dh9p5K8uRfqqv`I)WWP2*5V~oO zWYDqjtMN2Vt?<6Nt&{jHudI`uq-z4?v6JG{cU)t$V3P5@mL2kef7-TJEzpkL$W8;K zw7GZL%pK&T0?vd}^-Co7GK{{cZ8myu9vtR~>II~Y$Dt5$r^e(G{)~rl>G^^9Q#q}? z0Uz6Z361F5&(~8`Av7%lEL%S}iKKGeeq|Vy1*tu(i<~2iydjt@@pIe7v2RZg94JBRs#vfhPkxCiGpHWUCH&6~l8VbX+gNV|D=--$3&x(G8J#F9PQIP8u z+@m+DH^P8mTI`HlYi&^ng$!k8m;QLb+-_lmvf1=Njekm>5YS|B$a;)6Q|K!lLI)Q? z2j^zZANNFju7&qQ|BR-gp@%g&86yR(>f$Zwlt~Y9RLQak6S6}JaqlDXGSrQkV&!@_ zHH$J|(0N#*&ETAqtEyIM6OQssLUOl2B}VwZl$k0bF7i6L9mVDr$RaJwG^pH}s;;x*ul8Vc3}T{jm-q_fps5i_ zc2L^OklgFs$DwVg>Ol5gf$djcuU3sgk#NfZnSHY22{UqX)(1VpPjEzwiy!?EBsRRP zsNLIf&$Wu+$=VY#JbxCCbkq$hCYZll<>VkxpCgdQFHjSB*j?F=`6Hb~aVe<9`V$dV ziIy#Qu+gRY&1*|8MgWi&VBrZmK%lAzX7#ToOhB*#GMbI$^J?~=QwzY<3CMMSBdL9Q z4aazyXW@R(vv42$x3APZw1#gzPeaKfA!@+hWLSP{DsfR1u9YcZ)VyMkG*3v}eAsu+ zGd7ZuF+SUxM5~PM9OA^(sFEskS1e~EV=K2hL6vu1x$W1%@EglG`^(C>sY|A>G_!kS z%VN1+SoHGFYG0XWiz$AJ&Zd1U$8g=Cu~AsK6kF?|NW7_T0(_n^^pDvsN{a+OdPy6- z6T>h!42rC1IoD4SuD&arJ@ciP>5Z)w?uqxVC-1q#FA zoL6w2LJgXJkZtE!!J^YnRoUZG~jQ&RwEttJZG z_#{@voQ|a5)G-(95kAdk)_SgpC6=H}x{#1ZRu z4F2Vmh`VtipR@F4q68_kfBz+5ciu zv6+h?2RD5r3q5cm9&hEvlp$lw9GR_Cf>fGvL2-MCE2#T9=qnJhAfNYsnt-x36kLJ5 ze$*wqQGsV8z`Z5Ilfmy*XO6O^~Svr9C!+zz*BgU zWDC8tuYk?d%Z)5Y$7k>IzwrP*zj^?H@vnL8F(NYKtb1$l0`Ym5@0}!K^2j;mC)Evv zPQidvS+=WUgHV12_Gk)MkB{wKS()paq5`nlF5A+k@T zfKa1y4DJ!JaF^$wW}Wz3Qz|^6D!d;y+r)YGKYsD2t)*hUI(!kEQ^QAU?I` z4-zuwyG@PnOdZN@L)X3%-8e*Cxg|m+y;7qY5e!zN)8jOhf?Cw*y2={~P0&3flrCny zhkat>VbZX2VG7dC?2;s{@$|Jdx!m+t%WgSo1ZTR11fFshqlK1CqRb6_rgS0w9%=eO?%i)YS+@JLO}MMg zUxDlU*Kk$(N2&)gz}J@dTs4Vl;2@VnMf z-vNFY#NO_qtr#9llQGsq^FP|jRFjc)zrU+o(Ori-VF(&DwDgo_#rFEZN7q=dllfFK z_KuXU_0D@M$-J2 zUq#UKO!V{}i3o&dQQuG`tKaPOu>*Bs_`U26P*5Z>iP@feotpl%eWKC)wHiE6=w#P8 zCCo?S9_4ZfK?K_!1oIAu5I5VchhQSAaqz5fy5H0PSp{#Wv>+`++_q#OpkUV zGA4rT1AAAv4|4LPoO{EbNOYW_5!;oXju&-eER_|@5ijl0(C|;9GRA}n#>Ai>`&WX2 zu2p69^ekkUVB7)b$O7VU2yqzZdFq^+K`xM$^y76JwX!IR5)1T&WDWKBo=~XfvuXyh zs_D%-(;-#}c!T7JA4yK%9xj@ein&wTRO4W0LhVq)4v0F*{wfqqCu~4Md7uh+P1O+* zj_bP>J?t0M&Co5ef6PvWEVOL0^3ycKpdiQ;ZQQOUrJOF{Rt!w7lNPP@oCvzgjTj-L zyPQhdEj-V>sL^Q@f@tY>wMq4iQvRAv14^tf_H|fp-5uE@(TdFR+n%bpSCn{(nS9F0D5HByNOz;;aW8&narHr@2-T{zm#{6-O#A} zI8Ny8VYSe{{q`k_#9OL`Ok{BeZ)9K@@g6z7k*nU1<|R?gb%bbtIz znG&L#n^kv5ofFT{IjM=t@trI}k)~oY!^p|PA4<7M!F41|y}fQEJ zO0Of3XV4oqal+yo(1$wv4iO$~P&9=KL)y)6{Ht`wIx2s{Ed(g)Ot9ZaKpu&jDnaB? zLMJc>*S~`?J4EEQ^B6(@_)9NjK4zFyLP1P1IHpg6Njp5+LDbVq6RdS2)#DbUG$pe+ zRp(8kSH2Q%mp)I*b~TPng8eQ#A6fxq-7#7-8Ht&WeV?$!Wgxa4dwT->?)ded`f8C| zWv!U0=IsT!{dl{t4b8(cK{QsnQ=1y<@|uHJ-NP%B|7Qgif5p$w3qZ#x4|I$#Y;fV{ z^BFIEx92AD9}4*Mj2(#jmgaxQ;h+3f#0QAI;lS9b9pVh%WKf^ZCduX-0gp9#ZeV?& z%zUdsv>~GUSyaI@l_pk70wEAj(CT3H9*c*2TvkvH(8FLOduMvsE)_jYMOPQ~M`!QB zO`x+1-V?xKu8#jGhzb;xT8;dswn~j&_TA(iHC|Nu6MvP?ULXl$d+)H%x&GIiIcdHz zrQu%MfuB05f@l^(cC0SUX%s#>v{_$W-XbRusq4Ns7}K_>%g3Ag>~+p;^tKpOtiX`% zqtVxew9?F5U$Ybl{(!3M?v9->a0b@1yV*0M5lIY_Fu*Bo9%JUfvN40ZAF?qY59484 zTH2s$Hk)Y1itzi72`z&1ndI{rZDP1L1p*+y-$XObAnV`&G*uYF+#Re{?6wC|EE$XAw0 z-E@;stohiDVj^G;4n;Pr6U7bDnWOg@n?fcNkU6Cd;QHp396y>eG{7}ezPFnXv!IZ- z)ny?YskswcF58|C%orGFH_Wo9AV+WvThqdrKs>)g|CfpY4d5^*ehA$Mz~Qsj3i)}L zC~R!^kG4w2*un6x#A4=GurmhOmec&OFKo;DIb#?Udo_4e`3YLec6sNqy3jgI228Om ziM$7%t9nY2jJQ)zpW~k$&7H36Z*`Xer{-qu;Jxk+FZ(ert&aPErcRc~uMn;`5|ic5 z4Yv)e+QTpSNI3}fs7d2a9HTEo=DxH`(G|T7~_KkAPq{>aH1LO z&djiF!&BKb_g2WM^^_*$kvY^>l+o3&P%yU_|DcYG{&&^`viUB3^X>4?>Z*L?GFbDy^)Weh;mycnJ1*kAlC3A>RUx!;2B$D?7auHBi@R%R zs11HObD0jQvJRsKZ*z#sB?6#m!F)x5T4D~tmqD*y5O(`g9SNdloMwlgfNRZ`Z3>Unob--^eI7DZNz zoA+=Z)lMDOqap}+;__^|ueJ^TqUd<{%TC0*mO-D#IZI2i3>c8G)P_eHmSHz$99n^ASjf9q|*HO3ID()CTeZL z0)HjZ3uj_QN0TrIEwr*0`jswt4D4r#${K)? zmpPM&ld*%1zSVyc7&U>SlLN41dtovGy79kJ+P$8RSH!<$v7lq{r(kr40g~n4=rE&M z8fGnllW1|vllcab)`&k1O}0`1z+_mFw_nsk7)mjvx#Ov~wD*cOe3zphrX+}FC|gePY<{U53O+)qYUqLX|GngpRahzekQJjBCX7?E#3k4 zT-teaBo*HEOcYnS*VRgp=3xb4ztT(vsvN6fWYT5V!n6?Nm=k*E3f4-4 zvBBZJ38OJu^x`34R+D=X>bJo1QLRe*g-f;)I9zBj?}BMprFt z9cj+ttT?Aqd38RMqUoZkAjZ3sXABzo1V-J{mIM)v;`}jS{^~|QZQsmX_S3oRii}Hr zNK8{#-Fg1dSW`>rgmwI;Tj$uJ0wRjcDwyEIsDoo!JAL}>Cezl|VKHS}{C6wz{9>5H zKM|NcATm1`tqi2|pC)$`NRI`q)f7Wa`n`E9)?jxjJtT6kzs|}E;-7siMf{0ny5d`0 zh%su5_fFXo!yvLA&WK-S2AgY8VM4Vk6wPh2@IS9c zJ|sX=TarAduL4kM3ZRnwIdX{thm!#Lz(0RqOqu^n3IE@{^uGa3<5!t3@;{4T!2QN* ztt^wk8)qNKuZVvQ&HIf^3|<%>xw&<1QF9-yweP1@?b`{_qYvn@jV30hCeF_9uBB;~ zzRJMI>3>|<+Gyijr_%0TJaDB4ZFXhpCi*=_YlMK$O{EX53(eiEO@J3oif|sfae2o6 z>4X2LC@~&FWa{4!kQ>?&^0~$HorLv@Le+mhlC#9{_!;$U@y~9XbizYLv_XQDiEAZz zqM!&X`hWSXnE6F({zmsMg}V#6mO$$udhDDHbkuZZG5`A48y5c`t>t>SsI@K1oe0t9 za;6oo!%0W#m2%{o9A;G6IZ*SRaTpqHET#`t?W&s-O(@|%U?>h!OX^y#zFK9-)cRoc zTS|Ya|E?#q1J6LQW|Q6}fcjBbm$mZqm69U%Lv*I;fY-}v`s>V}qIn0H zl;=TL=h6p74=FRke{jSh4xM+R}6-`5MAh+dEVz zhj_WeL)}% zs&2gh&ZR~5pWWgCwbOHbrDAUA^p~z$NWDUlcf7zegD>zH`~jbRYfUOC&rYfCA|t!`bmQFlLF;@iIWLb-K~?46N^6#i1wM(e2m6&usVYD)gOr|00Zb)%CfSWBvZw@*G5sX1 zci~57GD%+=RMB7Cc~KQFxcB`bar?fbi? z6zEat+i)#xi^hDA8`j4RbO<(QCGw*7xk@=71a@zSC~2i-YRD4B zq*gg#bBDGlv-gNCk<JAr?hf zaP{zdII?HWFC2bPX_>CH;hyK+Xo7PmAsD8MWhJI*<>(paFrXUJ z?{rdp;OjW%%jLxpT#{!DZ)pinxcX2xI4iVN-4oQ3vlJ6;<@)+vwM$QgT(Z+Y^L@Jvex5V(S{7Ucu>rG>vc=?<2WI(?d<^_D7Nq5TALQ z)$>ZM=V-rUlI-1-zZbhmllJHRaNbjvW^$@9Wr>@yOE#fXcB>D5I3M2J*F&UnB*aQf zoLe3mB^Z?`CEop0;?OhTl)7t`$YS?rz2isww;Hj>CWGM(#`AL>IZt2jRW-N1%ES4B8sO&evES3W zZ!m3FYd2ty1fiboQblm1-ja>dDN@#F_T&Uteax7E#bdf_kX`ij~W^l0kE3zDRp zJz1bV6VQ$X#*q=#NlQpB;3SrdPL-Ru$W>x@+X-I5Z6NQ=h14#@vU)1d14+p$R5#Unc3+Yi5_@>XL$BWmS`+r?YL;z*yUpd!M$Wze}u7iqq0YXnkg?EG|qOz!AZck_`I8D>O|{I*a{MlTW(1zYk3QMOx5|ED#^z&)9~U; z5c~PVxu*~xsFuc!;OL?pd}3t@~--FUKpw<$r%t82<%)X%}bC;doz;?+1uYDAEk!LLO`>9DNM zxf9ZA$$(F*CA*?59tu(3DFzBcExMA3#h5G+#1t77Q^PX5bFQOKdqxXAP#|OdXifp? zv9HBz*6wO39U>{%T?@6_7sjiD@_6ynZMk8{xU9>B2v1c(o`0&~vLry8x@5XfrIguV zUU^`*NFmCiNYNQXUje$h5ZsoADE*j#Btr15li|m*@Dc@A1wZ9!N~C-V4Z6>Zshked z#&w|P&ZCz#mB|KLLdy(g`^jblIp=jWls<=GnWd5p4HO)6?+|Ru*QRuheOrIh;@wyf zIx%|JPACuaqb`;%>If}-5LOfCzfFw&U@Sg{(4Ty`c;`Nn z_|HzD+TUI|k&*$SAi(V1;Wr=*2mr0u3k?&1tudfOAW(g-p8qCG*Zfr^Nc^ut3Mezi zPS-@7$Ln$TrBC&mKQF@DD>@k+#)~8nzB{uLcsq zwN@w(p2cp$_;!P~b8~TmdHh*9#33#uTLs&UXg;8sF8tTIf#f^%#a>;ODfBjRX}{|w zD>VG!jU&q6wuSMnwQd%@s^V{DQB+PT<7?;VHRN|wEYqveg^bix#xQ z6Td$|h!Gev7#;~;yV{6l4m0!n#o0pE)QRu}-hl=gzwU9Mpv8V3&>XRE4Rkpdh^CIWPDr?Y43aZY+SQ-&35c&l~T1u zJZrr0Lv1W)c>Qd84l>TAs1>$bEq5;zQ5z58V`zpJj#<`jig0vX6vL1#P4=UMrQrvA z3}$@iS}r(yf;m??f*G3-tA(GGS-9C*jnM;CQb?l>Ik%v!KtcZ zo$u5;4nBD(%a}l5zO#hYRIul0)yo?|$d|-p$&MA#phSio%bF2nqhv)a4NAj&>H8o% zYi{rmGrSeK()rejmhQT4_|)u^U~6jiAhOCTvg8o1pW9B%q+m?q)Gsg?yO^cf0D_@_ zmGEyx4v*F;g(TM|yXQ%dNs0V_MCtxi%ef_MyPT5;!V&a_;*ztH_Y0(r%*XKQMYkD# zW5n`BabCkxF335g3&>^&vq0qG6P{( zW8hywV=Lgk`*R#qv@|#VEB(^;tIQi2BLKh(7ytmUqP`r5ae`%cCTsL%SHe+i@Dv?Y zm>`Y+t#G-zS%A7ZKvI)<%yVzt;D>(3V>K$Gf##O!VcNpDaedfl)6dU7m9Oy@;iw#~fg~H$XO~&PR137&{8vxw`RCr% z%cH3+^_UA=ClHGwsObwLYS6@D6Gv}!tR6o^fjK+keoV;iT9U<>n8zGLJ|VqE9qZmP zk@-rv&NVlEiXg=dmXz~xDwT&<`R4h!?NMkd^|RHfBoGW8u4e(p?0&Dt)-L5*zp?IHRn&nR|#nHmpzvHJa z!oR0BTA?;@j{Dch&deChQNQ_~-M{0#wh@};6q3g1w&+z+X4yP!caG4J7|#`d*~46Y z?0lo58og&}2I8foz?ddlJsFTwTMXM z`IO`>ddKtMqWF60L*M1%1$5r}3aY4~-R4M!7qe6xpMKw(abjw#<(;IsKeiH#M7|B> z^M9X_PQRXa(icJZgw54mbL+x2`W4q$XyVB zfcEZL?|9&*s;qVj1nzCg>+h|h!mNQgXw|szMR&m*+UK9IHrozv5QeLqa;BU5% z^R^KjGhZm^l4HZw+nX`CZ?w-U{0P%sIokVJmjAH|9_b_7orXnDwx(>KWX*Pd&Y84* zKs5nqnva;T6phG{YzDz8AS`R8)%e&Pcc_6|YRd09*QG`{ZB!{rLc3aY_KPL4qB9jL zoC5NiBk4zG%zIAaX+Kvku#DfThM=eqeZ-|x@hMIM@$B$It{rYp4Jq?vDF*@{(>wwDX%}AgaZdOyZHSY`4j+T73JiAdG4yoXr6U7&F{v zB!7^ZuNk@HJVDMiuJl-t?(c~Ni2{Orjka@}@ z6r$gQBKiZK6mbP`f4uz_$OCmulGy4Llgw!7nnQx;e&p+-BNeUSu+hqAQsR96QLSws zdjeZUtbsUFB*8kZUf#2*^F;I8>tUS(Oa`Bse%pRa1Al4@=GscdDJ+w(SYk7W$1g-w zlXFNN**tBt%!L5{zQ{`grZ|A12C1%dRvL1F%Ev=I4VpQ;z-wybmEGqhkT(*29TyOQ z_B!^zeTo?H1#JO-qk|=Z7~qiSXl$+jcW!RLz50^gz}yR&0k%Dw`Khaw=R%={S~u8% zzq%7mi9fH5FzP766p1xdEYwu&{McUMLn8$H_V=+X&#TrJzvx(=w(kN~Nb^5pWtosEXAb=CfTl}!TG57%+j9r~mj2*NwkgKI6XWQS_$2k@ui3#DJ_|0LZRbZ_uAmG8o zs5C$#NGaEBBHn_)IH_OFlJ4af(!P~txAYP#d2_VJRLIa38R||w)R|ha>+I}* z^mem6{*tqL++3_ZU>pGP!lyjdqva=XM5Jbd*zs> zNh78B4OF@r*CIL0l|Ru35{Fv}0`&jm?Hm8|+=8x~#s`+^81?Rs4po zb@s&2!EjXW#_v^QP4Dmc0!5ZEE(Rni`<0X>T8x95#)Q&a>X;i<-KTlXbM=Y|)Pf1JCNLpJ6mC5pMY%*QThHA;b#+o ztSm0UXaR8hUwqg9aKQk1?U&}7iUT0D{dXL&_kril6oA*4U$lbUOLy%>kO-5YRWrqF z(Iiwkj6URstVvT*!ku(~jpK+YfS7OeA!9%`VST-%PyZ%x2js5q#I>+LoTaix%0m-G zqD+ZV8b#C%OY~DjFN&kmnz?E|a}0c=usk&^=(`#?@?|oVab)K@Ulq8ZIa>a5mE2A> zo}T7-RGysj$CKn-7}iV1K|7ij&kO}tbN@n*#o9F^SMPR{s_VknJ68w5=R4*PoQniL zT{>{`#k26N-1^qwjq@7(9{X0&X5X zZTV+s1J1rcJx4Uthe3`=!0`zUA}8)6&92oDre#5M8ChW)e+QGNks$z=lGXEr=9{_R zrdN4<5J2_6)fftGy~l@zVB$s#HKmoh!BU+HqsL&Q5MsBmIy_8=%Z~h{kQOe5{MD7j z8-3a(3r5?gdv+IDzdrMZF)2=b3s0Lp`{+(u|-lB715n+n*z%A<- zp^G`XosZ2N+*lKnba-U-n-y?`e$={2Mq!7$PKUCcDM>l0VDzl#_9JulAWrWz(yTN6n~~8FR=OhLo?rhZ{5qy1EwP{ zCd?hUkMAI^<09*btf*XFtPwIXgh5XdavJ95F_PKLNxz(bJ56j4hVY&mXixY0wY#!1 zsbp3}!9s?K>Zdng_?Vw{sds&Ou)(r|!WT()u9r~@U{h&qzQ>635w$BY7E~u_-^qx z&tqqb8eBhjx=mO5Kv)gIE^Mvn`CJl*dV>hoE=G*rn2{r9xaunr#PYVLib6GZ^3Tnx z*wea%?;mU79$FO6rN=ppIg7Ns4bjhx?qyJFQ)8AIg@g+J`!6do;^$kbsUE5^6}vq$x$`%NIvv>sQWXs6exjG;7&Hx^vn}bTwDLuJS;s zMZ@N;o=gh1N|xKExvKk4R3liU-wP2F|8{~NK?9wF{lsTyBl8^`83 zRcUY7qdQ!2aCdptxMT4q@>f4&DD4KE2CH`bZY+UnD=K!M!Tt+6NRTIk_})-hbetkpnwjMKwq81DKgyXH}2U&2>x zhHWhONW`T*t#f^JWwKB+w}vslU9G8DDMYEY$HU2MvmCnQx+nRMh{wb@5oE_<_>{>s z{qdVFIit4)U1GnSGJccE)O1H=TPzQm=|f*88QBUJ`borpGEiD)ZP?Aj{klud$c9^- zmsg*PQ};{C`sAa~m7s?)^|C?ng9}*Ug`uBa{Tzf&WU^$>deQ zseGl`9Limtlp+vJWGhyH4GI9R2W0`?ri)k5k zjuHJ-p9k?*Rb#j5IZP~Pz|k-0ERgTjSlzM?q`W-MD!tltwBKt|=;a_rY&(U&dJSJo zLDFbSIit|u8pW>^NwKPSP1xe9GG+mP6fP%~I+N~J-4hRRH*_R{MQVJ*kRg4y;cyb@ zs6R#JU}X|8u)(pDQQd$yW&MRPwcs4T-B5%HH_4DhY~Wx3l3;BWb{PFx_*l`5lcyt~_8z-=e;3DSYi z`L*MkKg8&P7Y5F26$6jnjH_3> z|5m!dMy@dxhuqSdY#^d*)0>kYjjyFgM5&}DG$i?af6J*yRom>ae54+d(1Ronp6eDp zZih|Yh_}4;Lt}pabY41Mfav1!m8Wdm2EM4y!~h3^&H-+Hgm2m|k&agt z=pKGbLKAhwoyFc{;G|Wb@J=UcL5S?yv}K9)D`-lUAr$S$cg`4%Gsx4!bJ2{!>(3>5 z8&oAZ0s~7~R#?ulin^^<* z?XOVAA73Sz%Kw?G0MZ1FhaY9Eyr9D!-;$Ofoq&`%yJy-NZ+p`4*d@Im=L~|8rl#k+ ztEn@2ZxSnhxnGp$(?H+AV5N(xo?e7)MJ>YJ)>5oeKoOrr;0zh9oX_?Gv57W^r#5<*VXc;gB)OrHe%3@g%A1LbCRsL- zZmMKqwJPbdwDz(@z(Vlbc_oLH0<058nd)p0XU2N9VS{1tVSvVFT7T<@GMwH?d}$K| zJsdtRm3-U&mOux_Z@#vo-)-TeK=g)qsI}XH&c@}^F zxN8V?OH6R3v#f@_Rhj}HzSHmBveX5}%91a;aXtV=e1`joYR4jKAuEAU=_UbZ_vr@u zKYquHe#$4q{_=7F5GVj3{%Nv1zc98XUW}fA?z$_WS@DNN2B@k0jYkLftLOs=?7WCd z_noFZ;zR5Jb2b=V90VwBA%uLs4}v9|jOX&3$Vb_h9(F(0zOu;Yl~NPYKUnZST;HtT zZ-dzvusTL(ey8g3wS0TOz}bVy6;e0-#SzRAw}u!Wnni4o4dQk|ym?O%Bk^HDbjkIc z@u8XNH+5u<5zNtvE6q>F6ZSR-cS@1*)gFk##5Y)YsyU3LQ{Givy1v?>m93D0)HEr|9=CtTbvF)&&uu7DZd*JdpmZ z%T4L?i27nazo;U##41 zXf7NP3H*)r@2W1<4+OQ5M$ zakzFqL6puA_43;u{m})UBQ;U3i!8lh2>qYbv%=q8v?z!nh36P`Go-ff91w|t%!{3Z z-P7Ru8IOM*qE(*>)d%-2Ar z9xMb0Dvl`IQGcmcWBO_ExGM4dn}l}i6fR5W(Dh;R<)PNyJq|yZgHgGBxN(4Wy?nZy zf49cBkjyro=C>uPo>_dBK`?>LbnR|@8BmbUep<8$nczpaJ4U}CcQ&CU;E*>`1l%*1 zGHVB+<2XeWu&sx~vH%^{yN+|CpzXTe4RGssc)q*)nYHtU)=2H3sze9qdyR%ntR$Mu zbg)1DG7(!cw)&B(&#Q<6Pi*xuav$g6L#CrX#+R3iuJ)mWMKmWcwk)%Q;~Yx`o?ck^ zl1Ax1LXz<|Ne4lI)bF1~tc5)z7P7VpbEF1?PGjehv z!tvz4E9k-E`kHv!?hwMAG*_#Ij*9LcFX#8U**7Cz2b*YkhI%zVlG3w`fpi}^3daQD z(kn_^(XLN@irP$hEO2LO=LT83d#^b(&-3GFwH-f<&(qK6I+N2}u(WH4&&YH$y#eoo z?ot0s!t_8A=B@GicmV{=??Ax(BVzUfW7AO6=GmxYJ8*T1lJS3y0j+Zdh9C+!M19Q!ZyhgkNCjnxqSA*zX1V@Cwi~yf?#D|}w*e?rrVMX8 zC`UMSu%TEDON`Qw*|`x}Kkok&e%k@J_=CG*1sN>%vs6kEdrjahT##x>;a=*{lNrCm z_+14Kr}Gzt_B|*y!}#MU8sRTVh>KZYZS+X*q2!-nCr&%I1ht>FGV*IE@=R7ZA%0%z zq%#nPEb|L-ccw{$SnoRCG=(9R zU6|l^fE8GLrk&j8DN}&kv;9& zax+2&U^N-&s?+}2!1^0+M5b53Oiqxq0RRzl`tcDmnZSKW``Ss%VJXkGn!(a)+`4df z6^~|KKJfX;G;}JuD=$@3NuO$*#)~_+Web9!8h#N{v$ww>eFs(F>+KJZCP(P%PS7rC zE19!7oq5BG^>jTwjIAA8T4HGRx@XfZwu&LI02(i^9H}lurpRW22#K?skrG<<2p&O9 zLdt*?5kieFRKrA6oiLrS{zq!%%dSssk`Dv3VS`)r1q~1B78$odxPEibBU5a z0~;5_js_D~kV~o-GAP9xm!G>kxNtpsviTNI!S!Y$WHYgHwnMdkjrkc%n3z6tU1pPj zO!)7oru421H4w8dT%#an;LqGMmdF?F83c+gG8zodWI*X8?10$wWzr+v3Nfc%4OImW zZ)rC!TD7oIrO$T^D-_gvs?zIuHT$VnMP`{lJ5Pq0I-Tgh4cpr4 zFXIrVE?d6dQh(`JB7Rv-Sc_p=~lJ-u?>z3BP;wbLNEUlW*dM}r^bem%)R2Z%Yfp+=4p=AgC&;wpf- z@G_g?+(+bvvDxZHB{}EAt3ck#bHdN#yEH(3hVbBMxUlE7@C?qEJ!EyoO&Kh!uVE@aiQgODg>_8bf@@bj1r{+n6DR? z&1I3fX@suUuO@X@qRH_My>ENje!s!IsS3&}gMJzkTC$eC#u>KB40X<06Xf3i`1S)i zt?D$vr{Ax7uh;-8<<;E?%K%g+1E~BnSOriiWp85ZW(vqo%l(xWhtexZHUOHje@fc7 zexaCm>ybwjc`1C#7D(F=NN$4#eEq9uV#!ieBZ_beG4-@NKR zf7S%IuV#JNHty!;s63C{h{4U-dORjs1+nQaUb!Dj=)PoNUbbn>$7b$JPr>u2jSS7uF3NBm?IOmpuqYw0j*%6h{9ab;spEY9u3?Y6n*%!0 zXIUjI=%$Bv&#dO76%uyC!fD;idM(1CjKPSw>}=XKa%~9QpCG@JNH3T*z=#^Jr~@Tk14A85 z2VOjPG@03#^?Mt`~_FEDZ;6fy?rh%eL|~i$D|XXI5$h!2*NtCI=nK|fK}W>AI+!7FLCH4h80UwXZdgkd=|Ay2 zSWBA|3fd+6f!k;nIx=9#Z)m|$(VD=>PmmFzeaQ<(sp>mdKJ2t%4*?bBzQz&L-l1}h zlkz2vOk;P1FCnk%Mek5a5>I#D^{|>U9X?KJUWcm!w3^BX2R*(vSqdr`_=l1TRu4BB zKG&JDCFAi`^}P5&xT(D6VaeU`Xjz7)cncXNWq#|hPH}9H%1|UfhHR3A1`Bdkx<@() z0v~Tp?yoAW$xaQA;En3L0kMuYw!DHA@4XHa1DI(t)wf-2#2yp8GMTdpL!+6M_N*Ll z#CticWwI%c*#KttSl8 z(C7Q{_py@`IzCdXgSMmEAF@j>xO($Z@4ZvdE5G>{D~Re8OMJ?nIPDEi)u)1e+Pt%^ zS)%>WW&F(xGWqwI1!4l+MzU&2n_zu=ySq=_fLPDZ-gpE&Q%z-`VgX&|rYt!3C{Ev| z@58>2Jr?J}3|UDB2${K=SH6vHodNf~i$^8ZE)+m__V-y$NGHaPo-=_f=G&c+QdbKY z6dN30drG2Vej;dP#-Dq29TRg++l6Y_^Y|p?%HV|=r~^1z64bgy#2>0#7kh~Hu0QO5 zoTXJx7$N_dru0XFqi^Guec3(HxXrIec982TNP1ld z8?a@!%`Gf4w=Q(s1E+ur=@Bf{X@#jD@M>e`;CgAJLdHX-hoH026`!c)d6(eW*v*Cu z=PvAUMPo41oXW!;8-kzzdsSfqAoOV#v1bGt90)*z1M6Q9%9y?EZ^{9@RG zh1A}CX&7O6tn{Bil<&U8U@&O(aJ#bQI@h*zUTDqBOOYEkeY3S?^i{y6aEKdQNv5D^ zFbEWfCbL`uO)<L@@O4vg?^Z;8x-;G~_l|aWL5#ob!x>BG|={ z&1B>vBoM%@M+m@=YLm=pvm!C`P0-(c?Qx$Mq+>RrY#3@t5ko)CQp|NF zb-T(=EpjsiwZ}mj;b)L5Hp@lyXRb3GJNJa3^$PrTzep!JK4nN7PWwO9T&mMN)FtFh>7Cb?OqR5XLuv=uKIGeCxw}Y$2%+?C95>aqwsh%D zV7S!D$;oP7R&v<0^E!R`;rVdDxB3RQOF1f8b?dY428as2 zY~6EwsKFMp*-uIvvxt!$Pu9~KOXB)pCDBc(54m1hnaNhldM9|)Zt^_Pw=wb_W5(HZ zKUod^s=ch7vYGsUv3qqV22pwr^f_OjzpGvDyq^~=Ymfx?fk4Z_a3^bct(hLW+Xy~2 zCWi!3r!AAajWo_))rm-k(tsPFe z*iW3Yc~^yAF{sWt9Iayad9OR4)4tLZLwm3Kma{C)O+wi}9NP+JG>K|ICVN`{JJ|`< zq=uG2l!S~p8y|eJE4lq0#U*55f1#Z(!d6aC0^bOb6kv0X2$yitVSH ze$FXd3ZSMTpM6xNemMTF?>x=LqA)j{o$tI@53Rip*~9c3eBa=|fXWI4R8PXoo)Ca( zIR*Uw&FX;qq$%=u+R@ggPjH&7S&I;ck0zyt~+iTZTC$Es%2mK1JQt$~6d zh1f)-^(GBi#Xr(oUTa(YRGpr^(=p`2J1&m+qG5+WCLvb3H-4(j#0FCR!vkpPcWX7g3mrT@ISvHz!Y8{Ht5xH4GcOLlud@9k zG@PAQhpvsUy6Z2=hjJ=BMz~R|C4$W{u4at$lo`Gcpi)sKaGbJHn${{H8h$dH_x+UqUGRI zIuN%3n?%T#-q9=guWw|EV*-zA>;voCTB#xzyqTs|R(Kis`C`9~Z06>OcL+ZTQAhGn z7~9TwmD5)C46(Dh%Nrw_@ZjF6cMNj5pX40E(8Nh>cg#nJl=f(++E&*?4@j~*m7PVZ zWadR^gpNdAN(j_pwmZs3eYtn}HUXnQgzsDpk19V3v0z@%QP1S~1u7O_f|adD1{bGR zD3R4I>5dR*mxS9H`#P(&I7oC(wOMm?mcfyIURu_59j|9rOpEsb49&Ab)y%p-6BlMa z!7Ux=G9UlYc9s(dxR&Vsoi4$b^c_+IT2zstb@4&)i6gTWrFAGmI2Uyd=KHjJw(%$D1*V?0e@+Jf9@@U2J`KMAV{0aUvk!o1q*NokB>pcZC<%XEg-xrXZ&b}|c|2k6WX6?EA^onlqVqDxgJm1XIBr{`0C=IGh0Zq_IdasH(#FF!*8AO!|p3 zb{q3u?l?P3F7l9uMzsnzKReb|yrVHsxp`IIxrNQkXOpRnF|01bG~FOJzim8jc_ZfA z+*Mdm81KC2n`jYv<4i<-r@Hg4wGRaH`lAzFUsd}^-M7QrLVBcR1SnM>`@ABZ?FE7m zl8_H63q{D9huc^|zLcZ>0ZYv|WZY8$ibtW2I1f~k4Ffra0$XJ3RR4DhBpf1S zS|!TjUsGdnl__ytvxA+7uNr`+0frtPU!4l4Z%Ku-9pD(f^ z|2x!uwFVyQIk8D!PXk0(l*&K-{G7oCqq_dDnprN2o6aOJuHNJ{2~B zr3^&=jC6xTs`Led6Qwm8atGHJ8buD_tEk*QRK5cwHoxu-qiBdmIfc)8CiNl|UPDkn zdxa)@v7utU-{Q?ElS2QTM?fhbmm4hN3^YouaEaTXWrz}#M2`+k3LTz(Z=?@x>^XYZ zQsr-sEiY-wGqo;tUZ74b{wBj5CSfIcqTV_7X~)&y^UKUNhn4RlK@cT$3`!w++ij4maZK3RJx}@+5g_DZ-i3ZW+4PTaL%3te1%FJcj&zEQHWlna*dd){ZWJa1%jA)YhEkV8n(u~b+McrV}xpJ z!zIQal*v-sRo@awNiYLTm&FOKVJIcZ{DhhAkZDsrx5)jK8=Q1U1`^2cx+7={&T|%7 zVazF1ycQ+$JrbXONQS=qzb`lTp|8n7z^m;R_@VzPg#G`Oa^+IUgg71II9g#fjI7+xfo0Uqgmsj4 zz1l=n6b;RsY+^*Dg_LY+L@Wwj0t{-1uWG9mptgF?3^x2CpWp_B_RRX zZZqrVJt7njdT*1!qsaRE3;6i4hLY!}zDk|Jfr10xxfL2XqpVb_qwfNcsR%9Y*(Qk; zaNpt95@t>woLsi)W ztFx*dt~*Pwn&J7I;mE53=^Vh2{o^muDF8z++S7kjUL+k{jO^`=UY==gz{>qUfBPFi z0)KVa+<;jgppFRioCK;ziBehheq>Ln=u|*8=7BF3@X%nO*2Ia4B-q`aN^oCS!_5yB z+)94Ep)@e)QQ5{JkWHjAn@4bC^k?zkNdMvJ@&I1-rZGzv${d`>{sb?%Z_}<0DF|=h zDy?Y+ZmbKF$3A4sID$=>a%&1$52CD{w$Le7R!#LyT7s)mk}AJ9hCAf7`YBdplHt3_ zBHji*l0r4K{O}NzV(55Dt)lP*-D5J%XS&}Ycr{Yb|!n{!|&VplTSId*Fhzv^DU30z5t|d=AS|Id*>b(4^{`X# zmHmksA3qz*&8ZYGx>W-?1Ww9te|FC4PCpw z()*8#aEq`!Sxw}&M?);1{j=H&Kx0BKPR~o$@K>1ch}zLFb#hpb{Bh` ziHKbLFSsKD34Lq5h~_Pj%oF}Yod=<~m9rh85#fvJv8mDDQhMQkjXLG7m=~X8jYEO& zjiG>6oe)>vPmpjCtI4;JtxI(}3dXPls0;EkzjHXOH0gwO6T+W%M|~We-z#L2()ls# zowrQ5TNXkeoy)Cl$EcShUWQoGU@qRyB;-e5PG)S9jaiJmJ0e4(ygqt1P8q-aPQ}vE zH?Vd837ZYk+qrsgoQ2KARIP zCK%zv+II^`3z&w4Gp*V8XzcUGjfX@dbC4)8{n!ay z>az)tK&l)D%6n_Ntnj{2JL9#M&iqb1_%#FG)v^v(n8V#kiw}9vt#}blL+e*1xwy|m zm4Uul?ePV^(s17~J8>K1Xd`GQfrBVEf``;yC~g6JA0~$h@>q_UVzxm0q4k;q@!DD# z-^+H???*65%C&qVE1kkkM%gLrYmP5tKt}+n z|1?AZ!I*!ztS=oTX@|d(VBfq#XtYw7G$<2N2g9)d?}7+}jZFjOf(f|JrxL?b$n&_2 z1M&oNGO4!XHJ$}~p;Abrp|R}_F9&88oQ>5sx?*F%%rC7;hemex>3ll`%V1>_C9MsD zMS*zJ>-2nok(WllJU|H{Wj43@{ho=E+y!mETPU~qH(j5}L5)% zNMVt%h-ycDVI4}8(2v1el0Ocg>$~5dDOFSxk6$|d*mcY4pt6HHYAi>HuuLt&K^8!l za%@XEGD){!At5bDS)2p0IlcCUH@X8<3dIWG6N8@*dJts9_@!WbSR0*JQ=npWf@V;J z>MMR5GJPt6qgoVRk7+aJpb+z^^7^I2!sfq?D|c}6mWWxAJOhDmj`GJZJ}Kiwzc&_p zhlyV+=5SYMAy@|bSl5Mf!hQ%j%|-rl-4|S8Z>zkR@Pq3Z70;b{?UIBC1fC2w?@?MCf07I zr6?=YrrfE88k1*Uh;lr(JVrYCc3M|g)*AYjLgA5r*NYqz^HSwWaBUQeOmvX|!s3`b z_j3P-tJ_obSa_p<@yd5Rs%zO;bHfY#6_>QPCy(2UiWuc9=HT2BpGWt=$}POj;bl{A zJ?C{(#S((QF?$fhN-SiPh_LBwaLm;S+FruilRy!a33iaymgOqd)r^d0?Wk!jx29+( zb1;tDBGiM=cWvWaJ~_;PX<0DT?~1q7g z#CZ(+ZKF+sKgQ& zO$}eeL>`iKOC6Ti4n|g4O6GeWYc0Wm%IE+d>P8;ldCMU2XwVY+6Q-Yq``tp``6_q! zOAoYkvj1w~TxN%l9T8wJLuMTiMZ37EzIs)*ykxd~(0UhFAYPaOVvm2Qb5evtb^tR9 zFs6O68~SIcENAt11S$Al!RM3eKVv+|v-gIRz!==ZwkAzqlv5 zpzEi`19_j7+dyGk-aMsRkuI4iH?em?=ZJ+#h*k>4=7ACm{chDMIM1WP&Z0zLi3e+9L0=C76hb^u?rH?R zLbL9YUklsSP*3R@Y2;3H86>AF$0NKTU*IP81FIeCKih!PKh}@c)#79c&RY3)_L6tV zJ~enr46XKfwz9IUqo>^|C05mY7D-h5hLz?>8?N*oP8}Qd$M$V5R+%%vl{Kc57R`6o z%5N{Ywj#_3HVdHF38pE;z^$gombHd#@^D;=z5)j z%w+N(T?j=!aGI>}3(-B-1bi8Nd?nQK;(%n!(A;(c#Mlua#{TIb{<#{;W`Li!yV+mq zAhNu=4|a)vN{oE=@v)eWTXd&AUhbJ zgvL1P%yyCG*Btpq@hWmBV~8l$?p|p)hm(&URis}E1IQ~!wQEOzD_i1`4j=2-u9Vwz-4g9$#@eyK&V zE*@mLk9Tw^pb5`2@T5~kwrHC%hF+kYb2cd$-P16a*r|UNKMV~yTpW;LAjd7;klna$ zdr&v9fp=(I@K%iINRaG8YSa3f!no9p0X~&qj9o}uTt*1zhV#wo4Xswk;4EWwV|EmM zxfS<<=}LXA{m~vXdzOfwAEX*Om3NPcWx2!T@^thuPO349h#KCq*-Asf4-B4#_*A*} zx>^PF<}G{sYj*7?SpP}g$q7F6ws`-&E_jLA*RCZ{01(HF)J0!_SPTI1j}XKkAigMp z{aIQ1Tg@egS7>y5>1PYV1$zLprnJ1SPKoTJ$`%ni^=wG2BGIOl+>9tEZM&{$o)ob6 zH)IMm$gHEp2c#G0KC#>apr5#=W? zUL;QZS&RkHhY;6cH`^|YLA>3Yj}5eS&eZ4LgNxXj7Kb$F+7AHW(x?F)R`ZLy=z#Nr+Kl1XWVRvMhk0bY1 z^pW-y!R9{MB%-7c=$_~)k^B(HY}8iwh<_tduFZ3g6s4wAw?HM;5%j33(e!IyD0M;k z%qKNkFxd@SsApSZv2;h3Tj5r@z9)0E!)6rV>ku?jl_;Mg(CnOKLDJc zKh13^LWvi;JwT(DwK50fPyw~*zr{54dIibKe|bNUgYOCB0vQfkV+%qr8Evs@AuO9j z0)NUbC6UQu@JP#VycZY_->RQ?AEK`zz2HlTWk^k}#<1UD;Rp0dmapvuzu|m6i(c!z zTC!<=GvBGPjJ$l~xCi&q=wl^W#yAFIdGPMOQQN)5+$J)C6mb_%$8u%WZXG;psII^+ z`KaEQ#r-9zD%Vfqq?{J@Rm;-3i8$l^+Pu_ZIK1s{j|vDge#cZ*Mu9KXZt)?Ds*9O zR9pnVYpB50@)(Ge4#dn^jAHpXwghyFAt>Hj@Mt#>_uxsBx1iSq+z z@>eJFzQl+x54qaho*T9aubM9}d6=`@mOBps91;-z{(YiKzIbA)T6z&`nAw^Dx)g+e z_K^NY*Y9;o_Iu?zw`z^(Bb6_xu!LF>^d|p$V%Av@PnJ2nk~?S6y>|H9Dd!Ld5=yG+ z@%i_M$Ns&&=BwpRakt5!u(levVf)Vl{^!_LUkx^K9bl7swldzv9+#`-oD`Gcy+hWb4mbTJ1sy+BflVY=ad|zzg@?_E~pQ~V0zyC6kmS@ebSr<&$2jEraBiQ#q5Mm zkwQFfzrcVMu0p9CIi_OOjA*cxMTYWBu*ZzQGin?VSMp7YwQQwS!^`<+NHfnXFu=wpk|Ca_p|j*FOg6?wJPa z3g0twU$7-RvzeiUxz_N?;y7@P;*Y*98s>`lis*DMuWKM}_NJ#_s6#q(oRGQDW@B!6 z9$nQm6)xjguVdV^ER6gY4ukVM=L0|1;+SrRs-{EPS==6W zF1W1bG<(tktf)I>94<4^@#M-acK$JOhI!5(b<^cSL0#xv2jLVp4!Jhl-loF8LnQAx zTNd(+s9@}(jQ+^ymRuDVJDJcKt4Q3$DkxOvkQu!VQ%SPnf`>qSafo#tpG!$N)IYMY z%D}huM$Srka}2{LX9waveaPWgJN361(CkEK-wtVp6o#$43|3r{+?_{dxnLAD{4RNU z>EI2B6k@ov6VW_aqFYj<2MQBATsy+EM*U%4C95;>dh< zQB%#Maz09!BmSaJ);t2KzA@vRik{k_p8+5j13>Q2 zrul2g{eP>IDt|}$&EZwX1QZNn7XjI%)A$SgoD@(@!&w2Jk19>d;OI*Pf|tCJkj5MH zGwvvNOCHD7Y8sU@e0`Iit3JPQ*VeWoJ?IDyaNNQ)KhGs-r7(PL0Vg0>HA1khHot(W zF~ZB@XI_Rj;Bp1wk#=KSprdeBLwMMEW>7%jtVWDluGaIfD2LBjqkB3^5wud*@W|C0 z*|&A|G>%g{!`m#YYD~&oHnf;J;)WwmT&Ep3v`=nb+@n~B?r@{@E+Nhpy{7Xm-ZAWfVvpFmAj|Ahe zzJ)mLN3O?*CdB8#XclYoN%@=wQh8IjAa@fFev( zw?Ly6!q$arh!gY(LI~8Mb9tpB(-Z{=1htiuDDoiIip1{po%q6$!H3dpPrV`ar0Rj#Nwvl&rDzar&mMmd*BlnE1uw) z0iX8(Xrldl$NzldKg#XEn^@V*)XdBdARfsAJ2-#GFa_(?C(j1nDBUkkG^CHAsh6Ff zgcTW55((fatqX#Bu@FUgKH)B|n0w?OeDi38GUiQlwDRy+{p#;fT!{R$veZ$cL1WBh zQ9Wb0$Y*N-j_E7**4wlVrA&y(0Nxz!cISTGnm*l=dCVvM%M?-Kyvf_4Bs;9@WeS;|fJPUXXSe4~;se+TNMuw6w-9 zNKhH&*%*;ZhI9}&vBluuw=Ry3%;!~}TJ$uHpb#>FkUU9|F_lFTF4!gGk=k0GJjm|2 zyywA|d8=kL>)u;_&9FL)tguWyB`=i=R?w&`)0n$1*VT9wT6DJJO@5D2{^6SVjJwVp z*V=sDWV^rM@baLG98T!k=r`#%$HE{+HsVE_8{1bS>X(+Ph!CpZi@pdo5cU74KKy&> zG%Nv8e;@qklJnVRHRnE zlzg@oH6AtY8WnRWzi<3V(i7N&^=XG5%vW#BukUekl`l0j$lKUW+%CFiA~(VxID345 zBWrZx7>FF#)XY@XFlQT5H6+TH)u!>%lh2&*Se?H!yH~;(G3oEaflR(C{9bmvL)%6h znW@<2$(pcOSXw097~=T$M|b;-GKK;&WV|`=e&v#O#b?{v=+(BY+2jAm+g}A#9WUME zupo_uba%IOH`3kR-Q5DxAl;qP-7Vc6lG2^hsk~qGoagZSU%Z!Ic;g*=uf1kIGi%oJ z9DGl`r+?EA9)w+&z%fP_zfJB#HsMwfypGFviiSVDIf|O=!n5I6RzUlJQGyahdoyGu z>PR2f6NHu-Af!xi?=7X_7HYU}IQ-yZ85j8-e74%~+Fyw%GiSk2^gb*{jaxDE3hVF} zK6~PXRk5}7UN;`sR*iR`LNm-b8`%z15ZV_eWsZ}%5ILN&AoCkm$Iemz%m}w**{be~ zMuJNpUno56B&{IRqQu0;50hr4Iy#`6@Z~e1+;)Wp-@knm8^JV5+jKIkA`C|(`*81? z`+oLY;GHK2BjQ>$B%9rIU-)YJmvv?=NJOIBe&QSHy_6hhdncB9ly+xCOPVXIF4 z9dH55XP$AFdiEW~7S`|wYS)q8>z|1#KY#8rSFQGjmoG=BwK;xNMTa3IvGUI~eji1F zHbgnYJJ<_9DyFbbhQ`<@p`+Wf+UCg9q@-h=)@bRuJLyD|c z9}$?78&gE*)GyazTIDm*YD;}5ktLvnJ*+$v9TwO`ImzE@?-)2Z4C95gcPeZ#GRVE& zZ>w>>pGHuh8oOp`CtsRuPfvD9bypc{yvA_{YkiP5{w)@TL!?--I*)HU7j*3CE&lwe z>Ak0*=cCD+gK|zxXrD%(n1D`3stgD_l@6lYiEg#@K@Mg9i_A>vkZkq zF=JR#=2~8B%)z}ukE}avd2dIbvrh`QxlG{Q8~M>AYjv#TjQCgV`;<1e4K%^&ighfZ%aHvGl zPg9qn-@k0DsJq;q`g80HuSs=jzn#Q{qG>%Jo+WA)|7xWR=AJvAS(zH)s}nbVft`%6;`6FyddUL?g}$YD zN#TYA0QaAP&VK;sP9Ow*5|2x&a;A`~#_qGc33SRRrI(#!*+Rh@uRp)6A`Y*d+t~GdfN1-SI4-Yw^nh#T)w>r&pR|U2;`OB?Q zPSRJx6E&tw$x^!IXLVO_DxWJ#`$jB4-t)4SJ`bT)UJVm6whv5P!eD0(X7qlW=#$pA zeis+>-eOTek0)*^Y}c@u#n23H2*d0MV}VbN=@y5ggs10Zj7-Q9TO2+!sIy#z$xP(x zB7LU^ylBk{Y z?ak*w-H}5PY43OAz%KrdPKgFu+8iBa)R0&gab<39BqTk?B9uDt{Q!FpC#oXT*b_4R zK;KOIW%&XGLm}u74rnw-B3Wj$ghH#ugRK-C=bh69_77uBw9l{3fv5pgNk%rx_5i4= z{{Nr~xPn#!raOPeJc|D+I@#eBsv`cIl(LTx<@Pg|%xo#wmyuKm3)P%!K`bb@r!Utz zI9+VZt!DijJ>?vKSc^?k-06!zaBxJjNphv9e0hfe^Av%nSxr{=ZW#UN#ujQDh}pW| zEmVqr#0S9}sM65`Ika%)cDd-^Wk-%xSM4-dA>X?1G0R+<$2@M}$6DggpXczS?8 zL&ZzQ$T_z!W`1b(T0N(+$=?6Md_B-D;Oi1R2b^2EI%e;IH@4hp(BLd)R87-+f=n@x zkJ78`pA)6GiQ3WJujymvck5hMM4UD$u!%O{wdumi&*I-0(EHkK5eBcC$$$&09O|+Y zevzUrI6>S3F%ll#+gY^-)u-LgAcDG0?#z=chga6;32TxGZc#q4uRKg)9#XkLZj=PU zgXXrZB$qkK`n>ilST8S{huY(cyA`j-CbeicVHliGE_i6P{zJ)_c+F}-JyS&io`G9e zvDLKjoLAoD6tQwcVU56aN^#hh2{VDpBofrXF#NS&%bLosSc4?H=7M@`zB!}Nj$tWd7sH&3|QmWUyE?*|m5y$q1F2LaMeid@d z6EkKUIjqIo(%PEd+(EryP&QO`_GT+ z4t4!sk1FGviR3YSA=(wfl42CP)2r%poILAPzz~!iWRPa%0=9y-T_{8}ZS;EHJEnD7 z%dMDm_qX4{#~&r+&5neJ9E72()vl$)(?G@nxfzu4!!7TTRh@&xH7O5a`%c&;9aQ$i z&iA{l4#K%tEm~J4Bn@hz;6C_7iAr9yN*T{5--&`&^Akj@v&0U$F-91d|HQ~Q>l0Oc z#PQi3KT<@<#^Cksu)P){@!+cew29Fy?QL2ddxxl=*-1=g<6L6K(V}3fXqa(0N3~;q zVK25#-8Fn@&+9}r5C<0*moIUyf3g7`5+C_olzxoWI?lJLQ?KEf{LJ;g0pW{@!?HD^ zxCelwm*K&mSRHW!8RI{<#Xz=`2;kY_4k(&|$)b~+lcKQdn|xrD^9Je-8hpc)p^=_B zqQ0IUgP!jjd;$|Aa3*yTKjHlxx)h=q;u+cSLurq`FS?PTgrEH|X=uBDX31yz#k|i| zfszvE?@cB!fZ-Pe8CCHM+4_L~Mp+5&4W#lb#tMw(d+EaZ09`yh!q4bF=?8N4m2v?X zY_XHBkDMsDW(O$R#OF$}u{pMDID%m`f z6)ia3v1gEr0^1NN3%)$&r6MKP4kQt$*!n1_{6eZ=2zW z1b_w;uxkBJqok<+Hv>?}!PfCl4A?*88@g8j@dkFS$RAMJM2CIcih{LbRpMcCuD}uq z!X@a7!`L|&8(US{KW!|ITRig^l1G0;p}Sw$OJQT;>S)QGpyKgnY&8?gpIh+UO591q z+m5Z^Ybyko7qMiLJ5D}T5+pF+rw&Fbhzq~I01f`RWq<$85MB1F7b|&nbK=*931=|{ z4ub6j{6t~g8LsCNN%^9qw0{KdH8J>+Ev%Hna^HMRlwkP}rfVrKR!zEfVw*fjU^%}{ zF^sCW;H@jIR*99bH+CNQ9z6qr#62b zbGeOTVXogD(`F%m)P&pRWSuYVSJ(N9l}I@H6A|JL#PmAw5m*b{jWX zD0JwD(xj3u9kg25`1izcd5QI=I3bgA0zMVVUF0xL9M?v2({W0c(8^fboDJoR>o>z`MrL*yi7ud%s?V z1jhbR8wLTJH~i$?XvtIq&0vs&VqCfWo@h9;N!%dZK;ojR6(yNVis2-}?LCNfSXJ<{dE;0-T z-ncr7pwEe!Id-F1rkXO%(_Ng60p3}9^NakYwVQJ}ex1=!q=^@7te^-W8r8GH;H^_3 z?1%-9#9m5JfG+Y*Gt9YuOUpRTGw!R-@pSLyD|4)uIoVG5 zU>q(h+%qvGMHOivN5{2X?5CM@^5`$vS zrZE8;94=0-(2Fxqc;x_zzD@Q|pIo!n#`e@|sn2suvb{SCsLL6}pZi+V^v|zCxb(v7 zOvuuZw-%Gn&0@)rr;OrApNpOdN9fceMK$jP6#J#lZ}qs5U_T2{gcMM4d0}2;I^>s3 zMHZ^Em{fNa(!}xH^k&+vxq4zqw8vKdGA^7Wuy&&C_6wB?&a*K?J!;fYvdHhShZ9)D z@jJo@p(FN9h!E}D9{Pmr@U_pn7gwFkYpAA4{JcOWKwhx z&EdQLmar7{7-vYTJ?%I0BMos$@a7E*3bgR1O8G=Kd6&pb_DGD+y{AJsn{!hA1c#ht zL?q?#BC?ENip@92g_E$qn(BBq32OanH#zfON3gyGYh@aBtJ(lxtsg)tn18?8KY27V zwoWh6gaRfGKn&}@_>{y?Ed@+9Pk=uGfXY)ZnDm#IJ}w~b^*t? zwKlj?EA~If-P!r&PS67~=%Sg!YW;(^iwLQ6U`LpWzEIH-4j;dltHi9EShzFmRtX9# zP&{I%zw5sd6Q}lM+%(%O(UIk>N`R^j%UdJHC#hkooN&4S7-)535~SadBS+aB6)PM` zC{+9oH_qk3h@9Ne_N+fIWfBWoDk+J){{j;X(q~HVE8Tr7cRA#{d0qXaa2x3ILOO)H zE=vLT9bg)@pTe$zkvyDwcDucS|G$2k2KcSJdSVCX0f68D0P)8NPyBB{(Tf$;i%apV zecZo*h2!g%DONHZ=&1+JAbf)Ta2)Ji4-6^u3vVVlC()%BdE)Wn&2XTNA~1*Gf8S)t zOR=6Lw(ql4u04r1HvWt$4n{B;|ODg?1*HU@{>30KepY59;!Q^Pt&LNs$)!$8rjN=u4KVu*y&! zZ8gS2BEO^-O5}-YV@Yz-cO;HI@zT77EXZN@*b_w47}lM2lW3ID2&6cY8mu3nQw;(z zBawc^?_LL*=xyx?3~MlZ@vxOAE=K)Yml|c5G#{(drpKje(y+ULwC3O8cz~`&J|{Cm zZT~55a1Y|)fOLnlyZC`6%F)5B!1>+fnxloX{lTr4h>rgmV!F;v?&;Kl9LY#zZozNi zh8Tk#$(zFt-R=8VdLeQEOP&WlYLGz9IR9VA@`qghVCkh-Q3Vp-0W%4}zWrZKl`LN2 z>1&)oIFfwl#*(|${JwyGBk1&ucM>}n)w;~FuHlnxGujfEZl9e* z-8i93PnQ`Fu0UAd%+=cFogw=jow;D?2d9G)%gUD7qmI;?Uvi=%mPh;v31Lzpvx7$L z6mT@F3tH&L21kxWN9Jd8AhA(jeuW4vS5UZAOQuXI-hCAKwE4lKz%*2RpZ1eiW(PjJ z3dKF2-pJ&%!eFRh9;-Yq*1a?f4o&uvzMurkJDw^o$K!Z26kVitYMS)Hz7P}#$6rCrd`4wO+g3TsC z&l}fqOxQ|KFoU5!u;OyZaL+(>HK6?ZX0;KE)^r8S4!0UH?p_@HWdlWOPVy@jod9if z@B{e&`0pj!BDvmBav$Kom+R<1XJO)h^4R{8pzMn^k`!;q=65gU)J68b#A6)azM{OYT>c!E&12Fb_c00RCoVuut+=0Ax58&;+-6i ziq{U4Wb|Ft!rsN?Sqw2Kbh8%KP5|j4?tv!pwM_exi?&%)q{U0 z|Fe}ynH4jN(#_AFp49J_H`;l$m|n=aU66Az#@{G(hvQgGLsg4@U!f$u6UbZssxJ{SYQcix;2epn8^U4{vDZ;10(qtNz5O;xR|;1 zzo^HIulO@Uo=3Ka0r3a%4qqG9TUa@H3<4=%C5__F$r4zDCbFa@qDbli=>(sN#SjHT ze(l&hjEN#~P=+fwVuJJ(nln~&Q7l<`xb?wF(FcT0Q6Zg%K@JpiUneUT?ubca z3gcOGKvAx4#{kNgc}BWzhLOt6v#uTHC3X2s1Kf9QeS-FgS`@8)Uq)dXU<~+pY-hH? zgFN&d2H;W6D@&|Ho@CQUar-gF*_fw&`s&e8g=eOR$#BqV{DduNh~BG`xJj*}x}URn zlzZWP31~*nA|Pd=Q6uuWZbm(85S$rPt=x*$4sw}t)?H?G%Wff_4?a+Dd)1bty z-`9Sl9vl8~s=Z2jB#?OGvF}sV-5JHJ25cK3APU@{$}a?j15B*`cH}SLGvHlk?)c|I zKpIGj{3qkg<5!e3ryy-R`}YzRqb^uW5Eg~qO4iP|d=?Z*nS*P{$Z+ml4e#vZ{Vg|n zu^z(H8@g8DP~O|PqVhm8K880fx{Qo-G>^^Z+tUnNb1oYj{yxi;_72XkkbbI}9w~&~ z6hrmnPp7VL);H{*=?70kGubzfX?0F-@kb0mf>7CQ_){T{FmOx@$ZEuUtku0PaK{%- zXVtAnj1tm23$AC4<+Rb#a^WG$xFN_L`#sHZ%>1R4XG?2*^TtOdroV|*B4<)}Vio8y z`<>2j_AF*vGEU0+E17#+Ej$o6qu%nw;);e2v8S0BtYr|H=J5HgCG>R#F!;X}h!92V zIw$Z;AZo>=$vkDoa!Ht0J^S-%I}Yo@wqJ@Y-6o}ZRc$r&+HIjNZ#}nSpT43UD>xObH z916bH55{l8c&SaUxr(Vzt;5iHF^s{ z%bDl7yn2f(Ir3;1;}3DksBxJqE6_^Sw5(;gPWIX5XFuD_{ZMbc2j7v~lf7#TLS-W~ z6=kznb7C`8{>ZWeC$Nt!aKX(@thBhb2;!M;n{T!e{P4MhI;};#JDgS8Vo<2UQq9k> zdgx+t4n9h!)HHfm@F+VmN^4csXBJ(S5pm2hMM(Lp$~oRI^$lDfCSBs>+G~ppnLG1R z%_}glvMK0)kCZ{!G*_|-YI5M%vZM|5`h0pE6PEqMBSEB%L=P*#V9YisZ#$O}9)^V< zMc7@5z&RRuBA{Qw;+)rF#!MmepeefMNG9KTu+XeduigF%nk68D{SO=}lwXcbJm+W?%Z-)GlB$Crmc zuM0u<=UenEa9o8<+2PVDvmMT$7L7|y*@c?c+&h&77!I&wFbH0f{L8a5#zGD3f%p;i z-r2A{g*qxcSt~0U9FnaM+wKaUZKJTUs>bm~nit0!H(a@*(^ilYgya|)jvl&c47P5p zm=IPlOHXsrs-D#sr>`=}Ufk-`WA9C!0I>U^6&<<% z+@SUjlHn+FgOHTkJ#tN=d!yM4@nl;I&QpR&O3cWWk!M@=Ns4~FBG9jIEPE;@e z^;u-h!9u)wk2)tVM@!y>Pd=~uH@5j zH^QY0;AT%B1{=CrFDtjz<7JUxP8FQqOD(!i8Mk=9m>gi*^U*P;9m~X^U$O7p$aC9K zgHn9|yA!ia{Z#+V(<`6K7s1T4_^QwZfDRFW%Kq+wY+i0x{+gZskc~guWf-> z(C`M#-hsXIG58%mnq5r)9zoP*Hj01(32U@u96_<_<}itH41~D)E%;J{U-A_o z-6N*=ds%aO(0F|J5t0!FFYSBMri6dcKkN9hL7oSg5SRfH_}Ok;7s z@Zd*9IPy{80y9~)vc}*i&sB5FWKBjY?g^7l3RFYUKvs@z(&XCWkT|GEsxW-JJfo}t zAajmozV6!KQ`Txkh^s}Q6|EwNP3VQCFV($}p`&3JYkh0SAiwd6e8^8z+=B^U81TG~ za+pbB#8uKOasCt%FE{(f^DYKW<@=%`!6=;7axuqJip?X_{tu)pmJoJM2Vq35H;}?H z{lT>dw$^smc_d$E7n0L`i71*68QjCmW7v;G&a_Xz>vkQcMB$0sn}J(uYOa;CBa|@Y zr^D1jVAo7!Y;R=P1tg5Mfr|#YuZ?2VO?I9(oA6>%{eP^S-|j_A*H)#gPQ2*8XMP-z+205;hF7yD)XPpYRC0KJ+2{QJA3M(KUo zQLDQB1ROQf=*DMJppIFkgCU^9h~5(Oga%M50Aj(18y*Eri#lKQUxZO-zfSQgE9}s{ zf;H#Jk&#^#_u_vfvf(|E@@o5Zf>*gV-HL84h1>Se$r$vih6?qf&kZ?LKQwF04@C`} zs9WE)YsD8#HN777H$ey-(1pDam)TI);f z9g|f0&5s`+`Z*)iPIw`G)*~rY=T41hLmbrk?B4NPyAGXM@Ayaf!jB~e+m8YJkg!{7 zWJ-!Q`1wTq0}WY2D@KF);PqGH=u{pb8-%X3w&WqdeZ*EGC09zqq-<)XQwl z{)E*un-?jpP{$u%GHR6p%3^@KU8Wg2?6 zW>;v?zmW*+ar#wDD=7_gOdR!}FeVha*A%pF@v*;X!cda z?uPl4L<&pQUSiJnn+(j701i7#MAq8lA_tfMT8!tyW^_79Q_7=n2c@ptI-Hi)Q-JN8 zMvz$2rZ5`Nm9B&c^-r+#nOndV_uH7nR}VL<4z$Y5_0U{IulcOUkISnXOR}_&D-If} zQQYNuu%7EIeYqJ#^}$3k732w89=b5AC)UHapRGyW#!4KrSbTsH#`6bB0;9=M8OWIb zJ@(rw7`A9ir2C(r_gsYM=gL62ml!`5i za1lk-Td!OS_hE58hoNr%1v(RYZlCN2dtp+;LAkJsNEtoRg(H{Qs&`q-Jk+!7cD|p2 zpqb>{51&cyQKs16rBYCKcxqu0BbtHoNenOV60zLHh-O)b>+An;N1EDI#Fz|pU~v<~ zG}M`O;g#qs(#J~C+8cZ-DdjUZP<5+Iq#9YP%VlbI z`Ko_+EpdvJ?&c=>F>&qlTFjY9rt(%u%E82l!)h72wCsG&Ku;Slmhvl}Z?Ds^ivVBJvl^W*!zSyoxB z>mrCxeLR|K^viOwGzBEo-Fn?J40p$qGdLzSyU%3<>r#Xv8g&H+)ziP-LN0ya+RsD$ z2*yc1#}Jvu^>0d&j`_<#(_y8z9AtOXSw!|xmW;?MX~?e*9UC2%Dj}|x=5NJsI)5T9 zNycDA+`+0nAA(uY(USN6Oo8@GX7CziA+bY>&e3^Sj&0hq0UdWqH2~a_PEu^<(H8Dx z@4REYOeX?!W9uMru0f@Po~_Gt4%vH;QL03&{u`Y4YIJPN4^>8NTgx+C-J`Uta|cfD zAKjf2$4z0-`h*P`7hYTjKCi+8UoysdsfhwS0d`pfMieA}%9<09TM68dE87A0zb}J* zX>%i+7yEbuAuHfO_+LB=AAx7#e+yfL;$Ppa%DQKL-?vHN4_#kJprhqVrxA)#X7l2t z08_t6t6+-f4%=g}=%u%X#~voDKrGzu@fVxVX9p5+L%qUTd#jYK6S@zlvIjp3qnre< zNT=!#Q$8WA%1=wJNo`3Lo?EF0p6u?=|AuP5vv2efx|!nn;TR%ycE0E0)A1v_ATiax z{9ry+!@^~GEJG!f^T_|)Fn?cLw1d~GlVgj1&o{G|F$04-oe_7H`j&cgs^Rl~;jg8)f{JeV8Aw-s}k&ZY$$y7L9F0xCUTs9*k9K$ydx^foS5XoGtJ_ee$ELl!S&<@J5j zp<^ic*qf3>LR$W+G|uc8w=NG-R`*Ff<#Gg`8??Jcs_(L-VYJxc2{4Z4TADMeu^);t zp&3u#j4jE}8ZM9@=?>er`}Fe9_%Zv+E63$+(v3n$3$55x@Nnai&yaCds-&r?gKmLQ zM;2@#bv`kv#5I2tkG@on#+Ix4O+e0OLATY7cCawSz8Xqwg7q1nxlOrvQg~ym7-!ZW zV$XEo962~{32e}B(<+=%0<86YFJockyk(2pn;m^f2Fpv~$_j zta^Gx%AM57#u?4DCO41AJJk|O@EwJ5$jW;SH$paP|Mt-+i95#ZF9$i%-! zA3)i4e6d`xB5*bTx50_xuhu-pC(PV~V@eOqp%0mBi9Odv-LUKIb5oM`BYRN1l` zYBFDcapr!+j_6m^bZ21M z(Ir1t7X|5rQZVhFg&@`LlO&K129vVOg6cGHCJJX56xwPXSA;4dVITb@d&|V-{A|MM zw=b#TXn^s#qq6vz6@r?p+WepEhBCpK zA+3Ml(Ac!6&d|Z10)@rze{bz~tWnCa3(vtKwP%T z;G|T6Scm9E%w9O`lE}fqc$-lYU*hJ{t zfqv1+W|7KK@Cnu%jHu=3z_P6UP(y>3-hqh|=X$D67l;N}2ILw5o{X?Ea zBV;BLi|(@ZNxi>?-JSeO=kof3hBv=yQFM&aW$?usN3=62Qp|yGsw9>*^E9P8IgUZHY zzixbAI%wQSom6p zrNz(X__a$EfQr*t-*lIH)yCkAOe^Pw#>~z zcoyQ^GMPYb?bR(B8SrVUec1_l8FKId zMedKExP-BTv!lM1F>r$R(hG{(TK%I3gxRalF+u+SHde`Vcuo1`6a@J{-sU37$%(`? zP$4ynGh(+|dJ-+0|NLfpoWCh_=oiPg+QxRds-@K_I)PSU_k~lqN#=XbVgho<1xH)6 z7p&0&kMixuiQ2+l}CqJ!#PT<&ez(a z;3{dz*+&A^d0^!^h-}WzB9U_IMMzG^M2k~;voyHyOBwv{AJp8U%D6!BL}dx(zEze^ z*g0_fZ!L7vK7A4w4SbKs4&I6B3?}JLB>RYZv0&+>4a(cCw@_#Tu8}9?n}{Si5P1@k zIsVM-d2e(h6B>?aTyvZVu3#oL7{+Jh_1??&9k*0D>psyahkvKdps!h@4uZw6hW%JI zoWTH8)UQZ6sFy<{bFhxOsoiXq8DKeYCyCuB=~S0YKs==ipkQ%&tMU?93_{{alJ-W1 zIef(Bm)!N`U2}OQcNmyu1yzJk>Gvu#)7J!(LleK2F(S=v8-$)m8%7&X!eRn3IAeI# zd9KfW>EV@N{<=l@pJWQknzOozd>IWEXijYwZW$eR8zn{6wNQ@cW8ZScVGTACKc=e> zg%t`$Cnzdj8T>~2zKj09pYF>RTt~b5;bpk7^55acAMrl$`gk!PQg$SeB?m_QFQ`*; zwsyAqC-xjKY32WYc>v}BwkRLo1)fo#__#SCw7)CYXku8Qpwa}ps**fg3~e+#6j!k~ z5xqRKuOR?Y?lU)dT)fX@N=j;yw(o>=9W28H3WXf&xysE`~qon=2q6Dac$G z^Yh%e-{V>x!zv*~8A)MSjjri6?6%6?o)eG5pmX1$SV~dK80T8|d^dB7`AX$cI{%J? zRxNFmNNr|FTanCZTt%Y~^23~2479ERm5iW$R;u(qP+r5gwo4%6{QV$u5O5XE=a=U z?#KlSfh)Y%1mThETC~zJK=)DlB$M~_7@0wntsQzwStG?e8)no~+)^lN!@^2iUOLj*oir<~c4TJ0be8+F@cw>b+d z$$M625!UQ{{{3T52Zwvs)8+XPIV+|tC6+O`Zq&Q$V~4#axR^}ZaEvu^kzuy4%o+Ev zLUPV1->V{jT__|e`dM+19<-bJ$74pb5mW*V#rpqkFZ>7vYpu03^%XXyCxr+5& z(elazI>J}rxyhU%qxc>oVbU`tEadm@`LMoskk+v!cNC;ALU$HECSF{r7ZH9mJLz3= z-7o<`wA3Zs<(yJdV>`%VQn;oTL9!XG z?g$)5z6kCC1=ABT#%D)YFc}~tUlgEjE~zPKjEn!a-<|zo!-hU8hdYz)7!cK208PU zMQVjYu6{o+QN>e{P~XoZOv4xd>oLsM7UKZqpw@C*NpD&E2xMmNeD zUBiB^T~MTauo2aYc8On%LEH{Tc zSS}PFt(DRy&=aJqHEgQhME3IJ88xNa;fCOZR>YIVvtx1v2sn)qPa)ad^R349BmYL) z8d4M^Qrz>xh2U}&75K0<6`p*bkCGSdVYxON>GKy>%nLR> z>k8y|_d*Y^MxBVjuMu@{Rm2(iKwr*F|JZ;1{WZR9j9(I(|HUWB|LPM=Q20N|9rtXG zQFrIPdtxZR7zjlY!cu1Uz_~Fc= z@VLxpYe-j*F=G?QE&3;B)s-i-TRb;5&%{To^Qu(1v@y%fb}@=b++|k_jz?YRui%X) zhT!sL(js6!G3#r080FQ8WKb1%)#>uX^lu0(kgGb!?;8edJ=dx(Ah)G6X{Eo=cnnPK ze_&%_HE1uj@xa7YA~q8_fwAIEiS->MWNnK9i%8A-ToJ6mK4}p(EtX>HDDP>%7&QJINeyH9?t`}MG3ng^8x>M?#P`JfB!Xe7A{GPzTs}A%f zKIySxs-05GPLqWU(g>|g-3oi$q!9L>a9Q)I21~YRs7m^{i z^-+SST_Jj4oJFbPh`II) ztgg*1zH8;2NQU~5$#|-{{W@l1)lBo6uvkmg! z3aX+f*+sz{;VXjZbrGwCWYaP_$C$_>ec^-oP6l$qPM?-`xxX2yeOnI{GFlnltwJtC z3!@~U?2oBff)q{_v2s?ZrGRu1QkbmRE2DO3Sm&X#_PB#3D_|S=z#7Z#472U=MSsdI z*7O2pg63h#zJlh=c&UO>`VRfTQ5)B59~+oR>}xJmBTJZ0&!wpB7*n&5cC3mw2}sfNfr3OVJD{_<#+bx#>UI!@2x_umwbr09T#Q!HtVO zI!ghcrfN982GrE$#FOQlmh&{s4Pr47S04GS#mvJ(Uy-O^p7Wl4sjT>^+%$mBMp6m= z*c?T7oQTYKhlJ1P(~Z53eNfbNrUi8;(h*@?-MnT^W04g6!)Gf^lslGUquhJtgg3sA zQZk8VNIjVoX(^V#84S1lI1z|8{~DnzQu15ut|w186_v67sAj>lFD}a3dYu!h8B3Zq z2N#y`{NSR62CohJdi0!zv-z7{bpsO;k*3uC1;-&5wZXCXH#E62e*ry2Hd}B=!L!4a zhyE6BSPzy%3AeC8r;HEf*;Cg6>sh0#!to5z%y~TJid8_S)rle`r;YKr@Kl+AHfhKC zoMhXr@p+sZR9zFpUXH3=?RT=s6SET=@|y2ng3wlP(`!0&WgCp@+vYw;MTA3(vVOgc zhS;EDUt6Z&ya07TGQp`)CZ2KJvN{G&r@b40m#IeOFPT?P%%x?9U@*su97!`Ggze@R zWR1Q?>z0wC0TY&5m)lktm{wb2nGvXRIiL`K zYpUCAqT|u{IN*WM#r#HQdOOQi+8ff9l%j#D=GfJ_))C0*gH=4)qsV2!ch zjdRTJ(21+iO#W`C@SdKhUv5qTyvU`Kocyvng4x}n`8jq33FAIEQ9)P_YfTm$*w!^x<}i~-(posgc(5)HYEemz?i=Auv(+^)G->co$or!(E|DXn@;(M?4u zj!J%ow{IZLuCbak02AJPYDktjXfSFbE+ZmMJn%|L_@c~pFoB!*7c zpnV`--24Tq5V%78&Jd)-v>#UGTpqa3C<()0L#kcD`|)Wafd?P=(Z9h_hbmbd)hYu% zNz0X9>$dKF*tFs%ODhk0($57p@xUQrM)0qx!3+I;V+^Ad0}5$U0ZCu|%tXVl7~eoO zkt0Xv)=%qS`G*gLKD^2VApkfIr%S(^8sN7Z0G|JQiqimc6yUV*uSVsMzoQbMbN?rF zr}>KR0{@dj2zc<>?GvI`_BDXMtx;^ukqoxMh|cbelo-_zG@6Ru#`^upBXRzHnXZOE zCd%t<3OKF7|5`XIJnv3@{qjBm`TR+$i`>#4vj@R8HJtcO6Mq3 zRed}`MY89xdQS4V7M8_9jw|4^v7Imd2L4N`P(iPrz*3-A=ZCwj`XJM1xRlB1S~q%R zT$;qF|o!>NW0LJ)U zZxICD{f*nDM#614RaEC_qIvB8a-gOkrG(r>Ant@R?Zkxo`OR*hxK(ztUe%9om%N?>h=ULJ-t%p#p>M%%q^SFyYFvLCR|9WSxP@>t8h4WPE?%ze9ZSH@m z?c{AA$Nnez2I2`G$6=7vy0Iddu=Oi{ zl9M?Ejfg9Si=Gu38dWF=>A=tBD5o02Bi3UBapvc&EHQ2Et_(Y0%+p#)%w6x0{lXp? z>W+XT1ltYJ^-P5{vY#nt2nb-*f_^KAD=>vhXnEI8`6(^#>A%@jGe->G2g@1fKsQ|~ zs*gemB5+Mizr__}zS?hV%+`~_DGKT>51!2qYNY#ae4c>fBg8Y{!GK&iEJ=N<&6h{+ zMkEe)c2$+W8s8Us(MR!C;7Oc=-)lAu4jQCer^sQ2Y{3DWsilb7RS+yipj4LmE8!L0 zDScYQq}`d z%Q>AyN6mp`83@cR{}2fNg%zm(bpH1o8}O$4D+&zgF97BHzdVV2v|zJT8A8lw_ol5t za>)p$jRkEk4q1ztj4SBbFQ$&KA|-1l^&;+1mb`f@BD2{t&TgiSRB6#n-F-uJbieEmjvb9?miF`=@LdTKAp}Q@xH^yNR-{M|EkP&?}Y@E|l1m zlN+{qtpiTu8B|xdHFwL7wDgkKiVCDyuIq|P4Iss81K-0R3je>~0?3#XWZ?A2Z4V@V z`8Tqw$jgsGX&kujKnA3o1S02ctOiB!`d|8!@H(D=qHxI5Q!BB8xYw?Gi)&xO$ zTKvtW>Mw~5zTGqtQ@qptDsGwxhq@TLnmIA>x$DWo)(P<$qLmn(&mVg;!z;fLN7ao`8cYi8MAd=u ztO;*n4{$2yt`>kydh83GEuGMG&!<*}B6cwpN@$B@l!aWxRX?sU|3wztJQ0+=mI2w< znY?s{yZC6u1107mwfkfCC)WwRTvO4VdZvoO&iY58wt(LBhljD8OiIc$+cK1e1@nw1 zlez}tQRcUYy+-#G;2BOL4eIM)5=qNOPegf}ZW(P_UU7r_&I4~gkWqIEfU4ZBp z0nz`%Is7;Jpqi=O>6un6(Sc;RN zqad2_ZV$v^O{+`;d-hI)+A{&A7$YaKn6q*B{nMGQ?iQ0{0F+ClJc+uf8Mu=sLG&asd2NTH-?-X4zEw@2vHyIgQn3X2NyF1{z)e)X-ud#&b zivm8~nDOM%vXzq!@4(|P_-v!mz*H4AM!8uXmyrw8w!8ybJB=?F4KvLs+v*wO87$|Kyj4&h){f6 zD(Rr5p}54N`7O?Cue(Rmz{-BALk8|g?|8}8?w*&forBy{lx+P5*pNKyF&Tp>ir;40 z@ki1}r%$Uoe5IY~?Yv)>3@pToB~a(k_!}n2q=wFOv3q-61fC)OH#@lmN~N%l{4x^& z4ltlpz61!!<471}-)alGwg45*zqjQBe+dv38IUtg7w+YUt!WW@{fMT1#L-fG#tIR_ zkT>gTwa1d>w1B*K|Fd5~5;~px$~%<8m?wgp!8bk%yghUTbt&)y2gJsj4BeL_hmM|Z z2svPW@sYDVi<6GyCTFk1dic6gO@pDz%IghIS$xYsG@U+s_};-!B?fLOBi)}?yWJwz zd-n&Dnk!C_in#NB(+kEVCF@&cLv9>%5t7EvH<5T<&G0d9^De0O=U`PN%q% z)cWK^`~vOKH*i#E2_Aa1+gK5rTzhF@@lleI3(<&O+r6(R4yOei&vk;%Ds5P~(TYxD z^exK`7!>yhhU+oS^8`KW;);fP_qBi+NOS@bJ-#jYby4CT`Nve05GJK7!?apS+?%9z zx_$77R0RoEfJhyJnU`ErSTI-DM3i42eMvU72Z2D~=Q<&E!4N!^iWn3Dv`Kf;@YrID>J^QJqJWujW2j2KCx7{-P_;c9hpzW`F1cma3cNrX9 z;Z2rAX$i^)1pXJdN7L~$^O3t5uL#FyLn$h93e98F%`_!a@pI8{zO}?~%SN5b+t6D= zsD8GGE;($XMZDFy;B`BE$+GHY44vx z2&}b5086jGQ3oGi(yjqkDS+@!5W-_c7_{3MY{lGHA<boh>-(GdI|%V3MHhe4YNbpUZYdU?rV zOS~W72|?hEpJI$zZQDjgVpZ?t0c-CGrhVL-?M}Zl_MGvZG+TM*gnn9=*GXet$%BZb zx%BtRd$Og|IEsea8gP0yk zKv|We^Yb#j)P{%4xN&QhC}hYR9>xV;I1i)8@3Mi)V~Hzfn)9Y{&pC>+Y_O#8M2I1% z^UNEk!%b*x2MN*Agr1x=SQ|H>yCLfto%t>rIA^mQ(4QKN`#ei(RV0p0= z-XiZnc-wUDSv_@$)xt97wJJHEq66V0kSYOl&nXWIx(x!b!8aWjq^FO@>?tl^}R(j8VMfv(o3 zJ(N{i(FW?5Y~JEn40`pd3W`f)-R~DWXV9Uy;jgk+ut4r?0l@X=b`}7axtlG>?;P}p z1|S4x3Y^dWwZgjh0#!l(GbJ>P6QQ?X^?e15WWLhT1>5qmT29HzN?4nY=I*`yeNSrE zJ|fRETJS(XNVuPYyX;u9O^{(~;$Pf#ceMBzC}8xaxp{L7XG`O#d{J%~w^t84)zBjJ zpnh=Ni`eaZt`s8xd zm)139p%9o!($EE41!188kp!3J`jJl!i*{;qR_8+?IuW-2vR6;1W5q%J=AN=Vfv)9v z$=S?UBLlJV@jG#=n}G8)XSFb&*>vWHpAgGN3`;T2DN8k-?Dqcy2s zH$V-?)eiVANq^!f(euHBV5C^+!|6Sq{9c|>gVOnmrnA{lc@7hXqqqc@5}btashhZy z?PZe)pMDq(J5NJNe`<``ok0Q+$gQ9yUJmH`w-nIDw9IWY#7dk z7QXJr(FsX53V6N$ngITE>T{_#A{WP54U5pbYpNfnVm16yr?*$%ppnES*z@LUU4*{K zq3h@r&Ll{gl)kpQeX)nb0Um_HKI@1eCRiVU+kfVWAOUG3;Nrx=1?Y`6jBIWG;n@GJ z*P!Z)VjmRuulz6A4?zBRD@ZPONMsvP?3t((oKyzn?wXmOAeKrJMfyf)lV3s#%-HmZ zI*2XUn-zXj!e7|X~mDEe`Gv3zon<0~yKJ)80IUhsx#rhPmNZ_(Zvly4>ccz(B z22ZBk3!}U^ZJ(E@Ka3k&d8X+EM_{U8-V5{hQUpLn`?A)ctcTW{DBogfamZ7g#$3Y} zTpp~uxaS)Oq)XKxDdV;@S8H(f3iwuJUk>Ci;?muHmD4&C#D^?O5L%$j!t#nG8zAc? z?6yAPnh1N0jGi&I-@^+Q+pKIXc^ijrX3`2N#&MTpG-*6-LfM-X3@!6L? zLWIUBYM%NNacDUT&UrK9o{gwNpHj*O8+R9RisnS!)ih*>>7$Mo@2Tfb`LkNI8Uvg6 zfO{Naz@eF0*TBIh4dM0r!3?*Q1^Yyj3FlYGuF9H4vh)ped7+}9bJ<}iS0PQ$_4}p+ zTKpAC4xBejHTrh!QoOQKldMc7{X^HrUS^U-SF5h8;2nWT;^ohT-PRP^?T zZb~TZ;F}s?D7Odx_(0(6E7vTq}bnr{VOX9{v&>8Q-k1QYC!^JbS@li{$PO==;FmZWR#>1Moh0wxR= z?k->8)PrUB0}G$z=Gta`aJd9cqSSvni5OYAXd7AuuG5^FFa9^bdS zJvy1{H4ti8C9MtB-+)zcY>w@`yRRf7=&Q|FUp7;kA!!uG7d-k2cYwwdqOH&P_GnVO zZ+soaiK~9XtyHWf-_Cw22JaU23gwhNP(&Hf!f zh1;B`{f@4VeYUu{fa$G!eF+21_wb~rx!zY~A!a=cJ;a(Yj!rko*Gj|G5y}?Ai=Su6 zd^&{rZTAGQf4xv_Is&RHdbgOQ4gfX-0Brv-xjT*X z7d161u2;TK5IJ}%{u1%SYycLm{}GsivmoP`5DY@ z7F|!*W=)&JiFUzZW4zmkd;*z+KWY0C^7?i?|`ymYquhj_Et%Fp!jv)>b6R> zVGu?wDZD()ts^TvZ_%Q(H+INHf4brQ`WKqgma=oySuW<2Pxndnc;Ni5csaFnLnf^A*F2|0u`y5B$xHWw@unAbzX0mZ>2_xR7th^kB-up@=9D$42VU< zU)xTK_fgcPc^Rw!LRj-2+$jt~tt@?*H_OX3Ya(aoQ6hhqlP{mAdK$J8Z;T}yfAxtl zlBQJ2dW598Xf&M^RZutC)Yw>Wbi*fcK9^!NtDn4S+UeKtHoc=4MT!x~H=he&zIFo; z&;%gxXMQVdW@JjFU=O;f0gbAN#9Yms?T!A*{MP41>WKoprU0IDCqOwhv_kTJr=x@% zatuQzO-_9$xRBk6Hbr4eL2xE3aedFZ0;YrkRxCQ|edj&Z)e(rhO~Ahu8(`j&^)s-x zl+tG#zoXp?q59P~X#BIeH)q=}k`fQ8Qt&PMc3tea{mc7Zk8sx4uaL;36=UB|e!VR- zetdIh+}U;yfoQRX=#{t_AQ6=8-KJk{|E_Vq))a|Y2#boFt|xi!K8_XZ4n9p;T``8K z@6$&KFo&n2Cs+rW+cBI#Nc4$Mb7>yvwxxHcGboYjWN^iwOTBXN$g+Ru-p3II(=V|@ zO>m)>IJtL!?fQZAwRhfHQirZ=Q@OE*7K6GaNpN*~*Y(n9*&b-h0jUV=c22+d=vxT5^mo zL9$u9-@!gwds|&l>(3XXF+>1lGbQLxYyima|1%l`KxPjD*T2WOAQuE%BNtcDkA$4< z{>r|e_a)fk6=eDa0h82EJ|R%8i3)QQCpB#6_Zga=-C|wXCg;QIpmnf_M>TaAl{0r_o49uY3$8M1i^AD1hxascB z7e+FRn)Pmx`|{TO42%LF*~7DTbwhp$mZdep zA1g-ryV;|8pWRVMXAeU3u{t8uhj3U*`U<-TzN#vuuB;HQ#W%YPX{xP5_uEuUz@oB) z&WqtKZb}#(MpnQt79sL?j~pa0P5Vw9ziHiTI(6-^V>yF_aD^Dh&g@a8O&f*qJSZ5G ze{26ziR%O?C5$cjbHzZ2FM$yM;daUbB9tKNy4*i{T;jlR%krOPql%U7-;#fScv(__ zdmA*NKN+Q$3W{D%TTqya52FA%44qne|J#`d%jbz$GmoT|uZ@(hKns?t1yVaXLGSK;)+Qs@!e z`Pt+tJ);<$8oto*D&lsInNtdPX1RdZ2CopDWW))@yht ziF~DskA9Mkgl5_qud^kUg8iW@^e^tZ?EH**MZ#lm>5>C;clb2>;0BvkkkY0ZTlyS( zJ#d(C$*Jzb6X++|mRl1lWeo&A496wigomTNBh(a9p1ti+4YP!oSo8kGbT7S1d z4*zQkJLEMNb^Uc62lyEBGwrQVZXwPh+GN?8A6@kh8MfgV=dyQW+;NO&1Xv0H6DC06)cKFk_g{IYu)jzg zS@ALoXN>UT_oR~_9tyN-wuuSKji69p2QGh%MQ)3bJ#~+q_1G&=nd}~x0+f^Cye^8| z&<`ItuSQ|)Rk?Lr<#tjX+fV(k$G;DM#@l_mL6_(OCtp)JBdsbVb(HkiIL2t+W1X~! zDBkZ=rt{GMon7H@z2fVz?++icykDvPLv3eTyrF}91nhYKo~-bF*+dMF&S$j3?xs0y zvyZk^nv&uPK0XY}zF~>hr?k$Qa=(`?uKclPB}aZEjYmA5RgHQ&N=sI#ilq15?nvtr z#r1DHu+evGvWb!Q5>LOHii&$fV@)YocO={$k&QuH91tpwo}tfm{#`D$P^M6S&z(=~ zLi|T8UPzp(r*kikjm$+<3h{8Svqt;#B`-s^yNh(uNo>hiMcLi7N=LC0UFdjZG;e_` z?t5O5`KG;4q4_0>Hu6bHRuxBqpnC@aNr#^I+fMn~4h)3kNJodX6&-Hhk-!;WzawLr zIOM_767D@lpQU+vLgzo=b@jeXna z{KS3eH+;>tSfNuPYEfA0L8wihPFt~jf<3#BszO8=(b*d=H7_J{b`xYPh9@zqk<6L% z^{fts@|knAo;A9QM){#fixr_km?u|V)O<(3pz`UF7S?~kQK^PiS;z5bC=ZfxFG7U}A{ zjlT#}J9o3lP`}J;HKn+*8GQw7HPq|HF%`a8)Em>x5WQ-vvM)7pz%7V&yy(N}=v%Ef zjT|Zz?jSH_{~_K2O=C`5D=7Xu6|3HbHi)`Y9Mb`AC2o2qujjh0?Yhw2HJQTQP-a0!b-{rMh>b4ra|Yw$6F`PO0;Iyl7I`sn*hR+;P;AlbU%W z*yA*0FvV$1j+G3aqY!_yJJW^tDIg}6xL3! z0p$46ZSYFfc+JUpj=sL%yG<7S8bKcAm+k4m+T7fN#lIVHoDc((;qy&aQ-Q|aCm`iw zWA{zXZ8tQe!7oYwke(gSP`AeAYAoragPPwAq?@PKIkFTHd54HOBUhGdWmW8^WMXfL z8$_V7hXtA9JIl+xH?=mi)EQB5g{w(AH;xnxB(<$ky_lmGPiX^<+ujBF9a&t3O2kWg z6o#Dtu+y7tGo5uyck7klt@sF348yLJ zU%l1(r1jh3f7#_HfU=PF6t>quW@!LWM)fDVIM4!h|IbUn<)2Y1Fk5vqbs-Wpb1?yK z(f$(E0_R1#0hE0})L~HHx~;~wSJyyIR8T}I`3X-tVzmTS;{AKn_A4*UuNh{5YnFrb z2v}Hx0ALQ{J+&JF=X15TlW>v`gHm4WHb(zFoQHpSe*t6TRkN;@v8LIYn^pzey-;hBFz*>M87jDHWc4{ARGm61^h`$AIjcuN5{z@=nwXYP zL$a-d>KnJ^tfWB=``otZ#0C>6tsqlV4`!YBzne>%&8|EJ?ny1lAn4vY@@}QEeig@Q z%-{Qz6V&AdorR%)b_iDKm4`em6}$zXjzR_N#<}3ocov^cI`Uqm3p3UAJh3(rJZzpU zS~A`~l$*ym)`M9Io{uX6KW$re2CYj8THNYbma=RkX)voq(lf@|VOgW*tSQ$3e<3bC zkPM-oqKPbE-svkoZTpuZb2066si)Aivf>n7ULytlMo|^TSY-~gr|u0=oP$L1sAGf} z1=qeOU#+KH)#Z4!*-<5{&jYS~+7=1w=H+wcRgF#%;d}c6&x*Uu#DO{{x>C8w+@9rf-JmA;M>mFuyXo?5a z{NH~nH951WqmPJz0f&-=yNYrqC4JXO8}T)g4?(xw{M3xO_o$5@m$e15wV56E9Zzd4 z*U6+w^`zJLb1}+KOiJme{-4IXvpUs2iAH;=u9RtqWmaaee5z!wpZR(ir~C2CS06>N z;xAoFyhg@nEzeNcdS?}?sw_KFFbiw2@nZL4IBi5WIHF<#ZO3r_;Qw<33%luxQ^(jxi4~b;#oQs=VQL@BuyxjhCC zKTQ~jTv-))R_s_QC)h&n#xW_DIPFBnvEklo=X{7L_@rn#UGodgMz0z&eZKNsX{wWJ z$MIX~qhkHj3vbeU08@wTbwwZncE~rcUQz#(2OVsPw15*zBcLC0`Df3r@<-haShRyQ z6#rKB1w5$!TYW24d!y$7MRtub0oer+hIKtUBC1>%ji?PhtQZQXD-H%7Hpe+{o!C@u||9f`rcV{zb|^?4IYX|E@Yl_K2Gqu z;AFkjr_^r*N#JFI?BTSu4t83jW-c1fuOBB%KFc`Rl}7?>-2DjfF`%k5O6HLPqk z>Wh68Qa~-ORII&gFa#+mYMjz0-Rb!qP}tC3K13W8vF{YNK6# z)`KhOTlUJa0mX}1rrF9+`G)?3Z&>xQhtQqMhHP3@SP7C$leJ>afIkabBi}w=Q)JL4 zrQBn%jylbGCG!tTt4Uup!tN}(M%K=UmvPvwTVf|GCv4Uk{`drI9n%@6O@K3I5GbM? z_d90`8N2()<1ScD4dJy$tpzLWaX_UgzxY|~0qPFN|7tV|(2WHGjs6JGjs5s$Pb;Ipaz0`B0+fw0y|BQ6Z}5fx#brRsYZYpw{TU+W0vV&j zsAS#QbT6%UU>uAF9A zn1^PqMu0Gu6HGU5&lR*=(947pPlCxYq@ag?I^Bhj>kK<=m0?1*b4J;ZbjjeisSL|g z+)Hn65zZ!8ZKctPQ|Hj#mimGn0h*G0D2bgG2NJ?9P-Omje*g2IPH6{v8-b>(l7ES9 z!}jw3I-UvSzVWB|RT!HlZxGai#+96wE`(RrLk_U2iW0MxuxO5Z=1VE^hb_f`i#YfG z-u~0)a0WW&4E~!MS_4w5sEL~1+53rgSmIlGsun!l%s|Hg{B7{As@78>y^5#_v)I6^%oP^^$nsP@XyX5M~D%a0nr5XZf6 z!)!EJ6VCm15h;F3DXe0Fc|!< z&;!r~%Q@hq4{!H=;Pyg+F^-9|%F>)S5nRQden1(53e0WCl%(lwijD4pQdjcv6zERC z3uqJHs(gwQ!}uw*6<|#__m#%(;x}nD{61G-yB)%LklAfozo_c>k{XRYflSlm%ul2C z1-NxeU%mB2P=8G8OnrPwG6fl+C!2j}O93K%4Mh6iCot&qQgH;$I{s*bfFzB8Gdkc{ z$;k7cpZ|@v3*?3VpSCM1j>ln*5&k2f+KdEYKjPd;RGr@HRWY=ECX0hAc9=s6&Q%ZN3dg3TsR`#YAq#`UNUOF_nyz zaYh>1>00Zeqbi%|v(=idDw%-iqz0_m5gL0oWU|dC1`74ju2Z?O6H0RN$~8Gg*l;X$ zch1Qc*8whv$`Y-!`i!4vvY9;}MT~r%IuwT&LvrP!`FW-Zz7hw9PDHrs{R&%ws`Tk$ z#ekoNDPo`IRJfazz@4Y(;eL0$Jf4*u`6h2o5NiI78N3mn#!12zZ%*y!tBBRYG6Yn- zkm3&>v0wH|s=h&HJuz3zKxw5e;^Pl}P?}x9^rpMVDWGYXPKPvut6LbPB|bF1Nhv}N z?Y? zUwWJ3zeJJhKU_Vi5fB@dLBGw74}Qh!eAPGC5{m3duhvQ@l@>c5Zwa{Ia9wL`1_P@I z_1W9qt!JP=q7RO)lFP9hyD>TGaMJOe6stAIt2?c6ctbLC?lSsC zYqLVbqWv5@-f7w4nex{eVC+Is6!QRxc57Fot7Zv85)~8a3l4hS(#WD)Td&|v`3i9% zXQb*-i$Gbv{grZi@`XR)mX&O^8Ld&|y<=#U3&KE9w5YX-iw;sWcXXlHXXgAKNI6b~ zQbGB*&nJ&^R_ zjX~QV$%WEqR-VXMYSX1})0JRv6&se|o{*>vt9re5%f8y7|LZ1ysMPu`qif;uyY7I7 z3LjHc$$E#hD_iWI9db<_gE;gHBj}7*F`1^Bosa#d;^4<2l2CVAfeMR1#iqO1Oh5Xa zwJ?oLzoS`643D&lLC)sE3O7^djvm77N6+)vTCbDVj_`E?Hh7 z9p0IAC6Psw8znMe-pQ@QEer3=a7+q0N-90X{y{WFKcF=`p?9$b*gP_Mlqiz=896GV zQFD%bx1_pRKrFp1+E#f!Li^x+YU;iRiJK&=X-|~UtjTc33 zdLg`L*&xTO&4wY4meT$HnM@B8Vy^)zZ=SEVellX+w21L$_lV_M%E(kk3J6E-lzS(h zShQOAipsE2VAWc*s?kf@_Ai}{-vMb?GyuAz07$za$(uiC?|;4);BgHiw*vI7ze8oU zfAM)Fsqlf4>^j1O0AmYrL`JS%oo=x?G%C@rSH`CAm`5zK3)L}^67l+@{>w}Hj7q}4Yo>yv^(x3HIrgTQy`dMU-wC)oe9Q3 z%Yfx>rmxw4$7)RE|M1S0If%gRVl*<52v3ZWTtPf$7gJRqe`0}Xa_|9!;(UjOf?tK$YnkC!Z&cbel=m|TrOA7=xcwQKY7IO zEW19l7|!;Q%(|tkMJu9?tSX9LSM1p6{*Cmp`~fazft1gA;4(z)(JuS^$TD!?hsB$+ zG^jfLQ-2(^(wTYXVb+?`7HoVeERp~)&THli^LWz=sJWT4?48#suf^cl2}_0-^|IGv ztUh@&#&arLCQC01wuDWWr82+8b?XjiNoBV{kO;tkG%0^|88^TxTU4CYraRcJ=QmMb z{pDRrKIKAvV`{;o{PF<|>X(ZuMoq37f2YGM`4U&!*G0s2HPx)s zx`qo)dg-GN+U4jH!Hbp&CCI%=Rj(!=V96iUYk<5eSMO$|{I$=i4kJ|=dQi)7RDO6f z{VCA`>SOpuC41Mt<|``Rgm(dBzlNA$yQ2LZGc~KtDAZT!P#Ya9J!kR6sSgK}RS3aC zyjl?%olbVm;J!b5_W4Wq1bwEZf^((v_weL_*CG6q`(W75sAF-c#ZS&>V0h*>XVm3)KwlkE9hr#H|cAQ?-qDeot` zr8X26k*sWZu!}@ie{qB@DEc(($hE6fVlMmkU8B?d2dN*~SA#a+p~x?OOD2qSdX?9} zM}ujf6Qi1VB1^FBJ0nXsj_JWsTC1pYO~AlWj2T-k>krR#^(HiGbCN<0Mw&)VBo;KJ zw9jZMGN?}GX|5g_m7yQ+~-@{M{R2GhNj>2gMtzx)|>gL^`X zw&am~`UR=$6_9G2edESKP6;?bs`*34kTbFdl(no({t?;w|HIL9qi5i zih(xKi@--IGXaXp$Y(H@NZ=)kyCXFT=yBz5Yw})&;d04$lH8BDL^ls?ga!KLkndBu zlNOW2umHwr!{ah*<*qG-{67{A)LXiK_#^coc^xGB2Az*&k`@P6PQkoq zYNn1mt<$t!f;0I#zWby5A%c>(mSQLgzpGjxWl$A?L(`0j1T<*6K2UJTPm_ zLj@BzXPSB8t*?Eas&_cH9P(|ZBKGJFmW2MLFZXwx!f;|4bu3dKDE&@$Up-7De|c6F zL9r{U&YpSyb%-u7{C_+VsXF4_#G}M>N5O8gcVT}&BM8 z-%1fs$pmL*7H%da6W?!RENLmTKiw7Xp-XumnSBU#;1tQu*=^bb+qAS$LU=zcL{)i+ zyLkl187w4_NL!44og(5mQd*v4%tV}SOvFZ59W#4u>aZbAjD5njMs}aO zm=85ud+AJZr%G~)=dhY9@SCYSy*632Zjwii-)h4}k8OsOgE%H3YFqJp3k*A@6}Z?H zG)I2e-C1LT2%4_!V(>tj#;0)9ePcyxBM5~scCOCAnr?$BDAkC6>P0(5`#M?K;N zBrJ^pkse#nS*tMUg7Gi)icT+p5tRVEWdIGR`&W-h3|o0JC%HKRED&;^7Qxc#VP^A= z1T%_583)m#8z6rFBq-%ZiGO`XqO|35!8b2;c=$lEwmo(;3Y_hK^S>(h8dlKXnLCW} zfLlw&-wrmsY-A&jg#r{1(Lci>6;nohT@aSiTdUMJ+rK}SGHdIz%}JdUfDEe~wWwCK zXc1?iFrZ4=))P768tOMsLGh-f=fl8TaMyx=J~DHP@dlQ=#Yh6`UM;7g6W(cFzc_a4 zgiFai2ujX;EFy-kHLrEHImm@;8HCM(L^l+?3&kmO+01-xAcvX3gCZw2+e0OpTj)yZ zX$>${(oVYsly8tbRw4=o%SQ7nwMd7OjZ9IMo$Z2Q*2e>!e`fcyFjC-)W_?@#?7iL~ z4@WJ(93FjGcoDK|P=Ppyb9lY0Lo;TxG>LCA#gmQ`P1_VGx(k(n2eq(kanNMh|Hd*U z#h&}dfVzsiViN^_Vn2u2Mj)|7+Gh(Xjr)|8Q;n+e5vi=_eeFO!S=tP!7o5dSm z_9&J2(h+3PYn*lK1+?mWckIPS8S5v>_d3&+n6?wE95z} zQb$O{da=y9uY zY7Z|Q4b|{)Mx%(_DHD=KHM6;WZH_n0EpeY8se6x@IN&nOFZZ!pTR)>_e=5amx~Sru z-pc>ib`&R8Q=^vekJMZ(D<@mlHT5|b;hc0w;xniWRq-Qt@A3;s)vBCI z1?&Fnl>npda0=@P)RE1w*L*?Oss5LNCg1bmi0kODvZU7#qBGZ1_rUa1(LHI6aYJsh z&QY_`QZy17_7oq7AJ0w%Pgn0u%lERRD~~H2X&2DbdTkU77NvF2n__X4&c9r0GNt!& zN5#T_=6#ofUZ$re=yx!Y#d<+v&vyLo8CEhoFo`~$tbC7ARPlS5Z^VXPz)QUe|j_9oAOQPF=uq{v@B7A%)%jmgV%#M%*Ua@gkM%_ z;#GrJ>!C~_S+Rs-0yehbbJ?DQ)2$3;PKMzxk`rGi4&smHd0KE0dPcd8EtN!b_0Dkw zOq)$RQ1kg(dG}jO?rv0k;U0J5%JF53*YF9^XBA>BR~L>Xhh#;$G^}SSzpkgOi>DZv zR>1e4^F2E=|0rS!FM=&QmFpL+H&s9O4I%e&pq1RzANP~@WEi84#db>nseG!7mWk2f zhqwnTfiNrlEZ?1OS{o!c4CA_6mhK5hi_b&P_?C$ywjHvy`srH%XBRl7p3S#t-Um0P z@kR2jaKuJo0pLBY)e#vII!nACg`&d|IVGy6u3l*NV1kPI&CVL=><0@-!5E(ZS&hcCNB6XnGr+0~#kRdivn|kqSyy%zO4y>VyucYq`w_A{x zVeGgQSJi4TG(_N3I!Kp_WSO86G~d{w%liG0s2fU;B$xF`u$bfY`sOb^{%h#7JPv!( zfRBTvij5e#X?5cDfxvif$@zMUkyq#Sjn`oq*Ly!K2{`K!4?7D^G)IhUBh)8+nsXl+ z3ft#T;Q{ZYekn^peWD3XW0|lvarF*c2~RVsC*BI6{&kFeF=UcrexdN1u2ws)F$GnJ z#OYh0!*74wqSxvuXaSx-2`d&L?4py3pXz=en1ztrwB*$IrLgMdw%slsq5yxRj z$TLDkl`!MLcNmvDPf>P{7bZODo`0(17W53YI@KJ`uq_z8&&l3#nZ?ww*YTm(M6`1? zVyb$`qvRCe>1VpsOW67@)b8&P-|pkVs6%HC@9a{a3R~RX58j^rxiR-AZss$C;}!bH zs@faum2;mp!}TC!@@rb=50%huXg5*$cgiM}FqN5_-@JdDH@74uzPa#d(f9H3`$0nJ zbuJuL?Gd?pq2&-?4k4CTmfR?^svU& zAA1q+x-2J$5UF;k{dap))dM+Hllx-YZD$qa>f-8$-jyMtsq#ldbh9nE>RFdeHqJJk z=PyZ?&1!zPft73|>x4O*l;`^?1-^n`2-Z7XtMzRLVko?E*M>d87yqoTaL=Hkl<5Rb z-6}r1s+(7a(27GSZK76#Vw(S3)O6j_DuI)S9&-nE*mzyB@u&I8tAqvB-SgyuDaV%r zMj+)wH&Vv~9snM9Kw16gZ3eoj|1+lqc8kEA8)*C#{$1Yx4WzsjK5UGWkpX7{sMO%s zgwd`#bgTZ1>+dXsR_V1*xOa8amt~pBtYm(7a?FKbK+Xp-GJ0luPd(a0g7b3e>`oWc@zSur^WDcjAf)e`!z4LFqQffJg@*;{QbWc5h22 z!&oN-Y{keD7F}zmF~kj!=}21+pv3{LHvD4T+t>t=c(gD#KlAe+@A%uRYOl7?voX!M z+Xh^p9?RU<<&QJu9A_R$#O%-w^UnrnDVL>HsTmj0FnqeUeRSyz#Ecle{ZNvka5I3D z#=muF@0(`f0LT_)JAdcA&fH&7E2+6tF+;w^I1h3kd!n2Awm)%~1a}nEl|^-xH+_}! zi%)92yjhU&+Li`QgVV61p08dJW}i*GRow4plBx zCMh`Y2rhni_VH?D??5|EiSWHw1gk+3% zRh0_5%n=wgxmurJJT5f_*1di_+A+k6s69MyMx&0b=u5g1;Hr)^D7QTrOk}^P93rl1 zLtx0V&tE~po!~42|Lh>JuQIZIV`_{1HB)rw1FY%XCzj);F!l&#M?tl3j7u=!*-PMV zV`iit6e@4wymC9SeAlu2`b1&jgBaNatJYd-Wcsa^8dq7GiNA^VTEujlzo!!P|G2|C z7HI+_K^pbioukF+_60Eo^hS5^wBVZoux0^Z?T;cFNI?~lI&yXPq$Q$M1uP@}q8OIn z3y}E&Ae#rcDIkU4qE!)k85*j5Wv_fmbkRJ?sIe6(bB;xBCsucM*zRl#o^yMK%$*7E z?8hM%4UHQ$TZQ-5H>;jMnr;G^&7Ni%yrpug1ZH{ibKrby=eEpNl$ER`+$Ge8Gc7WT zox#x%)gA@D9;9V!;mvtJ@g{P3Lb_aA-;JIbZ5*u3O`MIE&&4i=uR9p}L(MYnBv!k#ET=AJpQFF^zBP!ii^mL_Y~IhA%Q z^DY87_N8zqb`_hos0#IW_%IY4QX}XN;KGD`Io}Jc6I*zTyXDt!6Xpq03S($mMUHg*}8e-pX8mnXLj&r{VQfbjnk$uO~OS*hh7eKDeLv=Zz7so zYvKr1r_B>ZgA1M-5~TZZGpW|E7=CAiaaw2#%bgo+t(UduDj>MPN$=4?pb{}hPUh+-}EJ$5s| zhqg;IP5%sYH8-CVJ!_MTJt&Z|d;z*;5NmPZ%fpN*<-*U;%HJlAIIGb7kB_T;n&Ore zQ|_`K38Q7P=EYD6v(K4YSJQG0`7zL8p1ef1RjWGL6HM7;@mA#JPohN%R<9Tt=2lI| z4P2CW^<>hI3%vUtU(jbjE!0@9g6b^*u>UG2{1;%d4h}Ye(U&8LjRkr!{S91!da3{5 z%2#C^G$il9y%dn$5}~*DZxDjxEH3ys^V(jU$lkfAGqGWWKX5$>VgFR7DyywHn=DSm z*<4c3z$7Vu7#v~nz~3POX)pOUY}g~{YsS>WtEbc&5*@D!6Q9WkzFbjTuY*V1s|HCJ}q68f@$JoR?y1v~O9J4ocY< zxfH2l-LuS+clYZmy0jIX{5W9z-0SX9*%E>jp}Uy2b3pt2O6vKA+odZ$PxdmbcLAc` zA37)i&VR)yCH|@A{}wQDUjZ}gU-317(Djd)j58NWNfmz`><0X&X)ffsWO)f#ai8uN z?KS3}+P7{k_Tm+wag50{CeZ%Q)hWZkJquB$V z?TWw5nGXQ7LleE1x2b~95S8wyU_Pizlbfbj=2Uguw@83Q)n8!M0+9%xFV~nog{uMm zh92|)TcD4?RdEY^o{S1vakJQoz#8l}+am~18CfJAdK+|#-L+t(GVN<_2Dmvs2SuqI z(so9D(cW9q{qL(ioeMfBebG0|R5%~F8lN!fns70QR4!Lal>72hI)W{xk7sRL5sBLm6AbXp#J7>pO9|<< z4rWCDx^iNr!Ay2Hv^O9{>Ej-a>cmTxF)ZOqU zgY5KW`^wcKSn8zwpKQwCm1&M`X)URcT^2zsQlk|_+tMeEl9C8bJy^YSgXFj=@PTm_ zo6xfOw)Sg{+xy-BdIU%yyy>K2&lmv+@&y{fe`c$0e>Q$EL~dcgMn)EJA{8>Xc{yMK zj<5elh@*Rjq#(t1Yv8JOgJ=&gy1!d6Ae^(&;7}-3yb(u|%8B`0#_-$nu(SW&<5zay zWzK;w`*w1&j5$VomJw)nkfRWla#im+W8^%SVIzjf*596ySym=6p|~o=$N4S|YRcVx z)=8KCaskD3Upw)8PuBaVP;||$=gKaEg?&M6C4D7%O}IvSWu=fgsGL;^MoBidb7SOa za3Bo3Xg@>|3%1IJN;|4OyN)yuMq^^p2tCZ==}PKW5(e5Gej>q0;)*y03)IdKlqNwK z0`}H$J0F!gPdZyZ*-xzX(B=T0x10U?nhbs^^c?L~`gUboYvvc;-8dh~Bn{=l6wPtq z=5|-aL1z|Z_&xl2q@x&FG);`PMb~|IM2rv^fCK-Qh#vlWA+=xg2go0-vIt|cOaqRK zhdS%Trc406w-w%|7VIwm@$bqjnINdzY4*Ie9ieS?f+`I8Q?m{cRzts4VOib?;VLV< zE~m%zLXq_> zi-$7o41#c(dC5V2Oc`o;FeGZiQ_McoLBuK+H$tpwFjE>o`&|pVT0;Vn7huSPk2VL3 z!L&ulkz!=bQ9Q#r+{nLdi+f}bMSZ$2c=+7g<`uG!dpJ*G@rNDmcm6`1y+yQ(~$@ovUidkzr*y~f9w24dVaCm|xAR8r4_ye~?H~Q*W=W^N6J)uiXwuC(ba+zZgtX6_r zm!rac(PgV(gLbSXeeDHY$bE4kckVq7WjB#sbv4^(^ocu0cRM9=Wke*5<|;|yB)V1K z;GYW4zEh}>8ExmKjWU`m|1=*p%QHz-r2e(;;W&IKB;M|f9#^HR{7}GcRgZ?-#t<{M zaqjxJj|bnYTjm!7*Y9Z0I?zDuivs}vr)T}g8Q~uxQ*mHG@(1o>Hh+aP^J3llUuBmR z`$xbz_$=uGzH9!|O202iXU|*8z+mAL9mJ4Ir-fW%S_#R|%|lu2%7(Bf@yqpmk5b7} z+^$V6YF_gM`RiIG(snI%-@#Oy*;?K#7IG63IR`3kLt38?%cL8t#7QK8W^5~G_(&z7 zaVww;MrRRd+(^R3k>r#}h|uO|Ns~5aj6F!}=!*$PRDsgrLff3gf^hBO&pS`!915Qpb zEeED5x?|?&`hyoRIXXeSMv<<%qas!1EU>Hd2}(K>~<6&6DHPadXC=1Qr_8 z{L4|djzHCj;sYYo??`iNT-tA7kdiHWywA%)17C&=70c z-u;|fJmPERV|baz$Q?Pm%RmH2Cj-8x@E&w=kv)a92nAP_Tue8==#g2&eSsO#)3utZ zEjWRpLN>ss3j0^YxWwo+eh^jibgWr?U;red&XAYeu25E=u0t2`d7Q=fyKYZUV_nIH z5TZ^3=Pyxf-_r|pAm$H47g$(mt82_Vnd&^9ao9E;53nIVOu$wel`MB1w42qKsV>a= zs7PT4Z}@&N`#nWiHOG&esK;ST1z!uSn$b;Im8+Uz&dJ+#gx^umQF?OXD)qFRUk5 zWwe&a<#h|w_I#E66|tA2T3Cy zJiiloVU(z zy$B17iXQos&;{Q!`g!41Mng3-=Q^q@rXD_g2H%Ke>hehFrcGsiu$va`H|U-{iIro7 zunZ;lm)d+AqF?ykMI`8&4w2r1OGbbt39RPU>GOraM4tE2hOA-cc>XZ-B0ug& zYkoYam?)LAU&5E|t%M5wIVa;L3Pdy5JjgR6c zlX@B`bNF$=?Bb&j>h50k|1T^S0sWrN3;>m2Kt+W38kKT?XJC%<3XD|=a)3KCAafCS zfus8wf>s_BtZWsxT=j#d3M4!}doC_GV_22b@u;(NlFjBmHDp`JhU#|gGH#5I_vO&g zP)lgukjs6R)u?)8CmLIhsCG~c`@N?>k*)hM-OlPKoeAsjOsgENSvDvAA%Fp)g}C}d ztt{d`evH8|q>%8a-2rFGkJ@U~@XveJY|El?W6W40L`0T?$Q|Bg6bJl zHe1JvWO`_e*8Pq+ej?)MJ0l3L?d0{|=WOkLS%Zb-qjdXbXTe?DZ=8tvf_hl!DXqgj z!_!E00S&Bp$DC;mPpYd%_TNRH3?AdNQS>d%@EbF>1~=wyt}t2VE_zq--52%(Qwe|3 zndCIZ?^6Wym=YZm&^#I5P`-a`fS6LZSC|*q-j3U-$L|UHz$AH|n-?w?l7T8aCXE*?FFHe?xG11h6Fr{!|E9y54`}>O%;& znc9N+iuHx^P7IAeqclcFjv$A?4v)3DaU6x5RUSfG_XpGsHJbZTbWk=Y5+gi>w(hxJ zt|)AGK8cv9kSb!HkPAOLxyRk)g7RFD{tBm^+4WmWbR-sUZWG7&L1GUc%xO3$Jp9^< zM9$>P2prETq*Pbz{7dN)JuRwg4)S+tKHpEm!jf`sQfm}&vb>(!;1^tk9F#BVM z@v!?NEawM0?kIo z<<&UPK0C9X_Gp{c%sQV6b1tZ^lnXY`tZFv(v6j~_f|lD!Yr~{}pA4hbUGY^eM1k_) zZqZ$^e*Y0^!L&L34H}qWGh)Qt1>OI8`3oRa9r0mn3IeDj`)?Wqpw9ed>}?1v{T%^* z)QkGWUnyoGUg0e&UQP}WcpW&CdBDm3@qqwRcT2z5FO^;#(*v{h|K=#a=DQyN)>eEN+((|Ir7QXrP@)f(GRal1X^=(Z1tMYj;=c*93ND&ih zRI!mu>Nq_6vhHIbk4YWYuM$&AGS_Z<3ZBN;B9sF|jV#jF}1`^5ef&Zlyx-*vf7<8qO*T*v&%l zLFqW&!7$$^2U0qmqv^%6wV_R?uL|BHQuIb3oM;Nna1k9Dou`;%f){c}!>Xu|%cO~N zRx>e6ThNv3fj6OuCUeXmYs(eB;`A^AvBjxLX<-U@WpUt@|LIzQ3lC+0n)~UGI6iRC z`Qk$Km%0|;R}by=a%V)3xB;@uhrQJkahNNkfHIi~Uk!sYfk>2QaY@V==lV`UYo_ar zOY-AfQG&%rd;6^&8(U+<>h?%Mo~usnbd8@uM4iLxIcp4LVmZ#!1zom+a;;eJF_t9x<7KboM{i zdKSAmA=HY=j~U!q0~LKfoFmM#Fk?o9Zh;@RehLy|QNblqF8XxFHG!7*Kw7x+-qDG2 z?0%3z7Nh#fYCr?gESlYVgIPyj>|Cz7Yvy<*GzK|1|~CgR(`weq-SA{F~Z2 z6i!D=?^k8e%Niug(x%V@KwSaQdQrWULI1Hs0G`N;CZmn*3uELzbw;v3!b}$Se`O~k z^$LSgaTBss46s3fOe0rj1?{q}z*FTYbc>(}er&G4(`qWITAJ&wvF6^1og(o;w2I-- z(7mUpdpOs(w``{4ye8D6h%%|o$a0?kV+~SIVA*y@!Z6Y2+)$|8vrAEty9I$hi5>Er z6T16MNe3nzzdOxug4yDQ+2^+GsxNFPD!N%L+Px&K5^!%fU^FljvkFW(g8iApW3as1 zJ}{<%J^39&awY5M*;C`lJ$UCJ>rO0w3JXKk?SoLsegfq@YX>{4WjU0wVvMbnqN-8Y zs@m@2>T-r0TnkS1tR#MbQR)6v1!t@kI!>=XCM-ZkzjD11A6H8FA+p~H-xG4?ZJ)^I zhBSr($6*OoH0wg1CIpVSQbMW9s&vjke<6$$seuRz1Le{JZo%XRthpnYOF5asW<7HB zhZ|l)WOZ&is9q{bP1@pEo;RG!Q{gvWM+X8{iV){H5v&yz#WYZ?PVW&{G0MnUHd$4= zDAA=A&OXaA8RcBoMLi4~{`Pz|SAOw`d1f&BW(MFT_rF#hzze`V{O8~t5U}~PqXi5- z{>CKn0T4p^Z?uvXD<}7k0mw$go*`!Jt{MYfVcQbozT|!&!a|WDz3^+K8_A72w!f}q zVW7jpAm|$APBL!2vz)QK%%mI#ILYzoE2W4W$-Eev_FG!ohcM^`rjirCDSR4-^2+nq zR%u%?m*JvWv}HPa%Coo87R|~qHS#CA1`!~io(zr;wELciw#2Zjl*+1h?IHSpfUn%^+_pkp>Ab4N6 z$!ApN|3I7oP7*dz=BK_4XE)2Qr6#uSH<9rCn6jEDp(1|qjcv}Ufq^Wp$jfvY30kb< zE{&LzTgFYiPIrm?9y`o6W|t}l6{V;n7F45fT#x~Cr>ow~IO>g8&%*iEvNh4XWI~F* z5>hmdi~cJz;tv1}Xyk6~FDZ-dzm)YK82bW!FI1igT-`#`Z248gT!%^{>Y}PUB<@e^*vP_2g@WZ72DM$LtkNuqsd)`NBzU2i+ z@Jws6Z^bNM3l{B(ML(7;dxaVf=a}T2J!q_N1rsA>qt5MLuCyf@Tl`Yn|H$isL){-( z%5a43AScGTaU>-+a|(ai_RZPPXd3Cwgt*H{!do|5 zIpVcsh{|yk54*fF__)aW`E-eh#Q^*pS?%zaEag-GFKawO%9>s2Q>wH#1FSAK+zxDc zH{8pq9R8nH9mmZ^3U3qwS?oiWF~^LA`3kxFvZi`NDyN}`K!p09EN$w4Jf9RG>Iw-94Y6Ut4;d%dY2<6v}CRB4dn#9C%{uk z2y>fXn#4=yZmMj@)94sDV2vTKgSX%li!ro)1Ejj(}QE3$25`R@MCO@S2JL24{O=i@`!i${VlF|;?|(9_wm=;4W=^g zgi*eD*4YiHXhHQYHW}eCV|jJZfZ3)qQ90X4>K=4%AE8zFhNyv&NI*q*oH3os&vAwE}heV-Me5wx;qqoCZO3qgxT;CvLw* z=w;8ca-F1}=s%E)WtdrzLO6Rk#rjdcCR&qYRl5vtG<#^oDq$6Oe1%x-TuR+52OTZ{ z!)Oz(08F|99D;z2&ms-J4sy&q5h4bmMN2YCE6M-*ERNN^P->w-iZlKfYQ%edFL$rXL1zStb- zB%zkz?orRu@J=HwL3%7OMw6;%cI&6ZOePV z&=hlkO!P;j@y`?gSGii+zMRSnm;oU{9xy5R8@0*nY*eJ!9t|{5$6yC|sJ3yUtz%ri zHTCa_)j(U{+#axfmn0OlgNx@B^2yOyeS3@|5hsb#%EZOgzO;13@k3baD_RcB2CdR= zZMEJ*vD(!kVE8WKD5f{> z{hE}b-evOTg|!FZfD#79 zi0o4XyRhkGYq5eD#Z>%wYm5*C1*eci+55iWcwsc+{Y{B_N z$R~QkP^?hcXEJWlPiQmY;Ow7?MMcqH)_!;?PZFJUdbSfjAF_*;ZcMMihD#eA{mbZ5TOxeSuPusBoQshJZ0Sd|7q= zL3@uTS@c#=_e)+w6N^OG&(@I}K93sYXEpBcF<0cgke%b2)!{48Y8c@XcN8#U&Tgn* znr(%R({6BEmL|=#J-r-1DD#9d%J^o7zC0;MuDX@Qb5Zx!Y)-wdlIa&=|E5n9bUG(*_&$n!Jg7%XIS+}8ta-7Du&j(-B}%T#zJv7G+=? z{zozUGsKp+2b2dL{*JN))oZx^JIIC@=A$2Gk*TX*_Pv_N4R=;wd&}gmX1R()DfO0O z$hOP7Fp=bh^O(Nd*D5~7^DJ(E-&U$F1gk+R8hKsDi{3k7^C$X}?fErK=3DL_wp~9p zTLc%=n4d!Y(RFE4$$V^56!9@1FKg=en7D};ddcqZzqDpfeMQlt`HB-!0U1f0H%cfN z9!vc$`1?m*fU$s`8x`n53~3JOu03IffXt09i^P=Y=k1@IslRx+!c)ugRVWlnvuep% zD;)Rv1B!y6HA`WmrUpHtdMVo0Rxm?4jqxK5`o zsp(1?_KQY#L%d!7aSy^6;K)d$&051h|Yj{&DBWoDsO z>zEA@-|*Y<#IrN?uTIjiv-OUKxdm>8$$kB5#glQ4vJBpE>~IDP&(K9*SJVq$#;cuV zp?(+0@4|v*nFPggn(n-N<*N5GcW4BmF@CAvOaQdv{WGVMws3T`1jM^+T>hM&3j+QS zmL`Cb*WXC*FP^mjjUE8Lt}HeKVn+<~@99t>TC8CZkr{i#uwhXG0wl=90+)?QnkV%Y zT+z$hvL=@g!vrB-EgL#IDjU2!MHMJKRrA%$+U7iux$E`Mvs12uXS_c7&BAPsR#JA- zE_To;coqW#?5xL+r+3b7k<3r6TF-pvciE>LCdIex+H_M7r@`7%bia$~9;|W5Mh7^> zkd;h{F+4fO#JpVU!lsZ?C78~{!DKk=E}|WZj}edBxdIrE*ipJm6D>j;)21Fz+0g3q zR4T3l*ZQ^P%`EwUtBy=%W(xkM=bzIbb&JSo+0n-0t@rhBd?#XCpz&@iIrS+oXgXTp zP)?Wg=eMnH6=_F{_*5GvE5mZuCwYrt5VQkEa$n-b&k;x~Ew1#_BD}@<>$+$8it+~f z!rAcP%fbniT12@@u;~d0cKjUgZ~YL7r=KY7^eq-$x)j$rO}qw}XC@l!%2IiXbQD?p z0;!8gW|vAoZM$S5P*gVMV~g_3|i6v~i)9^#Iliwk4(%^?21@$;J2b$89wS$b{g_ayV zecG-Sq^b8tgpxYJDhpZnn4(iD^vtyX}@~jI64= zq&#abd5`xq6B;n;C|g+Q>7C4ikz4MEp!{gWVrm=_M`Ycz!$~T$$!*F|&-L(XR{O%~ zEUnG79R!M81)$LQw~zUA+4rZ$P_wXf{KKkvF;4-MUc_wv#&q0Y zb_i2)AjM6Z*7_vE<{mYp8LQ*X7o(gUNQ8ijI}F35&Tt-;b!6WCJ;Q6G_ISm6x8>(9 z*XvNRZ9!LxgkLuk_I7c~Ntw#=!bl>FNDrP3+Q;3*o;#7q9N7vNDDUI31+QeAAN9qM z^6X9oG9eip192|G`n%1g5s?U*cagYpj^u@PVA@jU&^KV=Pxw6pti$8b;WnNnIv5tG z5w2m^_Q(ZA92hXOGvHj{Q95ZI85%=aLgouhtsyXK=5Lw6e-vxlXn=zfSTHuIyVq*) z1V}9@QI2-e9we!ab!H4mbYrPf3%U6H7V#-M(7W^mrxS*dQdZjU7@P?*-mQ{K_r_`n z9i-Z3x`t2EkwRsND19H>FyJI&`IRgtntXLtAzK}>LH<}br*L<6DU~A+HeDylh%o7b z?dH?K^RR|9thV)M$~(u7ZnuJ)=#h0D4)S9}?!39?Uszm{;z)%#;jP+=i9@?nq=S{v zCZ`;7j468OR>TqULxgBWK^|V}`zUZ(@m=x1UyXZTiiDaJdg>P<`Z*AK|C}1i0jjhP zfT19uAM_8-{=NzBi=fbdu@|_(k=vz5>3;;|$B3*1614j9pf(h7z}`W%_(L9wUDP)% z0%ffR)#h_{XGAnldPV$l7sgb^-4pih*Qk~PibKTn+Q{VbV(#lyFuZOXZl%y; zEN;7grk$*DcnPr}RfeU7L#GBLRo&GIpXmy=jyi-$n0wb%?WG01sP_}Y;=j#tX(FXe zdxRChTHgBo(shaKVcJ|&85=Y+k=URrP&6WdEoANEz+f6#3Aw51+*pZ&&&lY&Yo1>nxpJiHB7X--X0Po zxLH_a_4mpv36)$N5rod;O5OY#n2OP2NRpZk&K~*&IeWz@?Cv}5fx+%yjlv$@?<;2Yo_GI?^qJYB_pt>lSw7H1kS7bG;;BWv!N-dm_{ zTV7{_x&O{=>1l>B0o8bIF|!S%aLdZc$KL>!`hf8DAC>?XvW=4^^*_2pf2J}o`)pAI z;IIAPh+hY<@;G4p5wpq<3ykf2YWw|QOx8|@-paNsQb4To)NYiEB`_EZ4~jCuGyndQ zO$;dfh3E>RblpBZS)QKie<0~5%AO|+azaDT5cv(mXAFOK#@Ef|OLq6QM|IT{JO-`^ zbhF=Y5tORYj5=b(nSr6Qlbh}tgvZSfEj$dLi8Vsbf8E&&VCSz+5`MpC_lr!k=l19#4&;9sMh8(tC96L)9Tw+4Or?L zdwSK>zi=+NOk=}-0$?=-!1`~!sbpYe547X>Z zQ^YWsp%Z5op{Y6hwXK`@Essp#m4}WMJ`dJZ>4-R~X}XGmAYYrY&9eky0!OsCj$r9h zNkz*CS$g_3(iG{b-ObOjnoH`-ERdE+I7EVGy-T)9zeUdXg12QLED}lJ&{icu5k3Jk zDQBFa`uE`IXJrZEBSF4zUFNaS7M-qP9tFy@%8i<&InrYvhAB7{ROO7k_j4Ce`vrDj zIrJ^TtZnnq4)qlnkH>e32~Re@lAvhHVw3HGrLn=GWo6cq{V+jES>$Ar2;3AH93-9V zk!uwA{c9-P9`q;6V0N#VC(;@$^4SmR+eBZ<(p7yao^W^UGu4^2O936465=$s6ABKIrX zXe^Ox#Jz>h zelBhpHd^57M!ZEr=Njk^G7WndE9(q37y)D12)Yc1c9?32>GVR}p6p(!Wa-`&>Wwf$ zogBoL1%~W>`8BPOHcm*)q*#tyYAT7cre+m&3Kms?(Q2~wIXd{j+lp3=n$J#dpGNU| ze{tewRqQVp?t{T`I5-9ya%d63AZalr;(-%4fpxL^ly26qzB4wG=0O0b9iBA4%{*_e zMOHim=}X#NVRaSKqT^)w?UcCjTmSV1#5W*%Ra_;Vzfi8d0FeA~@sk6BmA!+x1#r{~ z*C`6vooNjh|*`&Cm{He@V*|r*(8V4me+XDAKbN+DFqn0GYwz8BENARmPrkf!eUI zYul{0jq3A$^yxnI!(}%qhIx8g_SU66HGhC;BHiI};w23;B6SFz>d>XF_AjDM>peO6 z6~`FsNQ7~v&ygpN+bu;N9d?>k+icw3`aStWT{VJskSIG!IE_w*QShnFCAF@hz7!HU z0`381qdk`d>a|380RhHHha1**!73eWbF30wr(<>H`_4A54+d>BZ(?Zu6Sn=JR9JS* z?v&{;k+*%dL8e_r(xR9S1V+l43EC>C$0{c!KxH6`Hsg2!3ER zK%HIaH#n0MLg3bg1&B5q(O&9neGdpRVF5{efN6ArT@XiBlK6mWn6K(vqxN(0ohL_g zx!5fjhfZcK$Yt=4)};BaettI|x?Q=;Lf<8mr@mC9sS=Sj63e2Er&kjk8X$1}`s9mG z0JITX049G!%!9v)J9@r{iPI_Is^&XxsGNA4vy_Ck1k zoh59-F;{(3+B1v0y?~yZMQ1k@ZuXLz*$*;Wgd>;BmFi8192}M&b;_>~%exDEe!Jp0 z86cYzW>Y?VUVG~WBRA2s5O2v-?4&z2aRn9=>i~bPCDxIbXNGL3?{xVUNxoh zHq!TIgr(G)T3q^%1rrB)Jw_c%36zU z4o;`$;2vDGnmuz??$DLXJV3w_`Sn&7wuDiYp$+69vTU{V}T zgmE1vE~Zy)i@FkrX!+Qae#IpxtY&t@{ zGC1RP%4f($7>%`|2Hfq}M zKC1Q1?};qkl!z&Ov?uNU-XR*v-DWH*>v$1zF`64(T$QBjM}%4~{aa>3=!rTDgN*65 zuF@$GPy_3H=M#Z|Iu8IC_YZi@ssAND{X2#fGPkfawm11Jen`Jx#Z@oBz!;c52A)x! z_>fy5v`Qz(3Wqmxn5YR7``f7)P^-D4(8~0_^S|v*If*8bZ*Lu=h$ zaj=X>`pN|vP=c%%%uI)r ztk_T+Li<9d`Ta*J23Z`wG<)UP5j-LXnYBwV35V1DCQwb+wHtehGK^n{EELr=h-gb} zP0F>LF4dmqIOVPBcIS&hE76kMg)MYG4A;fWudOFu_$t_t<-cEMDDVEiVgs=AVj%eM zDgU3t{=X6bUUO8E;wPlA1%QeT=8>sGEu2ChAa)OZG0evo0mb$Ld_%CZg6 z+JOEdjxh%0WW5^o?vRk{F3F4AyUcoSoF5kK}6wm!f}eh?X&uW!x8lQKs{ zdce`;vWQ{~QzjU-tR5*mjDaKR+ULmFDDM|1Lv`M5by>^3&mDhr_4xgR)fWFC;lg1X zwSC=E+c$QhWYPompV%sEZ0WJ_sXO?s34l`b_KgAW^9j3~npOEHH4M~_omp!nNfpv1 zi(pHH3;3$F_j`1_QP)CG%4pGYbOgzeq8dM7P+v*V;Ed6zUqtPHvHu=(IgYD!-@V9zC7YT! z38y2yOLBMxeF^hF{>n?qUoUp5-wptX2LRANn>uTJNx&b>z#8D=DmmKNTe;y20zC1* zV$b`!>cJf*qki*y`d5hD~dk0yi`CkT;OyhDK&W@s8}u2|+;oWvdcojv98 z7EFXN3UB9EHc?K_oXor*??q|{Nn2J(_b=AXAF@*izXr72g&gq?A_kNDR!b!@Loi~0 znw!*>H&J2t0)<_d(Yc4laVSLjHM-^@v!cBq(R`STLIx7Yc43|TeBur%NLNr2TiHbc^Uu&#ocFxvb;qw8WoOq}Q@x7R`z8GLW zw!=*4PqSm~^({&tu~%=ty&=jAG8BfiIm#6);DjbqLlsAGx5gIch(kVAsvg4b|5ko6 zLG+|)J;*GY#mY=5iQ7n){0aT6P|j@;N^BQS5`bB?Ss=2f5&+i=-sLklVy3LJO>Smdv$<|10XEDmme-gR3dF4;tyIe0mOPbNcEM|X~`cSwgBi3LY#g(cWhP0Z|`SA%3t zggA$%#EZRf-bmT^Tihi2+B9e}zC|*|31uuK=M+UL$ek1h@cK^&k1CD^LYA6ZGnjO- z@U~GdSQ!eJPyt67i{YQHIU~PD&Py4W)@fHR20X$+tLDk9gUgoHSCM_#JJyT(ipQd< z1wX27mVF>2t&@)*rACwe+1M9-QJ8crE_k^0^ST0-)a<$SK3WnPOOlKYKPvW(g1gIZ ztswdp@d<~dgu6rE6%4uBB{l8QhDwk*ytWm{Cy7i)lzlfOL2zU)bkKPHbkeoO$;Xk| z?S`?Uxo4WWo&jz3qG}rtCBRs6BpqHCbZCRhix>O4759^vioC;gB6{M+WIy*{AeW7$ zaJQU=mDt_Dy2{142Fut?qZW>I=F(+-NjJy_dW(FvL!TXhxljB4N}!F*2_@@M%n=@xwA*Ok^l``3Y3a z!n^3EaSukg+4PG|I9HO{0jW@9mMFbnVs;w=D`vURyV{#_D~!7DD#q05pfV~j_(2*| zO4rKrN*|Zfzo2bS{GjkiyJ1`_MJ&ZM4g3K<>};9GoN5QHsFzD3hhAb{Z~e7+@}xA- zDpuh3r$R%2>KT)>hFZ^r&%5DJSn@k)k@@}}{9m!6h*KWZ{EsVm|BKrXK+$+0^xMk{ z_^PUauL|~0(Fn*}1Eiq8@d2T~`hc7gW#q742!?=Ep7vfh2~GaA(3}#*#{y_SWkeoj z9(Jy;8-Dl8?xqbJK6)Ok6rh2PlBBpEzwSO~?HJvS&h&9OuN1=`%|JL-T)?TF<79er z2Q|N0^4oehbZ=?bpcyIy-&{ea`plA}vswi=fwTxpYdzvyBBwvtN%L)zH7D!Vas=g5 zP^H;VB4RA0`C(s>=ifhS`VKIkGIEU?!)7lu0ufPN z+*si&Ab6DPT)U(=KG#ka;G4@d;o zWU1gQ(@M^1V8w|}t{=13uJ9VNTlhkP*JbUIA>!`E@f!m;9-_~*k;qnx(CMB`YH zF>6qniCJuS^;AXG%;w-s3-vLwwSQ4#P!6n~N6NSKe{-Jq5L>@ETo;)t`=}hLn})8@ zV(rvYsJ@$O4plEp{ZIg3;A<#xSwcoiG^u7MFE9N8tY8-Mw}~-G8=)@ip2=H0ct6J5 zF9p;)r`W_87zLM_#u=)IZf_BiXLsxqb*RAMY^!IkXX1d-sZm8u0a5$%OA@uIcrOR6leb;SSoJu zSEw57uf7Bm1({F4p>-$F_2V8Bps^9iVLDGYGlt~z;zn{49-Q8;{=H7v4@y;)((sP`>pNP<3tn1O(dr}zGHC@qrs>Jboy?RbPxqo*@CxtbULgb&XM z5%X+4UnX4Gej@m{ zsYbEh(86VbZs+a*nwn*tcTXJ3bC*W z!%$U5yaM92#2w@BLDLZyMkCse4z{Z+p)S1|iPK&fS2}NWT7{jZ*!=;YXZhMkk6OT%b3AQ|70u2x&jFYaey@!Wly7U-k zrpF#|VK}m%FotdZ7ek2v0MY)!b@%|pQ0D*V{aCvgycnkbzX36@L9{fsFtPtD)vv)T zFntE(&@dTLI*88kbO#8~(o5k|W|c}&%%S)A<8jMz$4`x{7CM?gcjo@O#W_Ptg8ba9 zdsjQeWMleu0&W9hV{UwYWI#B=nQl<7-R7ZuiTU1E$|h{lo;eSDjtuLKgQ$7I7f0-E zTl!ULG2d@PJ6D_CKfTynK>LyPxv?vy$tx4Wr3(u|iQ&6ijVUyW3C$x5x!kvweQl~S z$_2H)T5Pdte1qT>ow|bMEq1wU$dr9o^1_y)#Z*trHr05SGF}BT z85)^uO2eLJBk&>xaWTFVPdkpeaayPLHh0B>s7OMVWVU7yhy5w8X|=FY2V%!CITh@{ zg!95WrF4OGT0o~qL%&RB1rOuxN4zo*XN&Wjf$iB}#*B?FNisBPwKg+Jxa>Eb%iy0) zezhjo^vAeG`h~#9rO{`wS6{nGc_3WB9r}%{Q%$O&dP@9|YsQ7Bu z+@xaTsM>jVBY&GhA|`eE>`?w#5->a*g2@R7>#RpGLHu4{g6_sr=iR4NqINX5!j`W- zkQ@>wK9gqj@{CuM&}V+GQ<##OY>T$%8D`jutb5*9_Ixo#A)irsv}x}B-K+!lxf6D6 zTij`BaVYP)bwQsLU99=UXjx1Tr`7SgCF~1uhHL$+IPf~)M(u@4@B^l zX{lSLwzwzmPeJXr@Al2~wwStWXa!2Y?Y~j$p|ULXeeA}5csEVe!W1PtHgM?Oi4^eU zrtULe?;a?&EpAI1fp-+KXK3UIlBi{riQ&bHIyT@Eu0y?rTD~P*->yw-?FIca7os#x z988VhNWm@KF1#+pQ2z>1XHrX8iN_2t)vA!RQ3B0DA%-`qwI<(N4<%}IM!5cZlQMIB z#dE_9nT4~Q3x)-AjX@>@y1vt=pmjZ0B0W%CAGr+@PLgTl|A_m_wk)@(TLDQ43F+>Z z?(Re2}P-bE}9F75~l!3Z#1Kk+x=8xk8zGw22(F|W-HN@8+1CU zpF#!3D8FR*mQaBUH3&F!1)O60vnUB%cba&tdUDcFi^UXm=e#l_is&oW z<5$0S_qK7`Yrdvz{S5eE<&aOB9uoNZJ%-b&c1W+vce1(>clzW4 zvi^0{*Uj%|IObmph=c%e+ydU=m+k_3NoGaR9Cs?nNvg0%TnkW8GM_JQWj2s>aC@+ z1B%+M>If;oi1#72wf5VwtqFd|j_`$r)-o6CS^Lsc4SRmWO*X3XUQOUfCnsTb>rBjU z*x%xFt{$a@&gG?6Yw+;5q2E5r!%n5&$&yl1l5YgG?S_a{ccl42tam$OhaZKegI_5x zZ>$tjSP&pZIjmEVN^yLlUm8T=)FedaPML@(v)FTlCPv*RORdYdjwI~|qd!N7=L?U5 zC;I4lZ&>&9Jn11WZCH1#x^7qR?8C8gkhTIrBWk)_vq9I`qIg7o%EgtS7h(hTsIE-` zF5K2wY$%eKk9B40Ap*CzE>nEVQcAqfe`y;i0GWQSqKkC^GVZ{`_hPp!2Vlg=z{bH* z&k|qG#^vv@yNF-mBs2cm)Bb&c2gn@)mFBugEn@F<;lQAnUKfO?EesL3tfqL^Sskc& zX3oRikVMpaTgk+3C85bqdLiw{J<@?#o>YRyguO*i*dCi?POs#54Gk@M4tXB%saWFk zt_{hd25OeLwOF3ro!0lGCuh*fi$_jsOA^)0B=AZ0qr@v&CN74Ujz(IjoG3a})W0PT z7;ChfSMt_6etoLBKPy4^I1Bsqd2_7l8E&c@&9sgc=xYpyK5g zD*O4$?2t6fRFr zTykdV=QqO?vCF!NDBt!**>io0Jo_7B2=hhYT?jBiqhm$d+il<(WfJEiHkW_HE68-N zOZeTALKGfn0;a3@2bq8vd?0z!H zApBIWoo;X^0fWAg@1jSezSEhpSk7eHowu+2a=`!qM(Yo^RlRxhh7JHB&GQo~XRik= zvpiD_squ}0%@#nm3HS$Sp8l`rAs{PnY2)%Y;uq;FTqX|jz5|?-g4dhZTyA{*B`w=| zB{Lkq;0V11-xIj-kvd~ z1bzD=o^!*oFtVb!AJ_$CbhsG4EY%W^A-!= zz+$+;`rnrh0vvS3`4Sr<@G#&5CT=g>@1Z~c{u}2?@akNITr?p|CFDoHW;^acb{;k>Mo zj4vcfL{dM^-A*;*dQ6tQ$gq0GEk9%6ksyJUTrdERKb$a%kXRBdhZKX$6f#$bw@jL_ z+fK!G*Q1mS2HKrKiocMLR7Jw(s#7z z*3aQab3dlO#tOTJ4m1sKG6Z_b5HX+X#bA#0^b~2bliuoNHF;-|VRhVc?Y2cxLpzOd z%ZltM7BAX&y{Gf5Q@9`-hd1e+_#ATpMp3Rfeope^J-ggV`KKni!JpB&w3*iYlx_6U zPH?qJOU+}aT6^)bH5A8#My+f`D?&xP zueJ++0p~loXYgwVh`o_O0`RgF`#y)YyKb0Cug|X$%^?l^Q`5o`R%pEEn=a%KO`l9|M5!x^oh(2fqk!%u`^XoTdN&y z9p1Vknnh?Xn={%vJ^R#u_SE}_uge==a4Y9JRnv?g$&~(AAj=|OVCx_y)7tkX7t@H8`^m)wvr!xpgO29(z5h`5-NhS60Aga z;oWDMViPBKlSYX?GbWxMu5Y7Sfp?u}IbGj*8d+@Dhgp`BjSC%XB@Jq&I5)Wrfx{yb^V0CD(X5@n^ zZ%xS@qhrGWcK>|SmbC8>)%()9ek~GN(0;n@u*)X8YhL7;*M-Y7 z2o+GO?_q!s{00Ewi@4SQ1406PK(Pqmb2}SZ1GcsNhRGAf=B?2c9BJwqSei3!9P)qVGeg%c!C--8DW)lcG@s4ZF(H<@;Dvrnn`J?+W zmQvNJ9N(9|dhRJFU0&S*>b`fV36WC$bXFw0@7xU+Dlq4I!)DY$B^HNG!k5iKpuCTj z{~)L@O>#q4w+L2hd8Ld#eU_1e7#oCDGTANO5kV>N9E2a8K?9cJ(*lyTPR@!-kk3AV zKhf42FF3@j4cSoZy3m$(7YGoW^%U8bnCu zk)@iiS1N)TSqA5Z%O=O9W{V(5pg3g0niCiTH+7{5_2Ms4vRActD25XjwQ+>GP+jL4BvSj`$ykzwK(i z{;o}hiB(xXh$$Z>&|@8+6BHI{3iwLQs8sEOo&VbkT)ySvQB z&vl#$)lQQ8jHVwZ(JJs`6Ox@brCzTzX=;uo6LWj92%go;FTJBnF`CEt84Z&Bn`Orrm0#>QgiiC{|=ib<;+r)(imPw>%P?+PX;%Rtus9Znc?SE_AArQwM(=3*b z^QP_5u*L`u^G&>`rxz>DlIao|sV}eow&OS!NTO=PB_Ty2%jW7y7LGuAUH~S2OQh*Y zhltX)W$`YXPZ;v8Z;yykZGZM}cV)-lad{+KzJq3J=z-dF#XCj^lJGJF>egp$FfCic z$Nt8jW?kREA#BpQ!4^zQ9CQfB$u_)ffGohcQT`)s#9LtsFC8r1R);eK#+V$0gQS9VfMwTM$QRPZhw|rc@f%wX%GD!ZBpG=$$#R+sAMM{3goFr z?w?LH=Q1oA?zv@WTjU*t>Jf*TrgC2<4Dp7EKP$;FMuZz&0o%;038Nm4ES;G@;F31p zCm^U^Mi%$Iwmb3Osil?6;!s-WOXTva%gzecIeD}m96iCLWcMz9jDtgOdp5@)_LCMeV-9XkUws+U zz8=z^FfGp@BqDG<%1>U#g30H&g09^3_lf;JLfV?1bBVt-&mfxc)&j~tr4_H}fsbja zWMg%s#1Zy#fbMtV)JjCN>*Bs|*--X^?0?%%jD`mmUrE&$Bsai{NuZ>Ba-&*LHI^o5iia*ygO}Z%%D0S{PCs*U zZQPyj|M?;(d7hTg+uI!7LiamkVGlee+!6lv7+d!KM+{>p9e+xTEiNPR_dvhUWmAqh zsf5!U1ATMpf)1eZDtV3QLN$_*%6c>MAy6faz;1iaUTgVwluiyuywj*_az#P<{rYrc9V$z|%rM`DOG_E@|_&G%$1NtQ&#T{RcxsSMwM(OsLA`OY2{D93=K;TRf0~! zc;C9ikoNSzG{C^{m#E#Q_Lsn+j+%QdU01$-u`}+(@2Yg31i5_+(eo8y_+~?Deb}~>J3Bh z=Zu2JaiekkRW_|_b=ARPw7I?77!!%mF9By{ErUcFDr1yfF|EGkiY#_p$TPCI?K_Et z1ZVsxEpdv2rAOZJlV#JqGgvM>oL0g}uUWHJnmhCHZPlmI8+xLdb`&1bABjF;yI}Qw zX5Twzxcb8jTfHWX{1DQ;S^^j*O_quFjL8c!XAuk3g|+6TXs+@~)OkyO8cs(mnV{kw z21zvMQxv5gk<2}XmHt8e^U5*v9>C1z_lSni0A_*!%)G29{10aSNfBRaZgGddw)%zf z>g%d`P80tDO_Mo-hno6_nD=|o@D+g{#HuMbOj%Y8)B4%Yr3jB!H^skFA|a8v4|H8T zWMv=KH*}CC;^LW!(UKmHdg1E~>v+aXYPR|WtF$3_qqzLSQ5a+knW>As*%wemO>ZtH*n`=2ipg^DF9c||;bJ>Dj-lG+WlglFdRsWXv4FR9)o2rg$sP7JDH{!r>kjLqk=yrr%XvY*nXb>FgObzjL?X zffZzo_oJp>(am)6RDflL$6v3lC!B-6UpZ*#!mC+6+F+SZUvg!WoSU}{&go&wE|a{# zv^#~jf75B3yK>>4uH%VUW@7{4zjvMz2*X~)DpSvLWh($qUKU6J6IBBzfb9zu5^_fN zX26!<-`Nhd1t2p0KYvaNoX;BLD?kbStTCRR6AkNBTMZRKNba{F7L*4@l3CzSp}?kq zdU6n3Xkq4`&gMe|f1Z*sUw;ZtF?8f^=@(bwi}0p|5n=lYu-Jt$&djv0mhc5Ces zk;)`L_A@2F$G~1dOHUQ-+4$FzS4%hV91fB1dZepLS`;TVUwcEyX@GETw2qRM6^7@F zyYX@Vcefy2CEXa8CITv_g51%an#v@c3Ix~^jQQDwX<%23O!{5g?w=dgJqtWM+nV18 zQ=IZhIF=2uea;hB}^Hew`RG+o2@l!OA`XVE z$x$1Ws<3AB>fe=8Ktx5V1Klr&k7X3Tgu<%e@3)=KOmuI3g^;S&XM5WuWV$G7G>bpr zwnbW1#P)F&OlT;fS->&z+fwRz3Pj@m$d@}rRMLIv1TOH)9rVV<)^Oow$PK8G8hr}5 zqhWE|i8Q_se(AseK>P>QXIi?yc5;^m5Yoy4xT5@%k~*6?8{sPfJwp>cv%khf{}-i! zaez`M{reZCPFFa{k74qmp{^ZZNSmUBj-SJp&+D|)cMAzMb{3{i2l~b;y0?F7|i;vOVsp@9*ri56f>i; ztRkBG9xH{Svk;EOsOX17{FKKxbisanOtt%^gm0ihpERI39WxXpu0L>7Du_Oud%cv7Gi(Jh{HL{mxzJt;V{) zM&jVj+Lqw_x!=#kOee*^r~KFgQGUm5RcoJ+*cpI$(|I>wS2(>jmkJl5_QZVM^921L z5S~*y4lQ#Hg2uC17M}`QkzTPaE%Ve|u@e{~{i8qe9Na>Zum&H84=EfncTqqAZ6R29bt zXYT>kEe;5_oa+M*^n>1@>NtX!H$N%E!~LP_i3h+}9Rwxr>Ip5p{o~vx_t+G5l@hy= zsnRoQwDWk(HV)(4FUh6dTM8Owpjg?$ny!WB||7VwEfn zr;AkCuOQxjSU3%08Z(2UP5lYZ_CjX{ZCVIU1^xn3r0%MmB?-%Nmn?W6#kM5Sz3aw- z&+42|BLQ{CrCdBGA1yfdBB1A@W2N%(mT2*wUzK@qMs+=Uct%B>{89KrAT&+=%KTdt z#w-7x778Kx9uRj|L40?6OldIgOI&(YhFEn?^MUF?1_E^~-XMov5?RQW?ms&JAqoXR z6ogz(fdYUi&wu+C3eVGFCEMqQg*3p?1K3u7$!bIF6`Z^j+GPMwm1fpGUfA`p&jZjh zx`f1eq<%{(97oosF1F6*ovWOU5dM%{)tSRh*W1I3Q6i^(Q*9mC-Qd2{C1*uhMIW2S z-ji<9FTH81|9t9=-A2Wg4@s#fE}Zq7b+U!9dQRrM{vGEWfyIe3!oeQ#C{mSl-!|I( z;E%*x0rEZ~M;YoVbWny6rxutWTpAo!8cfu>o*8sR7%Pl{?=!hnXLJbDHRp8cEJo##xSnM~urt^ZSk+89{8c%K5=zyPP%(#Y8HMNC}!ugs(P zUS0jGNW#@7h&l~uAoz2VfP|roF))d6C~i;?dv0J5f8hVqB8o{9S13pp>v^&QSu!a3 z5W=uNl2Ij#fpCBVd$Nx0e*3A^ zzLd0#MQYh(W?##VOH6`B1H5iFiyYX5dEZ~{<|Eh?hGok%5lS-Sp20oVe_?_g08DtMa_AHQ7%CuHhJVgL6oC~u2V=9p zqxxg<3Li#sfGa%Rd*N%)O9W)sSZQ3-N!op_W~dB8t1u2}RDQH-HYBx+^|()O|9G9q z+2QqxOaQI*tkm@_60PE0v%;(HZGi)SgaMj^^p>u7?moYLM?zU(T0I6EtWeXpK#Onc zbe>$YvKEZXG7MOsK5YkB_kA%b;)&v)T)3-sW2!iZ9I$hAjS1J|Qz)V(P|U6#CL1v* zddN#7QH!%gKUZ5^N=K0;NHjappR$FrBixezeEv@F#er=~fZ=%v}AHS_2rRgYxfo z^ejgGC-WcDs5kIk!!JLsXt*+3peVrBj6}F4ix+Y*jBZ$%Txuc(=c1?4(QbJLJiPZW zSjt1>!`WHL$JyiKf?Tkt$cS-5%lHza8A>N0?RRAHwY2sYGuMtt*`$CEbh$m@YlfwX zs&nP7fxIEph1hmC&Z!4;(n2im0X9S+zVG?m!n>I0<>Qa{CEv6+S+KPY^unWIZ|3|Z z)d{_~H~qG8%8>Fgp`AwSbU-z*c991QT7r@I*(>e4hn8}`UJh_L`X`J+-u0x-Z=zho z=AUzQz79^$SQAst;7A0p_MGh^{)08==k6=OtNc4~v;4o{)-Lml4%it3tZIYUFFW8X z-X+f?hX)uFuYvl3ZM?@EWs*dSj;mH$!ClN3Hm#o$xBT;Dd3}jujRbyUaMis9dVraC zMpa`gD_PgX=8yLV!C1hp9%+qlXQXL2 z`gwH-LfHIWGy=F<+|W+HSXplkP{bPdG3uM8mzolMx{*}E<_>J`CFEdD(47SDgADn; zEvBn|`@_|bObTONv!8ascv2cl15<6fjfMSJCs&Lq)(JRONS&=m8VUrNl-xu}EBoW= zfF3xzr|t-|1X`fS#1Pd7CSi?Ll1ZXVQa_UNw48}Ll=90D$7HjDB%|NjKQ(>GX!u0X z2o(el8SfVXe@`#|-oy0ra}i!!8l^55bTny0oxdhhW$qW)_M;#L-wz3YxG5caFB`|z z62ca4Khve(Wge?6av`9*MGk$WLmXG;7SB2>uvqTuG_6K9;U#nPl-AUX@N5+vUJ$b~ zu3><~%?;v4l8v1-QGtN|g+um)tXf2*ZJKu-_Q1AW%hLob1(Egb9(6RcCx zSIhAfqZynRC#_MMjp~uffABEIuCj`eQ#quFdR)0^V%rnMm!GASF9D& zx!G1Z-CwqFg|1K{+cbm5a^RDzn4U8)z-0MieuHstSxJK$vDIDf3hI9L20;vjA3c48 zWt21uhh1+&b=^@f)kLRJLNI#my~W%f2w?*zG^Owj&nQoQ^*jRsIa;m~fpY)|W;>D;(3LzzG)*9N`3~2tf*qZyWVU zrGvIBQtJ;ACPcQ-HNgdn`hr1>=)+}*c_MQzVdnYRH>7Sy(``mx{Fb7FGb+I(OcR&7 z*i&;{l$MAHO@7+>@%Dgdxq?FH{@3pwpOzX=a&A2+#uXZ#Fb>u5ZHoCu*lQ2FLR2sL zRCC$acejIgtf{VgSkg!A+2ZQ@k|w_V=s+yN6lq{)8|RJtD(6gvOWx}e{^{TzjW`{S zKwEappOEwPtz^1y@Hn(n@fo!Lx$Hi5^ddzx2A3F~;*aUj>?xgMGk1vH$Qm>wORiSt z%3U16KU^ZXT*yv-8l&xfT#2SjS$H2NzeC!>tKFCliI9{qj8NU^gigO&)Oz-C&If$f zpA+hIAbP#5y8MgAPk^U}=`&mWSq@+SMHT;VAo1D9`ClX^#tuLGI0u|Sw082rHy1!# z2_hVq6ehf#zzY!{4XcUzWJn=q9L!uiqpP6}EkU4s`p%qMK7s4S#9XtY~ zxP@$E9n}2@SMD3hX1-JWMVtxddw>bOE^9$PHEFte2gg%MsZYZ5NDz#mWOh1_dgLzU zHx7T4d+McyOF`-9F=SzMn07TG$e;P^zM_h$5B1|v7Fu0VMQ(D7+(`|B-?7z?Zta7X ziA9IJ{g;xui=$vbBPT@c7gnI4g*!y+SP&As4TQ>4BqdWlIgK@QH&IQzPQh^XH(-86 zzttmAH;FiK3!=@hrwYG&crWi2oPkM*BXAzA&D3v$+-$&Go?%}--0@h_YV4Yh^x;}H zF>kDhTL-oa>nn1vtAVVJ3~R#4z)A@j;j9K>XA=`?*_yk={W|QE>(7N0*f|EgpMpWI zHW>I3L_Tt_$#l<#>x1Ktt&70j%K=%^hnJTN@b7-P^uIX&Wq5VrRtoK5K(f>fd5(Z! zzuP@LVU^2NUlX(3R_h@)H<7O&X@IE4Ks4Xuvl^RV>5@w}c#*Zz#G2p8c{t~CQ+{VE z(6zb5Zp3;UuHzkfZ=-XD$QHBF&@KD*Gy?K_a`?2~4J z(Nq*%h?L}HHa8vutVO6yg)Y^H7q>S0E)rek^3ODz&-D+29k2ugx!sj7IG~ZGh@j}# zg`ir9q>5T%dJ2ru$wxm@rWQ$Ojr6?Bs7Ud9kAlD; z-_#7!mF>zTCH~BS3tC^UY!fbb&do6_7GRQQDqm>2~XnylBy$aCUQ$Fv~Wbwi2*rQlzj+(QiG+zYv!(9Ov|b7p;B(F|z; z9}_hb#lFCfGz%fna4Z=k!-YT&=9$WMnr)OX~~Y^577Cf=94evHsL; z8E9&1uR+?1wuo3lJ1kaLQj@y;2rXi&%l48PP*iKOX{dH%3VJ6>AQu_b8Kya+nb@5{ zXene`Mn;XCIac+aaA$1|%XFcIV2@!yDC;+)TN{`-{=-zar?WQdH4U^h7%b%`Hsvun z-#42}5Lxk_ivR&B(L7cx4$AP(KjJuRop+~j=pw6a7HDyV!X-0Ils73b7}xLS3ixV` zGLpK9a;J@}4T)!0Qo_uyW^TSw*joiHi~X5^%U5(_c009amyZ7&YM?}tL7KBgwA7+G zdj9wgOj#eI?9-e`0u_hWgfXRHHsp`Bn(x;H4~-w3Dt>(A7*G6A(}uc4s73&GEP4Bo zS+ygS5_gQn3MRM%*<1m|LV49$(${> z3+S%^E+he5ysRuKn(6^me-&T>;6+;)A8=)PMu`H@PW~%G!$z-A5*R0GvBU@4djjDD z*^X;JZ!Lx=#}XVT5RWVHPP$J)0$yFCHaN;ebJ%s*^IpQhravx7bGz`57%%t8=z5wL zLbhE9snAWDd$4o5%gr|H>G}py6G+_w;G=}NqLoqU zIuUjYPXgst8C*Wr+?OkqR7IM3$Z&XaQrB9Drl5?;4LH`|x85jh#7~}M- zp5Y;oa1&@=4Fv(9(g8sAGL=^}vjo6ZyqrcbRp+ls0>LL`X@?)HT3MIm+_fXzz_MeYjQka{eVaplFjWK?{n$n6i&wyQlvTFn<_w&q7u!>zz(k!Mzhh;U4TBeLobxzdt=t*>V{WS2c z=Dc{*+9hR8zN?noYi^RFhzl}bLR3!1>)Izj=;dF)ecYM8D(ot{xjv2?CWQ6gRu>5R zBL65S$^RsGIF)RIdgMoutjaF#OntoyN-GlVx}zJw$;_0yi{0R*GdUeW>x?Yax5)4QL!VLDP=1yLPqp|jQvUD?7C_Brl>QTT4fX8-Y83_ed zwJTN0ASay}$7iA4A!uh!-mdMdkmg&W89fnW{YFvwk~l+NeF2xytpKj7w|gv*ZB~H_ zx%F_miznJ5s~z{8L9Z7DW(r#@%Hy+X*!mpZe7A=hK=rEo?#8v3;ER_zg1ufcisgp1 zx>bJ^vm}Yvt0Yo?xb+uwJ0JVHUBr1%{ZX)G zl^HR<5RM`Uu>R4#eU*VcyMLMGALir&_Y4kH;V-5f|J?J7s2{$do)vKA)&OY^Slxah zME;GtR(N&SnF@{8z}n-p_gqii24-(iyrhGcWm|tb@=SqKu#wAJ;q4R4%M5V+66K(5Tgu&wb(~C zapTE>Cs7fH=%LWTBOa+pd9FTHXMrUTC4<}0ve?L*=CMsw;U`fHQ1Jn228`~Xq692{ zD<3rmt&^{}Vbcj?Xe7&4W+rwwqc&G0Q9Z~~rGrEYNtKskHwBcwB5@YO3vEC;iWSiVagz1OGO-~d3 zbkXkyTCE>!O!tULHlsg0>2bK(0(zB^DwrCZ8s_gv_PDK5u14;eGQate@QfJbHuKl- zBz!qCC@rf>qNdj`DE}p+4=V@xu`;81$}2K9vR|Dmm}|8`Fn&-h^E5YiMwP9`E);(J zFyj)v$4+H8w0?7mZ?r*u(*=z2&6d8;Gd1=mg&&?V$2O< zT8UC2YL|#qwA80I?O)o9+nDDkv-5?-N7?I4;rAlFKUS#nI*29FP=q0b;wQOm=AYyj z3kUCUPWtuviJBl91A}rfH{F7%;4xBf%Vi|%Yj1*>I9M8EJMt6mmhLJ+-hN*T39?$l z`6N1*&917v=pSe#l@%#H4TjN?*K&y2&Soi6@hA42b9%8@J;ZBZ!!s@k5!%$P#E&N) zY0|*v;7g^!*ZVLyJ>&xx6s=KJ{|*7e86jkKDKZ@xoNj(QJctCgTBh313(TdE>K-s^ z%{t^5M@j9@aA-Z!-$g=cs;BvdePZF6ea7#R|I5gqm7qY!B9_yDI2i)Ghv+Xo0vlsT zAYpp(>jjE7BYVrgvcJIkDmX?0P6|_WC;`vHXhjQxA9Ix`inl($1yzC~pD0%+=vuGz zE+Jy~jy)n7Xx*9bpD0`)q`eZVZH+MTbPGk`cW){raH3x~-si4&N<9;%we|Xq^zn0K zb-y&eO@)ySTU|-Ha{6zHStZNa(L<=!gZ<1)*iu3NW&W1(Qk#fz81mgAl%iJDRmD^6 z4|Di5)ev~>$3zVwIP9~x!6NhF9W?qo+`;)i8C!q;jOjTAbHc~k72;*jeBa;iI|TWI zz@ZMEVyJUvqD5`3`jHs7p8*0oRI>{%TQp*-(3A26m*VIUwo&I7^nx%8MpUNxmuqK% z>M9ory@HG}(|O)IZ0z{dc^x!2zsGF~iq#OodA(G)Cex#^1NK9XD2aQn(#Z-kk?#lQ zi)L#urwn;cLnSD2275K&+4_5~Ip}CVeh8%Ft>Ae-r8v|W8fGAH$LI^JV<#qI zeEjg?OefX+Y2UmJ_f{_#5BrrYC!zVbBmk=|?m+PnLyJmQd%bZP76QN+$? zpKx)2NsfNDHKE3JaPf*cObuWMJ*Xw23c$_-5E)S2XehhIgf=!6ml#-Xqz$SFg)}e!BPV?O5`tY%O)1gfetJ~`7*Nye`QlOOR zu)h$cdb>QqlJ0^qJa@?QSB{ued#eF@T9j5v+zW7$V{pZjLOEDb9aC(kY1*ix1Y#3& z>6>cvtE@o*P8tbp)acJJwWmR_+@nlq;!pb5-qR0qk_0#hLH0@Rqa4%FYG18??$cKI z*^&KLgU<<-_ts16Est-wgcpTw{)81qr|Qb{E>ECLti#^G&o>&xu?p=M<)!!zhgV44 zfK=vQf_4d1G+v;)FI0@Ju7CTH>b{km8pCH-*!HE3O{rK7a4?o!8IMa^Kc%ZMyZ30T z5ys7gaHG1xZ}J)OSCQu4uLb?N`#C#G&D#2kbNvwj*>UOk@-_gnbRa;zcqS;?SOJ!T z&+o6XjXhwp@pmMe++IN{@W14321wqzN8Ul_j8GtP+Ab}%bCb{t-;pcemmptWV}!Nf z^TXD~-ptKsVvG#Kc_DZg*yu};k~V0Fl36xB?v3kACSU6Sq{#N@jA}bD3cTGfsR!mR zrY?0o8*?q%6&`tH@UeSfEC@$C4ICjq&T{$NCG3Z{X0kCfcqj2kU}1;=O{EZ0#9-wK z?ohX}P}KP9D`gg9wYTo4Ad-pwJ7$3DT;qhLCIzVYAjHYT#4jQ^IYMR)qi-bfgqe0= zBKR`eC0>1&4M*u%af59)+20OYoxHx4E((b=I@Q`Da2Nd$gOg4QY{F-iZ&g+p`a(>C z4`KO8F~P-r8NZ2*CGnKE6?OD>Y^DK$r^j=aU!c+L5MUMZFsag?x%QuNsvW-@2Z5JTc3oEovNyjfkkTyoYVLX9K$p0{Gy z!nOW7DKtD8YfgI0XLOAn(5^|h8-^K6L1HIQyJ`XP_iY5N=J24k^PgF_@U4Hk?kA`b z3T;A6L8+bD(!mgf;9@LH;q#1oTlKMF`soGkjJ2N-Xr8sEdSEyQKBdC^vW83C#qh^> zd8hdv{_S;D0KGW~tyU$?&tVN2>R%ygWs z%c1Ts!#8CljYbjgCCa+d2-VJtLp$rlz1pbJ_0lHi^UZB6aeqZD?LYzeWn3 z_^Kk2cR@E|SFA1JDg3o*DPZ5! zK0}gjZI%n?;-iuF!P18vDw4W|d|UPUUbdwn5d!KzgFyU9v;?*Nwfy+O$a^kuK4^wd zG|3}A@2erFQ%#s@u>O3XXObcplh>L|PbI&zcyRljow{-Fm&l=IH*0Z`0&(-0WfOX+ z%WH6ZDq=E`Kpaz;gJzK_eiV!Y7qzX6x5+o3ElNhSUKOj)vOK>^lpH4kaOnZy`sX3F zHM2K+(H46VMiBz2;LiCt)um zuSHdD6!beDnyV&JO@ft|S@&rrE5?e`56+Z|_rxo3^^jcE-`&d!He?>(obqs9yp{Z@ zC+(8{(NVA(&$2pHKyX#CIS+DN!c%(lJr}p^B{{aD8uis8_!}tr#Xo`6+IvcQFkT&_ z(MLtpJZ}ypb$Mg^+qBdkkgft+Nbg74f8-nLt0Hj*`%Tbsd?@6}c>8#d(!obz7{$6i z622eizL>^@NQEPX4$V|rbpunM#hI(!2NIbkcI=i%m&g0#*Tdb!A1yVm++rfUFHs$j zl%j&L{;@&|X8JXGIX=(@23q08A?@F6F240cWp*`St3(w$;N2@@XMS7g;u((6Q@x8A zhqp{Q@D9kfWMQ1D9d$55iYU=@?6Tp~!g$m~goe1pQqCfb!Y-(qThAW6VS-FD0 z#5WQjhie{7ui4QLwwjnSspd9h4W(?6Q9jXFf2kZXI`$-0RY0yEy#|5b|Mq>k+kb6F zM$+fP+fQM#Gxg&35taZvt;;U(OVESkReWV6*vZ|ibnaPuqep?sPqhiPB zDnQBhr5F&fGy>GcUPd^;g7e?#mpqGZ{0qLo7eX(}5VlN>1QqW;Z9O5K~`uNPSs2uN;PuZ->ofYQKr?c|4FBj z3QbxH*%@1v=p!i{;iZ$)aA6G2Nn*WDlB-y`F%`DhnVyTSEN)U%Yl={Gy>!%?`E_-A znvnx~3qEQ`YL%T^Q>8FRik$QNo%r5Q{-PC9k`<^psYv0L%1wIhr1`d%NL@Tpc+p3# zZK=4)lzueTs=%Ge667PHc)=M(At_(a?-;lf7mqqshh&(n>0JtS8(t%_Za+Vl=|3MGHl=>G(&k5UKv(?af6}xH@ z+O3iQk=h1yzjlqxz(0W&l7)Q%{RCBFhA4nd+PJbDYkbtr7++x%&Hc`dBnEgx`=t)g z9#(B+M@H5pTt7>)lHws-7WrzbCfnS!727_YgL8q@iAQ19;goK|sH3_D)Aw%aJN?i$ z%cHG5Xe}H_-qMHv1?g*Ee-rVZoe7r&^kpJ0WtH zON(y>mG`b0|1ElTj8$dnKQza@unT(8XT z36b_C+{_x+S8&J#LF=Cge1;R?n>DQ|o25BE5*ma`t97}gxu44q)G0ZhZB_+ib{wAg zTntf3f1Jqm%lk^~xXh2VzcU2d9J!M-1LZD^uLVhwE@ z?4qVi)wD9W?#eve!JkSPAw$SLrhf5baO{YKFv#ONlh2`>pUo57qRtYK=O>*}=Gskt zD2C%9<=;2f>t&865`9?q+lj8$h4-MO>-|bgx87|Tb2Si@2Z!#VR7hJh0-3@#8b68t zP5LuOiRoSA8$7Y$n-pZa56GNRL=dIjK^HyK+!44I0d_K!b@_FBLYFJD&F)~*EUPsi zD?t-InKl(pI-B7a+vU%v=HbpE5k~&#!J;hxWV^r^lIe{rp~AhHSnlArY?kMArY-!; zi-7DsYhq@XZl8o)qkvGFM{}@=F(6ezS?<<0Wx;vmh0_NE-stQN!}-J0f7&uA(Zj|u z%}|j=?WaUm&IuG&&9h(Na5W24NGb5@#Ddr3^8SqVxgOc5e2_*LT1Nav zH?DT#%43`>6um20*dAU)=TZCG?M*EwPB>2S-PRjx-7t0(xFO-PSX^1C0z139toIo1 zV5<)yNfil(BunQ;NlV!_e+v47JK(hiO9Kfb_&LNgkV0rWBYsU)8)ccAb%{B1FpI(M z=9FA2iyuJ5bZBFNDX))!T!VYaL#%iU`5diguCtL~8|G1uw~Lr|-cgbGWUj5aMmdQI zQ)O>j<1|?o;J=@N7$`7<7MaCu6&Ps5i>vZ!SNueL@VR;6=_YT<%dID}8EdIhFc{lu ze9Hz?l~uPQsdCE~N{#y6>9x8-kGp37ynbgT=k)$CmGpvJ2M^!(D{pI)34`MvNXAY2 zjR_(WPV<~Lfuxbqvr$RZvFs)y+(8WoRre=?r;Uqk`!+4Z+gJ3!=fOno*K%M^{^m_R zfEBoxhMSYM<=-MjydFU~#nC_S_653tJs}{Ru8~NO&1hwm;Ho#0H_V9z=Yi2=5znBE z;!~8|U2`i`#p562hY`x2v^2UPAlz)!w~J%~Jix_VyMgixEZ!U3(rK8J&t3=LO7_{Z zX|4_eu?My*yxk6c`p>-{&3GEIR|upy(Swco)@BH+lslJ)$&Jdm4k^3R#!A=aJ0hsa zeNL22=JJ2dm@yF}L2e@_bQ#%g{xS_#cOniS`J-7_-!=1AHED3AYoYGG>p>%_8#${F zvg2N}jWYCd!0fyL*>whyA4a^}$hhC??G;uaOO9OR`}4j_wKWC5KSkN2EFwD!;ObO9 z_9&ndk_jQGPbA}z{fZYzfm_OuObTX&LUTIxGMxz*8nL?qLxUHOWC1Ml>RfnTe|tsm#lHBMI;z%I1Igb*3J1KO$m33 zzvd|&#GTF7q=TJL3r5&;{ts_&)mG=aG>uLmKp?^0-CcsayC=B2yIXK~cX!v|?gW?M zZo%CXzK6`U*2~^MAP3{(oKd5?@9ydsuC*&PzaJ>F+t9qqgb}`|(00gQq(5kg8;_;l#(Zw};sMT0 z-DL{%BQ^Vjcq)fmE3t|b5*6h~5VwO=?@}6qvXb(qS#;(C(G>(C^LviEp%q*zO_Waf zpi4jWK88#j#^AD3&2UZZnI2=-neE7C^O^pQ-q8`ejmYp&4jBic+2N!d)Y z3&R?hO4NTgc2(LTRof=R?(wfN@H$H!RloB&g27@m<(_|OppFbBnzN9~)+(0PNDbt7HOf6KQLMB7t3dSB;u}&>MqI8 z#)dV$2=jc7Xk@V8P@ip|Ox_em&<};wuW&?r;LgB=Z_DpTJj*2p1}Y%`NMOX#ZPb$% z5^2^W*XHunc-`t<@pQ?5l>K=b_D5;jHGgj8k}3xjQF>lp#;K_iY?6?72xXY1s_2|n zc)(-ry(#A@@du2lE}uH^sj1X?l6Ht%U@Ty4gmp69(`c7r=AWuO(JV7f{{<1Se%`1X#4cK5C@^7Y_$ITh646ydVZ$A3gJZe; z@@Wp%_Y02u%%%%eg<^{KPd78hCv4H?^mQ8-3&YRa;ReQnP~hAGN2`?YLCe46bx+_* zE{lO9nNgE$^XL;a%|#pFNGG~+q;QWL8{F5XzCfaQOOr`PI0m>|9BXa3vndRPex_28 zCM%nV;Lnr4MdVIGbWcp1@HH+tw&pRCb%{-Whqgk!|ARR6j$x{Md1~26VP;sYttaQY zW_~Qh0;+(O@!Qs>qwM2ywcpKhhQ7F^SS`GMzcK0{Pg0RbgjnV8ab*o$&gGrWd)4o> z%SQVn!t2Y&bCtUhx4s6wfwYxOo~7B{H*;MV%Xl9>m6|z7B#}Du%Ge{u_Zkfyb~fiEhkhnhsJ4fn-H1fYz_RGjkcPy?S4RvKdOH{~=Z$kXAB5A9 zVNcUrb6oIBR@g?i^UK|`0X}9J*l-e32F4+a4){5mE}}^Y!Y2`V#U&0DxWOS9A4%b(r69 zNY5Kd(gIaJV~Pszmp_Q-5ER7o6sIjVAgbz%Tw=aFW!8KnsHQ=FB$P}YeQ+2kDsm{8 zo-R15l}-7gkEN+1qur8--0t0s=@@7A_yJe5+; z7JX?cI-C1?4^33rGfccwnph!})OnmD{`)rSHRJq1`e9WaniCP`nfWuo zB&D32%q(pD;D62om%lxbM#TNz`4L7#ztUW4C3$)Me&G5m$)%Wy zudA~U(?sGg(EsbiUTdk6Nsi4v0I)&;V1H`qAAl*D0^kDFBmgVDiWdI{s@~Sp0Kj?` zlOMjVs{UmX5mFnmoR}Pv4TAzUM2r^yiR<{55y_}%N{Y_ph36Akquww=k*9;Bva;jq z`iy>jX5wBkEk(ru4>~WM>?4h0>R89LXl?~Ny`|4ujt3RI-S&K*q}aJf4kRikU}T7S`lG))?IZtiwR^@jHz;EwLMfi#i~sXw`OlJW?;8Zyy3{E5HMU))la04a-qbh3Sy51@-A@vZYpp z=3CsxA)a@biH8!p_~=A034@NH3j&ADA$yLYuRq3@UqG)37h-EmcWqc05#``uwjW%W ze2i9d^GLiFnM%S6oi7hS^}am#^%YE07BNTS28mJAoV{U7f%AUla(K$V+HlUy_u&P7I!1Cfav7&^RdDTX&{)>z{v^mb*2 zzouOo(;dpBbAn=Flh1T3MQnv_HzkT&z+ET`eO-H@!dfxRP$tgm@3@Ln+YV|jyl`|yWm54D;j zjcwT9zWMA@X$?d6w0j(X^QDDmg-vF39$7K-DTbmu9IFnr~34CInEe;GR5%J0*`4f=j$v>O&sEE*KBMV@4NBDcHQPgzTt|u zN?I%cId{cI56xJFidKiq{ZX%@<{IQH=$VBSZ2mWK^^5l z6>3Gudh9Z3cz6Vnobx$GwEFio6)g_TsPhf)olM>qtONt>Gu&>BB|WASwkrtbOmvSivrH?h<@Q9XsB*(9&nl%kN9 z%k{V_-n2Sv5VJpl%4tQF#F&qR{8$nG&F#@D{a{u8rEz7)^$xRKt2C(&*>hJ$64&qd z8&ii1V4XDQ=t=(Cg?|H><~ZzxX_{3&`B7K7 zw(;zodFDX4NQtnjq4W;cubYsBuflq>VLuQmm-hxp=Jy(ucFho2aM60!IILp>Qz z*}%b$7-9Jn6ttUG3DpT+@N=?K{BTVvmx&eP4lNND0>^Ox=T}MHk8C&3iob z$ja9qYIfAUkbk|iD+ficekP!I@`9@gj+O>@zYDk5cSRJ z^W#$X_8k zt?38}QESaCKSMP*O*`Aq>(}C_4hl>cPN^YxbTM}DmQ^obmZ601Mx>*mZM=2H!&Po8 zk?W?rRr}4*gu9)&jH0fG%%qgrRw~w>jpb3gMso<8>UT}-p+hj~IOd7O$h){z*jc7+ zC69Ejk(u2MG*$)#(gUmtXd1vrf+`@7!hY^BwHESyt4CScpA4*Th}BxU2n*8-<#9H0 zwc8s|g$>}5*g~=!9Z205{aIZKv8tmVB$MJc+nxStX(>4NIG&m~@6(SpCTGt*RCsOn zCmXUNP-rQNNCU$LKBD(V(QuCYZqYR`q%ZT6Wn^=`R^ zQ0bIGQR%nSDn;%66JV^EWE8 zv>CA4UaP=yTeKPX4AYmz3halr8BHsNG)frk{X6Rh?&cr zgvpiNQA5iY9O%YBGC#Szu}|V(kc|{{5}&b^bS0^!ii-TI4oB>kp)4(3Z@wob+>(@PmC5 zO9N#=ycQFuiXrbuxsT57Jfk%Q=cnU-j~tS+`Q4_fYO{OhA7`_0QXw{4KgHW<<$H1W zriW+THIVdAJ?%0bQ=h+*1)cK^H&9~QEsvc#EHFfRj|aWrJ5QX4^Wb}IjBRhuI1$|& z^&@S!vff*Rbm)uuptLW{9BjsTJQ=pKGfcc=yWF=49MIp>uw-@qkS?+<{ccw+INY5g zK3yp~vhFrN$L=3x837m)H zsZ?0-Nlc47YkhAGeeZ?^p@TjInFQ^wid%2}@7!|z0wYW5|0G1E5@A1-Pql3(aJ(Op zp^|M$w5pT{3&lxVUqY_@$Bvw=8?0;|Rj{jTN1M9jw~njN$3wPllF#|07b+%s+NRBk zTb*~Mx>=RdFG3xvM9RG3CK&2#{b?MFa~>U}FvB7tBq-w&5(&zg>=HvX%sT>@fulBD zj`X=2BNf~JUK;`WnJ~^QVGKD^r1a=+l528^6AC#bKd^p)(PFDUObcmv(pMzewtOcs zXRev-j|PlM#5^+8b%>xmML=B$c!ngi1Zm-xKnDxDQQ{Q@DZnf39`9J{`PD@pJLcYzm;cTUSL26-1{`Nb}MzczS8eP_X_Lf%3r{ zc5($%6P_d7phT@X>3d&#l444zEGKQOn$E6^hQJ}$lGj`1Hoj2l)%bT5`@&4;5SMGL z^w>seI{2V8EIH_;ubZ5%1-Lj_x%^OzKNE}Al(SEqHNh0V4%Jriwy*si4I4~cp<#N9 zw!(Oy5a<~3IC~cw@!;-RAM0>PV(Dw?v&fr|?S7R?`3;*msAct`(lua)zkhmW_%ngx z#!-yDki&sqjNO&$Vq^rHfej+1i0cwQHBn8)g5~4WwVj)9Bbb}8#|n%iy#%X2QoZx< zH!JBw0CmQGb^pA&X}t$}u76*00O~|c?SL?F86XqT;;$?P@849h3P51J%m;rAb&dxi zw=sAg*pKkkKyUxh%&}9`R8jkO`NvWjB$rKaINYOc>n}BB zjMJuQ&arB z+7RdYyTm>hYLwqQlLYG@7s($)AaB-b)s|@+K}ts$e5i z)Ez?}Mc}d>S|Stf$QhV0^oARR(xT>rRYzY^W+6&*ly{Cak}Cc2jYIj{R0UH)qT)qkf?uh_%l|M+^DNnUJ|Hzb~Z zsIVx8j6om!4@-J#ZSiuzU1Au?M9|Yyv~%NTk641)83R)~#_0q5F{QIjfN&o9@gS|f zSs{|wB`S}WhY#F8&ORHEcs{Z=uEprpe(NZClXf*7mh*wNL9KgPO*h;tiYUIaw{|n= z+c*v>lWDtKj0O8iMA+|dgm1D{NwQdWJ4K+L4PhREU%PojXz?vd1DKV?gbzm;4)!><@i^i)xzQ?73l^*yZDGZZfich2cj*W;-@1~i2e2x%q zQA^K>Tusxt2Cf6nr#=Zsd}}btbrF*Xm7cr5_V@c?c8dqP=j3D&N9=;#YmD|JqNHP! z=DEow4Cfhy;cKV}_B?{`uY*ITZ?PWO?gC>@%*;yXSmaXs!DbaDub4A6AmFJ!rA+); zXhOzfaYdO)iQ*gS*77IkJUSlIWfe)#grelD)yuI~tx(yu92(~v?X!tD9srL3o_=Mz zQ0D-6!U6E~N80cQPj-fYpM;!=tG%hdsqSB~B}RK&G-LRIByaeD*E2TUC9ov$cPAj0 zve$B`{w4*&VxQ-FEf}%X9gj_pEs(uD2gOa%LQY6RxUIE1TBY$v96`Y_{@O$Y_sX3T$FwB{b^siAkoCQ`|OGjV0@eW-sCZk#yO zG~3&IsIO`;ut$?808@>SDV|tI7-=awynuF;wx~cb@3zkHD^!fKgv6gVDWZ`?SlBSR zu3OojQB$VYvpONN{t2=%TCf_>S>w9#OL_^_^Z5GnkL#l4*E}h{c#H0~z_8acw0>mX zb4XAi3C)+T>y4;lFn|;w(LG1EU>r>5Db@nPL^JqKJJY0}dE>KmBCfh39b>tux+?U0 zlWGJt@?ZoLpgwguRgt?`nA>&C-Jz`C`o+x##<=wQ8(^U_yqgO+m zYV*mAb~*(s$y_D6Taa1^R@_#WiGh}rq1Y1FDkoRxn|}WlLY3s>wBo>PE)l4L|9jC% znF6Kbag$z(WV@G#~9pTf+)!)W{6xPVsL?jlR@@bSs zL+!G ztlrbhn-%MLp9yS~RE}HyFmCc4TYX$l+JVGnJ%V#|91F8Bt~#SRlF+W~`=&{N1_#8i zT-OvVZy7w~Hri^T>ouek@e~jo{faHlA}=B+kH03vHNJD0h)toD>ISNs!oEzG7#Jn? zr!5RDyq{G21a!kH_w~^w4ic5w&)&yrEpD8l1(zC6nHpj-rjjYmm9&CgER0wKJk??F z<9#`XUk@bwrNB4+fJ_hB@+TALQ7ztW}ukE=A0T-4@ zo-T)Mixp5Y*SlnFy!!%m0X;`p25~_E`94f?=H7vYYNTFp!bP9(!`kNym4lY`)t5sNTcW6jYs`kabpl=9hVoU9C|F_7D)F0|o3OS;Cd8(_@Uq>925)&Fp zU@VPQmJ%4m*`D{NSg-~vs7}S(1-ephUn-Hx8W^VL9YbR^drcy$mz^ONN$*6$Sss|( zl!TFJCH<8k0QeDUQ?!TYZLQ|f>qh24SE_+wNL(Xb@uXAFp{ z?(Ep*f*kaICy=kndo}aflQ)4UKiuTR`VTe3Wsqi7f)+-&rev+K=v6|KpSTG1g)t+c zX9J0sq;Y7BSnN! zM?xaE{iEFD17&U9jBX#(U2?KswZ7^UrZ@-_S$fiNVvCEFcewC*g-rH0n)t%|@k2f=IxpWR5XNI+`v{__GDm`}8B`3oLb5z5 z5p*>cBwT7V$Y$D`u=G-r1o@3{-V|dCCE;0`YLZo5MIA27pp=X1b@a1t|S3JZj{C>6@0 zJ!4F_74$T7VafG#(IrAGmT?_UyAU19B}rmZ?ixrUZb?rprePG_0w2Q}4OO7mSQI|C zHll+FE>vSlEWpMPE@iaU|6DC&;a1=(`=yi)ndH~)TRPJVR_tsVBr}tqOG%WOK!Iim zA7`3=NS2%d7eL!D=+n>#rg_INn6m=<-M0pXrooHZUp#~Y)$wj=6>f_vg}@#2v4zcTNR74YELWGQ5Bh?BQBh=LSk@vB)`~msgM_xvMG*Vhi~0EUbENL5 z9CEDgqzrRTQ~_N!qjG?*cp;65^@(l^%yu5D>T@ZjX8+34U5*+)CckYx?L2`q!R3y< zCW|Z|tFKStL5eRz$E@Off$>iSi|CGW`WGEN@iC{9!*3uT&LI5JC8&>F+BH9u7b+Gq zgr_iKl=k{tTOS{$OK*B~{Ngk;flK0#1!>&G=8}DuUJ{=Cry&M66!*)P1ffI>gt+S5va{H-&2k^w(O~bY zwUM&3^YwwIY?ce2V{m<@+o(}W)%D!!^slYVHG~t69OJx$<;lj}gYH)N5xqvuar~|^ zD_7|)@A&@tDI=?oa~Uo;9HG%1V!!0o$bL$z9y2rl5}IoDEFysyW&BtQLyJVgy7XDq z!bGZN$z)l<;YX-vedvz!YK>)pL;i*vK>%aKRi8M^(sJJ&5^o)v8v>qxY*Nu~E_Ei2 zkfzxz{WTG6;6XZJqjfHrz~hLRY-J8T^VkX*4}|bRQ_@__dzlsfu8-y=QarLmb}<%0 zQ0_k44wXI9s$51ZQ?AAGOKi_r^$lGGF<{`m99St2oe^z80jGP@2Y6ckJ~FyhU|OFQ zPt!TOhA}@W@|Lw>O~23G=P)z2w^%~w-b)zn0``JzRSmCB$1SC!|7Wg94kx7)<_H5JymQo@<(yNQgRQjg{7S*}XxC__ zTduA$=|wTZhdQn5&L?4BhgN5HP#Z=ppmsTT>ujRfGUHqlgnk#c1;eu3TU4nV;} zyd(|WcH7y~x?N+Cst-&^u`?@cQ%{_a<;9oL4%SG-@Y(YFIlb*FD}U?0Ry2xF-@L-+ z?SHT@d;az~3I(VEFEs>c3qYk+0wDXx5ApSR0=OlZ0-yz~?*B${6@LTPfLPgA(RKeB z#4`@CDW}tHx@1~I5QIamKEJ&km2YKz zk<7NSh8;QzK1v|8jkb-o{s3|H4etVCL8T|P-TcBY%~Nx1J}d=$c+kk!HQ8(U((xjv zGLw*JIe%PO=;5hR4A32W-rH|jcLF2xal zXQHmQ$_|)n5B!LQ+i^4-A!It3n|OOo%kq$Wf#wP21-Cuat>7IF_24ILVd8kIdeWG{ z-2C4KnMm>ki^s80#MyVA-NUtae#v1pogAVF-x&t}qs$wv^$WfSN1NVBzD z@~;UeV)q^MO*3@_cbd-1zHU@AzP=tlP9~%s-JlSk*Ce9 zN#Q)wnmV1->(?||Qzy&L4&wD@0htjum(BatH`D(h_#SvPx@gf>`v=WkYMX}vv?eXU zdVgW)YR{R)t+4ge*9?}q-zRnm@Z98$*-Q)^lp{g>!W-kfJWv0$Rx2MmYmSYX9wk7- zIr-ffue{5`!4N}k;v5WSrMa4B~&OvNea0$cUb|ojzO8aDWgxZfus5@_Q%VvT4QfGPaVohl^0}56pEfjw=Wdw@Tjeh zxi{IvU-8j`GxTn|IEMqbA@1|)ZRF}leERrJw_QI_K&6jAwdL8}kZqFolqCQjo?PnQ)rdT~rX6<~N6nK1B_ol`KHOL!}8`s(=3ppW=EQ zW;hbU9_bTY37d0>myVNj{iX!5YeATiEqhjmH|#ZhT3bq#bd8sjSM4ul)q0-ixPN+< zIUPLRhc0+ipWbxOuN#pBYj#XCfH_}-Z2#4#y)vhR9iZgK2Xgad$^H&}5#PYq@xMoP zmvBMoDkcc^3Iz_736zvMPXE+kdGuLD&^1g5Jv?RNKm0(d?_`= z!kGWHNTr-a<}t}lotbC@SU>I<2KSAyIOA)n;gK%z43IbPvW04EOF1_Ph z3K`wuRX5dtdK(jy1cqIvE23PElLx|XmbZvmuuDoJiLRJd$^q|mG3B2!aeP5 zwbq-qnKYi;x+tno-$z-!vzBvkjZ=L`Wv5lWky#kG_{*bEO|yI-(C6^Zgp>rb&MAKG zFFjOpye!yq<4{F?=krkrJfKFJs|zY|gSqK=>hcGR@xYZbUL7Qt9I9_aO{#VY_hY*Z z2|`KZE~O~bm$VlgIR2DK@Q`k`iO(~~C>0;{>Z8Z|i@gS07Afoe8>y;TDyWEW5h_Z( zM#qZ8m$dMN!rc?s?X|s#_xz&|tE3HXi)u#+EQwt0JyXWU6J$s#l~&u`VbkRqpTK$$ zugjZc8LlH>7KLyya%H^MO%}%wr`)RCd!9^+a=je21m&LAk9jYD7e9z2rEb*Nra(+z zv>CLSyKi7e7>%h#sGC!$#Es0^H-_1i4b6{+-puw#*!kvRwF-;c44eDHTTVq)DIa}p zt&W$YEZaSoG3B!7ss8=OzvUfJ3=_2lE64%x+`R(mKY9%yuoa*XU7!jApdywEoMuEaiH5FCoR{GxIsi z;ZOKyT9Rem!EvU_4sw>p-E6g6l^nf!@&q~yhx&cHbke)Rc-s+X6L)^wu_3q+915aU z;g5pJ>?E`mr#0;4MFOd9c10W*l$9X=)T0f4jcGnZzhYMXd|)ZWZ-i`PBP9$|Veo(k zdpywjZdQ8k9CFRU^UA8abD+!yDhcqZLDZ{Ihd&@#KXJ)Yi?d2fr?*A zrF($yG6I;^{&(R2-|zEPm*Mc2atPnP0W0GEIHUvuZ8e@2evn2R@`;_D3{^`bgfGr~ zF6Sr%j_G6&!7$6K`jlZ3w+)xE>nlx+hd58Zwbl7SMtA!`E;PbKT#Y~ya8kPh`|XL- z14N6&RR9J5{J;$>pw_j~m4ek`KkA2Fzy4Jq>!pv)XpnyML|vyBgY5$$Lcau&<_LgYJqsdTxmw(OdK3C zG_@-liYWUkhje&_CAr_r&*InKRQh+BGpn33CZvWF*tx|4Me8ydD!d%;a(AMy(fpsq z0+)h+Dk!J3V=?4V-fukC^<^_FBjATfK9cyROEM~&`9d;=mS{97x);U80izjjYol+zH(C+2eJjRxx!@J+kK$RQp@H} zZR{;Ha%D3q9Nm+6R3pVxnJO4)Cs)@d?{d=C+wGXtDe=(dYEG?DlX};?2wfQ>`3u%1 zd7*uQ!OdS3S(uGQpMetxHnZs(gIS{ongC5I=%#%4vqT`lCV4;|PSHi=bkKd{{0`*$ zGZRZxg{DdjBqT}&ktEm+u1OI!-7h$~kl$iFGg-eSR0`BvzvaT#Jp^)@{glKJcJRhM?l+kt7D>G1)uD4#QP0rWbS~BoPH8( z`~L9YnZa(q0A zj>Rz3)m|Fen9((Seqpnz8k)KrwwZM)wdYUa%7eXIR!zhT8py(gPoIVDHHRVvi78CA zY{SQ`2lIw0)9`V^9G$fIU9h4e3DX1(9#6EF7+m)Vv7$18$R|UD{1D{)D{#k{kBBAqA$$t;6g5nboZCpNu0#Ksk+sOIO}j9O!a=&-tGj$pG((IA6=lY1 zGrzkB_SK99UanD`&8YP^-dv5bH`u-mhvi(W z77mm8Hdk~a_gO}DW$6a;#~DpHZrt2UVx$~n78UfYA|KFFSy!)&pNK|MmS%^T7i>8c zG(W`!K(dAD>9bL|S?M?#wNILpQj4Gz{l0Lms4n|B74v{|+$|(JLF49meqbVB;%iu5 zV$sB^_mqz2I6k>3|V=x0q<2H^zp<0D9KUcV1ENa25%5l04PdTnL#n#*0u}kq^9dT;aJxKjvmwXseP@I<|W}icwiWHYEIxJb8`gs5g z&CYz@r9B2UFru;V7^_gbBF-%)X=hGsa_`v^L;Ff+7^D|lDOBX+>DH)hR4tzC2lv3~ z%0p}|d1aR;EbRy0d((UPmZ3N6U>AREQ726lz@=x>s)$YAWEzU`GEK`k(6bZ0Rr)@k zg5TMcIZhJZuA_u2j-i<1n#}$V&pq5ntNHqK9hRs0?H-H~^+2*r5}j?JFE?|Pj+`7nsI?t zjC!^AYF?SjF9F(ZT_5a(ni6}RZrZ6MKlc_c?}1;bHFyP>rj8e8 z(O*EEgvK8I@ki0dN=7GSjjTgtL2xjQd#%2xvlFnzT~$z@J`ziGVb$>QN8aXUz4z5E zT$x1^WTHNtHH#yXf3AM&=^+t|x!$QDxMq)Ntx;KK_HH zwPuDQS5iUL6WdhUGW6L2iRWQmp(9?6NX(qVHSur5OmL*T!;o95Vtdu@vyuT{*kayS zy(6?NR86N|YqM6XMG4$}Bca&^-~}pJFVhCV%NKyxe_lwY0H6RUxdI#nG&*(wtdyyFxQe*j^0gv%z8=G&DV}mWjtv`qlU~+$xM#zfi$}~-RalC@8IxG}5Zm;;8y$7s6)WR>abq0EtgA+?O;8`kf`^#-JtpkR7-#R} zI5_{~p1+t|gNjTwT9E|SWioPwpr|NUl?;ohla8{;NZ$nYw%ou4^w6B>*A>Mbj;uZ= z)Fs2kx&)f>@j4Elq;N0_`=aF77$XI$Pbw>WjZJ8aXFNRy2JVnTIDSlF8psq_4eSXZ z&pIarS1C(fO<)~zZUUXKbhEGpSDFwAS2wBuj58JG`t)wS>8KHbnsjgKilzJ7oKcZJ;4X1sLHA;tyM}D)z!;8rbkWvYFYr zK^=M#J2ft8hLL>RsZkb*jWJ1k>$yZ^CN;KDQn8xMr?#dY<92RC=%aVIB4`-g?%r2@ zG?PNKJN*`sR+u7T%mch0mS!DXlJsgAs^Guv>?+12bK&2sD%n+h@hv9D@^IW-h&SUn ziDNSB4eivS%b_8mZ&J>en2uL1Mzf)<8`clNe!g^IbX*U z55{X<)V&DJfx(8NuUHBkglx~VP07biZ=;t`ExUUNu-JfP^?#>Xk1-Segk!Q%5Ftb+ z!VsfzKo-@r{x*sA3}C2q*v4ia06CO^lHxz_F+imvWNH8im4Pr#Cqtk~as@ti)>eN7 zH=u8D)1dV4PDB0D8@&ONmf-W~AVK<4u7aFIj2eoq#<6Kz^;(G&E7b41HjF{bcaXHZ z@QaJCOjm1Zs(3h4!m*j-D}oozle?3mXsEaO6O2XMar$oJ4$H%-Yfq@rB&{&pI$d<) zJ1rb-s@R}GNz47b(Lv4*gA_~@Zp|+0%7NA}c(@n+=R11l-rkQ*&|sg$8*RTRYjZ~G zXARth_YwBdCPbc+$WNRd9$IJ)iE?aDgP%5e$CSinnjSHR+UxQCD8>zl71LHm8=C!+ zkuC4gZ|#Ax=jbTCpBe55^;# zUH$Zm;6M5fIR>;ip7rL}xPFPKq6VwcmScTPr!PzyQ~cy>YHoiXLP+R%V~d={`J|Tp z$?dzK>N|_Lar7eSfG3RmNrgT6S+G&TuUlj#iOZ6CWwOmN$f8fjE3`c(aS-A3dvKlM z>6Dz#y4%w)$){LYS2Ah%@n1l$f2DZhdf9?etb%Ybx3oq6e&yHFl3IUEehGJS83g8zhg<^}Y9>ec z$m6BB_g0DcI_N$^Yejoy$=5jaKiM@(07;6NTAA9L7#a`&00;C#K(lA6YXX4(@0be0 zyn%WEa0~*J<94D8Jgq*T4)<{#^Weq8LMr2zQ4Nm_c z`*{p94YDaXNwK6<=(lF!+)YTzJvj$q+BkZj`Ba8@A^R@tME#(tq5u4EE5sEX9m+qW!idX6cjA@gXS9MY(~^?J%0_uy?~?F9oSt`I4-feF*IW&Z)%62+7@m#+ ztX)eY<>+N#@&bD0z4(js+erNciTw_rvLs5`jvhMWdhcq7ydvsMAHJ&Rr#Drv=2N4( zDV{)iZg5-PplZl}y;we$`rUT`W=Rcq;gEQ4%#29IW-HDBF4HMu3=B; zHY-1RWLQ%Vv&(^FmC>H%@x75u-t9Ith1^Zuk@8?XrJ_8g9)o6Nm8#kuw7X^@~G9W@KQIF%2*=~H{P0#|nhu>+CZ7cg>A z@W;&9@nKU2EPbb&?54l`x?A{3El`c$L|pmn`S5P5jH=1-aC6|}V$sK-f+*7C5g5D! z#L6n|Ui+`^zTB!!{6nz1c)>|6OW4j>C6YFsPhh^CL$=@C6_v9V?(0#{f=^HbwGkar z=#ylb({b3EH#U%@cCb8dwLgMQ!^5L@X1MbT2)6T;#9K$CQxnX9w+%8E#_}j2!y?f* zcnLf>YfopGEeZ^p#IFw0c7t?7L8*jFbZ%DJS>SGfsFJ$L3xzQlE6Uu<3Rxc=iou%* zl(|}_g!muU;Zt+)#gq)NQbN#9(9+Lg3opfuzfvp0v0V_lnT67>^Z{}QHI2Ga?buM;z<2H>2Fv)JFikpWOou;P1kxqp zlHU&#;gQM*6!MN>Eom8^>~h=&8WE~4Rc1rn-l(;F4-Ny_GUC;Ro5hbG1N%8sO88yt zub&R84$WTTw->`RM)mf!wRUU)*BcQY>-ddKHQXtC%(^9?HgHRa4*Muu0(n=havohx zighVUSM?^c^_>rz(TloVOcU>+Y*B7-{%^uc56~Im$#d_k)GrE{z{CF;d079!8t@GL zE21g)H{1nS4O+jR|0|Dp+oVaxJmv^&!pqpa7pBy}`~=z8J?z4iCp8db%p&>1eS2I8 zgHPB;fHg7he(TcPbGZRoWm_zN#-O-zI-%Z@tTlb1lAm_aPih(gCfO(p<4ng`)X= zL4prDbM9$Dxc))7RkgIr1L$f;^bgeHNF5qI@YJyy>?IG+@7XBTan>^unL7NF(Iq&1 z5rnh>kaFdU%T-ohZ_o7UXm!a7+rYb63*|&5cpu&;Gd?ViC(`k zq_01Nacl9Zk12mlcxA1#da4<4#oJ)ra(W6 z!dGNLlqG{ohP1*xd!%2z8h9MSkytm;b^O#%J=S+~H;S`3wz^oF-+qwDB4NW7y&-o> zZ2R@MTO`S!e*LvUvC&yqB`G|ULxtzjahiiilBZ@Y`EDF|HER zFGNE4 z{nsv?>|!3i8KGh?d#CU|wLtproY@T}Jz0fROPC)wqR-SE6+X>w`g9>t{ejS?@LgIn z%ID&~o2_#VxMeIVdFbFBCjpBje?!jmdoJ*|oxUElE3YiO&FT6?Rv0x&qs!&evyv5d z_JyNya!DU`RDR;jmSY4>40))0+7mR93QrT!F!KM*<6v~dji{zgQB{qI77-B<&}!G!*?YS9h1k+>vlg;`r>mYJ`QxVkNB55mZFnt1Vr_g& zpKg{zb*B71W8mnvmoI&p*HeGn-}#VXkI*e+^Q*(|4oSb{pHMygdAs;-I9JEnEyjHX z;b2Qu{8dh6>se4Th7RliHrA z`TmuDR~CrBmZ$YL2lE|WIMgX}O`JT3KWe>|fV4S=nX-v$)9#@P!S`HYaZV`=|pJAsP zTb3Ee_TneMRg8yLAmxV<;a0a$xpyuL>8GhngF2J-?n>M$YwdD-eqZg~VfXe+%@%P7 z#}9ewQVWUh3VB^3{7|4z%WvjTk}PeTPk?T{V@efw_3o(6MeO$yzmnemmA@Z%L>D`# zLV#`}$s4%jlk+E#gon13pn|R9?&@KO)d*XAj2)zPb!{EsrN958v}BE=8aK}==@6s5 zZsc!NTFR>`Ah8wA)7BAwsFRoV(y3;LrbJTr&7od7Gu17&)RUv!$y4^M4zdpgyzkcs zdt8*X7@>(56^-G|IvlX4iT;bS1y|##!2UkF$at9g|+2eZyr8*3P<&W07F*XMk?&91-qkZ{m@a2x) zOCNCB)+AsNcJ3BlcNz9$8!Q60gYn;M=cX8Ec*^X*5&Ur+!Fxal<2y0B9?DN_B40Yx z^b+$QW$U*m28m@fD9gSaNPeKrLy{DGr;DZBY)(zmO$+L-=?3tos7(v$R>Kf@FwZBv2RiQQ+PjvK5WF;W(*5{!GV)bUk7wRC;kk)JoIc@B#Yrr8|oWZza~HF4X{ zJlRRSoqfA!#!vMPQqQAvlBM(tWo|58^VW`T6{O|jy8FQ6m72C;y?>Tsa6e~=RDsVA z?=sPclUm`=!)5kPe!MBt@07rJZIoyJ`nShSwG?+lZdPS{h>w5AF-|4q7qIAA^5AWyYo%@ zsmfVR@6xwQ$29hA<+4_@d&sA~OWfYuO#G~J<<#cLS}QHxOJ_EUlO%IhR@lAVq)yNO zsGWuTikalo0Gmb8ZO$B5jFQ;@Zf-58OLmCUG!LrRPdzo_WF_h|i!+@8 zJ%G3O%?W1I0UH4NgPn+l^v4yO>~w_Y6MsKQ2%#D727&*{4d)OXJvwTnXD|+9XdwF9 zcAZQl?>@0Yj1Li)A{Q%jLEnUAW^>~lXQ)c&@n_OXN4@&#TI8}WC0yOv!}Wfktv>VF z>Du>FH6_eWzG1(!{32h_*r1<8sLfOILEFloEYhJ3ak)a$ z{m+_a++{h!%A?FftLMF++K%f|aW%Xpa@%ZmTZNnPWve53o&RZ@4%w}XKj%70;u~91 zOFA7h2PvpC#$+-E$|o$t%x9csziGUcPhdJTQ+ICQ^{0_~i4MXkR-f$S z^}PJwwD-4pel{d&wM?^1jjroViT^)J2v!+5giE~$*sG0dux1?f99Jh+bg|RQaWdA8^O|^J;a%y@1dX=W59gd~0WAXcj`%8Z_*9Ai- z0hEAK4+=|ONlUl;ZDM1lox$&KDQ`GNn{@bvnx$lm=8=bvg&xved?VQdQJ+G$94tB6 z9PuRkN-XWgjo-_Jj>(QMJRV7jIh#W#`SuaNaMYlXk2%BfCcZ8aLoI_oMY|Rwa|+>5 z(KEFRD!s}d>Q1{q;Oord9x19bm#GatQo(X5GppphR)(om9QVez=hA$_702z(sx04* zm1U|b_*6cTmUa^5xJ(&2d{{*K>L5A1lg;&szgK`G+a>h4WM6F83yB}U(ra(~_1e7r zRdvn!D&>I#5x2>bc{jVin+=j{8`n0iV%|!W5dC$Aqe{F-M{4*q$>wjOJ;qv$F%Q0* zk)Hcm*DQOx*x)heNb)@0-et0fV>SBd*f1pq$d=f~Bm|WXHH_FCXT63Bk|q)->|=v$8#*@rbV8yYQh}#b!Ev zq55ucocVp~Mssl~nqxE7O^iL>Hy`sYs_!;3FlNY{ii&TX8w^_9+XzpbbH(0y4g;?A zEcrYZQ=1tpSQIy=KPnT9BTF#T&2{oCa2;^zcJ`vPia#etD^Pm(y|bh9(~>RpyM+o* z66t3>YFON!f9&_={_|I9UYK>1$~9$X1-M+g{+dZ8Cra%%_hS#9XF^xXn+9???(1b0 ziI`#(ieuOQWccs+iyMFsWwLTB2=rh_IX#Zyq}Y?)H#DYvNPK>i#GKM zYf17p7LZa%TaEO|DAPvGWea^M{NiqZ+iB;X+uV5ISarNBo1>Fw$yIRzWe z1SFrT9P}yb+$s8CgWJm=)j^`8RlF}Oo?+aUcKp+$Nzfx*L8(}F(4prLXkZ6?f*wKf z4Gtw8@GY@zZw4^r)6)<6od1PyxenI`ndqP^Zg=6e!oEa!tuSuVTg!m5s;YsNB{4@~ zvz(X|%a&xdQ^rRYdlQmg+FDT8S`4TRwM<?}QkCYoM1P0i!phuFph1Y0bL?myo7O5s67hRW553x0Dg^I=}D z`^{_J6D~3r=zg*_2-2mUn4>$MlcwIuAaCn-)Q4oAmGu1Sf}pXn`A5PH;>ue!J;P5{ zT;*xkd%Ekw<#|i_#+|NQ)slmczaJIx91RXXw4{4Nd%Wf?%?*;i`v+si4wMb}UO&Nd zW3JwIM)Ru6z6e&Imr9r@YIKw4>3Xg8YQ4rMnGX6At+J7nBE1Vg-n@J2ks>D3<|QAP zLZaZ%Qk26uO8HLTG?l8OkxVCGj{d|sIs}EyFJ_@fxNb@1G^gbzW#rq#Axi= zA2a9=bN*otR%9>9cWT~wH@Cs6lK9Tk62hG?O(OjXETONiMD`|{9OTSE(?@!WA#Y4A!UMiLST%n+DF^Q5y z)%Ly|q2C8A7dmBPI*clObzCgX+{t(<4a-}Z+>69-i`3P|aB8|IUxD7Tc6A0RabZHtiqPd@XaQCSHK1suI@qE@Z8Qa82scEFQ zQR~Q+#D5m)KFI!S{;n|1^iYFN;O$wb4;Uv>;yP)oL9LrAqr|86JWS75@-|l`yo-~& zNbEamM*p$vsO^E7d4Xet)9!B5t}oIZSjK|VosRRp*T|TluPq?o8!ImmvJ|J7K~!s2 zN>gOoF_3%0se-M3@3+w>VzeV9sbO`Fdt7JEYKuP;>3wG8HqB&2_Z=QhSF;{lE1oi1@zmW8S%_L@cO`JQr?eRGcNLtE)Nfyso!^ zF5D{Rt;M~QkG{Mw6Lp%s6?aFu(?T?hw6)%*FtU+HbAdWnz)7c(&Lh*Bg!*3l^FUv^ zlqv;nUz%DeKN{0_((7)2Ihz@0{Ugd|R<3#{#fE1s>NfP{dwPsW6qF>lHB5_GZ4kVm zzU%HhrF86ltv+6ArI$@tM-&5R&)pQ~9PoSB;zv1#C>$CED!MISaCjx6D}H zvTmT6)Se<;%q@Mzlx7zYM;syCZy)fs-g=S!jAab%2YXi0e4`@@sd2-jjfY!zE77-p zHQslg^RRHIZun_${{vmWKJ5xDYVXynvj5p#AWEcAQ)>05#4RNtx=i$I#cbN+xknF| zx^wEt(}P(*9=l&5=y*btFSvd?(^+!`gSrUngHej-1c}`pF88_jWn57clwHbF(+ZD0 zGSPg|OV1tm{^L9xQU}M*`Ak7Jul7%3*W3}b3=Qgmj*$Px!=lDfugYX4H!wMoQ{hYf z=dEwrRVoY_f4VZn{#nT=#Cp($RKl{`{lLtW+zx7)H)=++wfSqx4w$FLse+%aVc+q z95usFI#ZHv&Op7V8u`Sz58G;*#^XOe8(42)#qmse_nV!2^loaCe`u6TI;xdC{YhI7 z<33iJMVfTJ-`Ji>$NZ3CH4&we8tsBoT-RJ!oC|Yazs?2G^4nzzO%Ep)*yH@?EFRx0 zXKJuKxW}2f=@5B$@~+-ejU6*fau#(HjQ<=46oH4@$i}z8mS=%2kDWnc?g-(xEq1Ow zbTLAo#eXz5GXQ5rB9m8MbX@c$7zSPOK<0^ z)Q*YBZ*dI7MlMgN%q9+GDl)e3dE6LAf5lOf!bbI-<^{Xje5#W~+5HX={Wwlv4bN&w zei0jE-sDE|i@RxvA}eucW3BE{b90@#wmSo9f>)GHcsBFMY!rXDsll5)?rg(Y?_j~x zi3|*eXIo5^!!C`ex(~k&PCPG4U>9ZB-4KyLTesKlT3MW6gmlWNWyn<4%e|v)F`40s z@2Ld4=#Ei8Ifc#^b;~`DyF@R{NMt?{?`Bu_$XH4-=*Lih@h$fb zp(qgAYkTPJOrE*2%%J9zobvA(X&K^KlQmKanvP9jvmqoQxgYF{3a8%E4pU7m8O!RK zDL7ny9G*0QGsWQ>?0ViGRL(u1Y3RMseyTIg@f_6(hJs&3x>I)P5j|i`@)BGtx_t#gzzT{QhfiB6np8r>4HOWpp*2) z?#DWdJatje-h9kvG8nV5e?f;@%(wN20wW##g)$$4J zgHW5dN@~7xi(`TKQ2t~NZ@btAE2f0J5_0Mzr@Uvx&Xd*-IUh5~IuSDbg#RYp@o zt>Jr*(ZO%lcNf0}k*i+e^f>jR=W>kfD^10*%Mwx_MH&Pwu9ulxJdeEd=;@d96%-Ej zcM^Y;GAMiuD^iyE{^MfCp`SQON7Q|LbDBx+5-4ITxWCJF45WxEF0L?n3HlQu3H_gV zy|_)c%XJKz_b{u~<_bq#(tWKcWfr2&eQxZoa?;W8hAr(SHBLJIFP{p2|M*~}emI*& z)u=9Tsy?)HZui}M>4CWPvcWU$-kX~PhvV6%Hx|C)wI-5tlj!RD#(H+zh2{1B^3U7( zAAcFPvLoD{7DMWxxNST{@^WC;cQDgSmnbhqOSDR$28wB>uK{v zy<=NUIqUA4?ADVF48D3$BS_;6Wdq32y{Bw#j{ODH)- zn`GLtJ6baTCi!<@{s8UaezosB3uull9GKftCyzPydYcn^FbM0%m}21Jg#RMIX2P{z zCV%JXyZ4&UEoW;o%njPqK_91TP$nWM{O?MbB#%{S74GpJ+b;mvO9)b@9dJ3 zV?JnoU$}+Tv!q?b(PCb2A3S@u$nvazW}Iopp(6_JZz4nHYl1xZ?`N@Frep}W96gce z))z&0s9wazt}VAm*d&gV(<)qHeu@5r$=KGEs&{Y5YejMvtVx#6JI-t{co5TPta$9{ z-UJQe2izo@n9n(5!iVEUq9^T66yA4yUReL?#Q3y}cxUZ8j}+qjhwrzHOo+-$#3abp0j{ z4Py3y&7CGS$8O79rux{E^SEj(j7i*=m+8!M}vn}x4C z@WJ>)1swzaA6#Vlugu^k94$Vdyw{)!u4pc3|Dw9(_sKYX@pc6*`}*@Vn@#-!#&re< z785_y%)Rr~SIQ+e+LqMQdM{PJzV_m!{tvz}R&!hQqJw%;^}8)+AMA3LzE|^rzeK?F zLF}b@YZCcsUfV4tLb}_ins1et9yS(oe&4r9i5Vj9x6ZiMJ?GWYH%IMB>q(T;XHu8l zv(wAbYPjH9ZgO%avxh0YrjDN7OE<5xmAjR~M3~%+@5;Q$N_i?6r+5C$$i(r3m6>;M z6tOg^`DjsX>J(t#w&2;rkm0uH^Vs(rzlR4W#r#=D8#(H@iRJQQ59-n^+HzPj+`#hI@=$DBmswD^U& z`-iMZ16tny-20+vhi55&5zExenc~qOJ4(OEuUDIS`rs(F$-(dmoPnxHKisd!g;aum zpsEV@vVn8-WY=Oh@%IUET)x=!S6TkEXZ3KkB>tQdh8A^Oz&L94wv&&OZgiMPv%Bn% zrQ{+Plu8=B!}*Ou`;7{zC`H94W@H7~cr7~!v8F|82?f4m6n zelgchSzAMwKvd*iaWaXNohpa?FS=Ue!AO(oU;k(aB$13t!3HC3;3F*YmCK%)9;@9nQLK~)d zxTf9I*&dOhExYsgC7(Jqp>%U-z`H)Ca-m6{{HNKd^0>tYtKp!XXXCd#y3fpEbE<XuiX#dGEuTgkNNpi2DBt{WzAG5fbzX6o@iqvTtx zFMK?JIq)XVT>GSVRDFJ%cbh@%Jx9kQv;LDGiOw>ARV}SjAEL>eyX8K|BYB}<1C@GN za&kKFd&?{{EqGO<& zQ+ayB442Z<-QHI1vAlinB6EVMk0roW@%K)yE+Yph)kZztlj7cu>humJDcT%*N%GvJ zJ*L9xyMPP~Hr?S`EyP&J_Kx1TB>2s58U7_}k$|0dhFtf0x&*bmeXMJnk$U{l>ePVp z3uT{d(ScUS{HsmDP6oSugwMN@izk%{o8=^FsFH0DdcRBSS@-v2-!Hx6es-szfm}P( zY7Z_DEf325?} z4kneVE<8tJxG8 zH1v90xSw4-yl%VEx|#mov|BSWzT3Y4%H}$lzp*UJc!%@}fow`|Y7te^P_|e%tCN2}j=Cx+8exs)Tv3 zj$%wg-i`Szs|50-y z?%a!dlZGQOA-jIZPikVtuVq@A?}qv=ChgO@@4=x*tNlr-%1PMvX7dvl`=1NxmNLy( z=(PD2ip&HR>Vv1^=Z7{Ziu-#FcX4Z<%=Ab-m_dbERBrJK2%*wgm#}_U$_XzG{)U_P zD|ILo!VWc2x3LDxXM|RyTF>= z8mGirGdnNK>Y_^`H2P%k*{G{GF1+8q$xtrrX|#f-p&iXwIp)42HFrR@)&5D{jxATs zYAX~J@-EAK4IQQ&XK%c`I!m+5nyf0+%zx~wVY|c@ zO#9<_ZM&2#CPwZX#F4x1zO8z%#{Qh&G39pFCZ_!k8YRff}C z7$z%i0d;RX3!g0uuirrCKuQmWGW&oxSJI?^C zVvsLW0tM4>gtj>h>X!;|{7(uq_Bbcy(LdvN*Uzw63?=Gx<^1r~(jPw|EEqePn2nkf z#O>YAbDv7`2)cYMK|9WauSCZ#W@s=Wp{6G1*>>iLYHW?6%^XAEq z<&EVv{>JqFd1OY}`vfX_vOA0BoG~3Ya}_P`2z_oAkTy-pc%yP}%k~~|jY}e%`^kjEkG!k~RIPa|Q zu9*$b4HW11%f2Ia((4$x!FBB#h07i$@V z_QY$$sgLDy?vy>VF_`~a_f79@u`;vyv{Ek$qTgX5Y zSy=S)Xh0LuWDSOu&;TkonlS1B<_)4*E&oN{Lj*?yZzk!Xa`EXYuAfnRzTVHf5Y4KI z2@!*tEHv`ls51k4iYKGe%`}(lUy??Xa`AjEo1NC@dG_R~LpT`~)xuc0)c*J)Z=3Fl z@}P|r`Q=Q-Cq|Oe61Onq#%@fIWLAAFxpn`={UOF9sr`M4w-0{U@)3Hmrx}VU-&J3Z z{bs?*!ey2E=u8|nYYLyg)|(qVuIVVgKyH*5mdc zE_y8e-tVGuSCQJ=`_*yV;akIrHRI2nR6dKln#uTI&hhBunY=boJQo;tcA(x@cLC=T zEhVhi=LhX_r(wO=VZE^Lp_#k5qSqQ=B9kY(i6rORPOlis4@6d$I&-oYInt9XUG+8KQ1CtrU# zQRE(e>VZGy|8Lq++u5{z}b z8!NKoOh!mszOWmYy_RhC`_Y*ec8Re}_H49>OoVae!i}OYvbUFXd}ouKoc$Yj?uz8w z!K@p=#s4Fl|Lu?5tJy!fc=OWL4`z#P4u4W&XU9X^@c3e&wxiy|(~TKdQydlET=U4P zY;?WaTFuI6!`v@{t6ISh@Q!{F=6>>wCW`PGqkQOqh&% zepDC6IED)DW9)VpJm4wpMj!X}dw0;o9^**;^Y#yZM#k`u+~3bSd5J9a{(D)g=PG-h zqfD84$||y4N7j=iyR`P|bUm&2(#&G3rsCn2^OR`XCl@Jg9JDtiFmS;^SvfuKZq8&-pr$&>K<9pKX8x%dd zM`EvZ=2w`X_30!2b)sAJgl(nGjt81PwrRQ0vi-}3y!DJ@x81|8luf)V^<38_O6%TM ztHFJE@k21HLEKM=7RH@pwrx6EL?*Z|hHeI1we4lg2Q;IS1k@0F@6g-@byRlO#Wrh=_uVY4Gz>o8l))o*x&A)w{kxnxL8`=|*G-x8GUbP9cDxJQ zCduZVVqGsBy2}Og;VRczsRA7@dS&|p!yE?vB+`wER}IO-o9OKsy(K?rZz7^&$@_ge zuTO|L(y(ow06mOmtX8Fzq$__E=9pvN5}`Y=gVn z2ghrL&SgXXGIpQimAs9Y@;VMUSX~f0>nLF>8hb`SDJ`)<*nN6p>B?juy^FgrHHn^q zHmdp@Z#u3p0R=(q^aLi{-!3gd>@g^eM)Y3q_MUOS(F1G3*8TgO%9jN zIgl;=$V#Cz-Z+`4#&&ICvcC4E-Fndr2i`PN*AS0Wu`A1g!MzZbDYU59K=;+PL_FXCe`qg zZdb7GRZ1IEc6dRh)*bP{n6LN2?bt&zhu_=$IF#4wYai@l*>U=+`bY_pM4Hk~Qok#t zSLRZvMc!w(FSeCC2WjTIl2h`h%UU)iclD9ys&(3%N^UVY`Cem=ltg3cz&+<-+8k%r z3+mQ%2r}IDzoX$-@*huF~ zQg1AJ-{7VHsa(ewLPK4e?m;yAL_+KK*Iv2S*kboBqV1dO(ihVAeEmdT94S{VIX+|` z{;qT-a08h@2zMh|7z2D*J?WYs}>Fq^6yAFmAxms;6sMd)Cu}aQf9P7GdFcCxB8WB6!~E~v2#4D zH9h5_*7vkJ&*}3|3aORoS*RR)+LGVfT0IhZ*Barvli6PC75AQG3t|QlJ*UGzn~gJ+ z*QtjSV|q7G6U-Q@rAnAZy*N`M zWtTof-apJZd!F{!DS_%&F$TI;`8e0sk*iBRk@6e$F=c|fV9ThLE17nnsPo#2{rFWqCwixA2TW~YD@yOsZ``D-d#~TY5PX$tgBUl5X06>_m_?( z_>fJMZ2t7{f@2o_u6MJ#z(z(dlXa4>My!why#&7R+-1Cxdzb4s7 z9mZR?bSD`V3!Otgrlc#{&6fN!HJ8&i=~LaBpsTS*;>sKIrzZJM`Ja9$jgPSp&1`!A z?YZBl%|@fyEc*KO`?#ywDUUDK z{<4v{^J_+Gr}Q)AJ=YezKXknc%s44Q?v>xMRl`?>>{=(8md(C%(&HfPj#G2js^<_ zy~ta7vPAS7EYo@-wsmySvPiy81eC4e`r+sopllkTY>ex+!0*?sTfdGS`xBxqP(2ez zp8#7=KkN|&myniEs156K;eXk0O0teD5R?QLRRB<)CG8myWsr-qvWpbN0N6A$qIW&~(& z4{pUuqcfVor{QC3?~01z$lX}QvKj%)sCX*A{rmA~pb!TX@P*w(g($egG)Oc`17`i9 zaik`;0j?My7`*Q2xEg;m0X_=WL?hOzJO+F{5D6RJ7tGUO@gXCK#uo{}O?ZE}`-W&~ zVmvV`SK@Kxg90twrThu#nntBg-hiG6=sbAnM`%~jHEmtpv1OO#$?@QUVUJ*5c75AF z50&5$VJ0Ag^szPlxy%d@Nn1rEj(&xRp(Do29o@l##srKf+*$E~HCDy2tNwe#9f!yu z!AkD{rA*bCu)7XKsDX$8-j@h(`$NRX*3}m#K4DPju@G7f>}^5R_rR`?w#2Fh-%s#0 zNJp!LCucXpV(YA+GEML)Xq@dN)$4j~_!LK=!iD!K zN-QfBfM;}1P1y&rja_qPM_YeCSMZcqeZ(XI3G{~QYZ20GabTKWz#ncr5;AO95*{li z!WO>8M+jw`eXuqHZd(Tbxm1bs8K(#Y#Q`+<^~9`D$@e0B20I=NUv@;p16mBA?HJ?k z;0hrh=oJzhPJaOo{6PYEDu53!>T&7}%BmI(YaNe-ys z+%3kTB1b@F7f`9+y)jM|gmeJ@P~cG!2`9ioFPf<`~yNumWvm=q?9;+srcJ_HQrKm~Gt(5t4r5;oy*N@#FC z1EBF$@*K307626ApzRmL0y;aogMD0S-d(=74O>p|ff^cx=f zTqU9P1n_*ID<^J{nWX?c7r^6N7^2-+csJy5V7m}CuBLjN#mPSk#u+9eH_#1H? zEZ#CW><%SgUwdk=DLwF^8&RH_=8mNO{|9AUs9$n za6Jlr0HW-G^yXjY3rEpWnhMveLuxa?!LcX=_@VGA`1pASEBFW)thOGkD#8a91p;dY z0EC)@#6Oszz0(k|KY)3~*PJk;Wqd!lOz#P4o0U^S4{Fxnc7apv>*DIQN^=O8<0CA` zA@3rA2>6D_KnLFm3N-~9*yeqpf!Idh@^=-M=l!oDF-5Rug8PSBIXd@ez0pEkT zX@(`?;|l2nr0y`y9ZLl|A{{{oU@(cT-`{JIIE$f7uo!|s#d3c_e-2QQ1S}=)I%)j);MS z!2d~*626xdZ-;2OW0nJhHJL`Q65$GEJz_dF@2&#?4gur%Vn6MG0HHNa2$U3Q5U>tR zYh!?I*aK_E!kh#Y(1Qb4x5PA@1PWuTD5N_5rJ#uc!(-y*>f`Ds1o09ya;v%qtYw^4 z2`CT)1!igtrXDCDKZ6C&;yrZvO95+!l-yV10W=BS7F8jpNcNV(^*(8$ji|l zROWy93=0GlP#2C#L8_J9K(QeoVa@cjfXq3Auh!R7_Wehi!c380k||kHlA+CrY%SL zurK~Taf(9$O?e}vFtjc>j}$0`0tI|ea5flA0bWZ)!8|sh0%PSNCSE1>69l-(Xf6qi z?@{~(MBM`$QA$C`Guc+00B+D*W;o9mweS~y~c zf&!kHHv)<5VO)UZ`r!J-9q7F715kW@GmHUHHE4A4v~^#Rfe?CX88$uE=0dC7 zDp?cm8g39okM^t>nnCXj6!`Gi{s`F;K(>dSj#>VR4e^#OywJZ)&nhyG1kzcA50M<9 z=t1Er5s<;pCTpC)+~aqC|j;@s=*X|Puc{*kIw-M#zj=;kW9dLb2f2sXqY|6g z1zEX6UJiW?&b<#DbsQ#;6Ut9b#-t0O;7J=k2j6x}aFjt-@~tbQIbgWSiU62j~rej_rmx?7I5tcH0W**VzJWiRtv$J#s6}2z1o7%d; z;bsew5D)<$WfjYW^(xf){Y&GMJ4#XUgAK=zwZ4}vg9|NBP~8vL4E{tH*eByx+g#MQ zjk}6S@y0ne>O27aQozSQLg_32;QzDZ*r4wJh~@|#f0W*h8*J8f1`6&VbbKp$`ZksV zEbBkcA<7kkQH@71uJ)@>Xu`}c@@w-BHy0KH{8oU+-~Fj~5IoBE!{3n6fNk%Gx?a)e zfS0>U{HRM6S3D@`kaCWV8bw65fQ~Po@mhrLkLr&oXJP=`9?~<*=~rz>7fgWd>g+dR z&S4xVjJ|D|Lzl_*VFB>Pv;QvqMiX`>2UrwjwQONJff6JlA=|!Y(D z3Pt7-JJ%Q0!Jss`Ry9?eU;$_Vdmpb+daHOv~tuDt}H zOaQ8H=FffuKvB9L-*g2$S_akfw0HM+fWUB#va%5YB6`o1sH%im2M||hee2s+5PhLq z4d)QDwvO(Q*+I|Xu8|NyZ;;|@Au46geUMf`{kdbnFEgHDEPe799W1S(kK=N&8i=gH zxETTCXu^JIwTh=0s2>%mAAUYo>e({1jw5op^p=bEYpH}j0W5NjsPDO*?*NW7fMa~M zBY&|BiwdCFb#vrE*;ACb263ZJv{uq|t&(LB``4z^Z~y@yfmEV8u{7{1lxT~tsda>v zN+cD&7cm(qMMt(u>&jm(L&H@YlxXqybYC;cODGeR*1F&JrV*vSzrY{-b7Jl_7Sspq z2*g0x@YU27l;wZy6T+?otg zmK6zrQ9&zGx7Q1qAuj;Ox0Vk+t^hBeQ0NSVvD&M+MPp}i#lQ^QUQ57CrU9rhaEtE& z-}RR-m&13ii{LMi-`3KwB5t3;EYh;gi5dfkh-&0#NRRow1 zfcTE&#V>yVu_xaDv^t^$sAwoBr&sCK$AEeQY%%_p6PU%KYM{C3aL}Q;54#hj=hi$y zf(f9J1f&byWwQaA1{lVdZsr^o8l}U zUvgO=n-;ldC&}s0a|h17g16XJ$u08wI__=UL|W zKSXT!3#x{Wby$XxFGH(H=$qsQ^d?-+RbZKC3lQ*&%D?mb53|1pK(vZ9 zc%ICU4aL%=sOsSJ|SbjShC0c?CbEwmYnjqO{;0Tq8mx+}B8nhvOac}I4=e+?_`z&5(?47k{2?LZ z*o;+N`x1y6B@{zPk6WNNn(0-d-pWjXj)eSo#E;3EdVPd|0t%PYD(!VX0sqKj!gm## zSrG+n-UEA?i`@YE&$`eCfYLh4ar9a_kuL;(8HzWl(jo#ifl3;D3;ymo#DS=wN!G}Q zVvk5D+N=_e1Oe+qz&anxm;H!!X3%l`CDqvZ2Xy6S36Q%U)mjn{ED1p6MuV`zrR@Z31~_y?!kd#bdF+gwp?)6KCV*w8}fhXXn+Ma zbVM0IT~|oH+Cr-fBwkj*qc{F=;ZdF6;>>U~Qq0x;xr=a^K$Z;N#I$vH1djqOfd(`! zud%v>6@FyBYz~%wM+(`qN+BKhVS#<2hYY$c!BK{st^qGLZ3#Y+zlXp3YPXXxXNFc_ z5BI>ab$}iOD}bLfzNq|{0<15py(}AgAIL1Ooikz}5DmJgTU@}r9zEHl1ImM+$k0&x zi|-13h}c6F%sl#KscfC7aW*JjAO%2R2aoZo&TfV zmGA-r$|}-7`k>|y1dywQ@9MvTgJmTJifkE5e63QKeFSWyT)~g3=eJRX5h3EANh8{S z5e-rK3AXQAZU!jPF8%@;4RcxdkC2@Ra zC+y%)W#(ka@s{%Lynz&~h| zph3wmY`SC3CMy*JY}ABy@CMjYVRgJkeY9Jl#L0%a1EQSJ)ckc{;T*0wLi1SC*N#I`kwtO&m1DXcEO7inXQ zNMNv2>Csm}vEJQ*12MlWEU*h}wi10KI0BJlcKFwgb?DkoHBiBKnriKrsh|#}mAB`h zq3i!BdlBA=LysschrOplX1)~3}L*1t-IUuNxSAcpx2Obw=v4- zkY8U)LCMLFz&?JDeXh$Ne7JXj@@A{HEMbEV>ZPH(r>1EIc<81bzER)e_6H9;bxI4{ z5Qc4r(!=_r`BTO*JUpfbXZ4!;Z!h zS@C?)Lr$)qu${1l0&EZLDoIHZXbdtuBeM%518}m^!e`(sd>5RBSPI~9EVs2Ux0vI< z>5Ah96E=?`!yxS;^9$Lv-2jiD6SWIO@UBjN%MGk<8TILC03tPvu6637REvgC((pz`u90LAx)kDdJk zincH0-`4Pk2@z2{ZT=JANi9G;3gW=G_umt-h=G4HdC;E$Vu76ondDVcChVm}bAx@3 zo_ma#E&$%;qu>U;qO6WRSUHk4f zVND<~+=pF=m`)l9XgA$1QEFTIW+vKcSLC5k*+G=(prl&Ji|Zuj_f= zclz%8d1u?djDN2C`R?a=?&rR*`|zf^BYOSmlu=4StXf@n`RE}!A{_Z6Gr48(v+&Ah zqo9xV(#~Jjp@~tLtb#co6Y(a@VIa!FEHTGmq9`jeDF@%5_NUv2Don;W1vPU|v;roF zq)R>50EJ6$fKDk6H*5;MI(j#bL9~{vLma|48(w1fM}U7AOO}33X@rE%U=zn7Kz(UQ zcmvadV{VD)rG5doGG|!&6MGk(g=9ZhhU;7Zf%Lrg{+r z2P_A6ZD30u{zp8A#BLvCOKdb!fF2u!6s&Q~;C92!4Wmk4`Ohz>WU->Y07^)s4oe zx5t3}8L;J}abT^B&B+5eIzYr5?OZEsE1y;8gejmt-A>HA1n4E$N=fdoUPov+dsH)g zyWAjJrv{{F*^g0Og-cr-*>}@MSiL|gC|ag7JA(`=!j1E}keV7GEwy*bz&m#Wt_PMW zO-dn5fmxz4*HA$;l47oneai2?>#{dCd7o}wwI>L`K!kOmuYBUbn@>C@!3=T(>4)0S zOQ;15<$a8TZGxiu3`s35?(OuIlxCbI5oOGGuVt z{7WN)NjzM2+Cjc^CW^=Z4fk$oXoQkL4V60@{{r8lccCE}Cso<)N$_?yE0^A5WIBFC zJYsnZUu%gapW*e^#g@V9w*X&iX&P7*EZJ#rKs5@FYCh_(hSJ>yKbhfMimZ!3={1g> zeC7=Es}ZmU@*2eKH!_Oe3*#HbW1s~xeD)Qquk^#ixvFHt6ly;e*fKNx%K?KOlbO#p zheS3~LGw|lAjwKa2w-VPb76du<}aU>yA9fZ2HKJPd-EFyosNuA3^Lo`+hHu3yM+(j zA;*%S2NzH`%Vsogz{m6=euH#p1|BvNVzHAn?=I}`Fk;%XW?X5VXmB^IB+o4ETrtfQx~R^j$F-oN^q(7b=S{8zbook^JZ{jv4aj16?e9^iDO~p9EV?}HB458zz?IBQ zos$OFZNoR(@a39yt1XigzxFQ-uYuiU^PNn4Zur5V@;kvGQMj+x5h+m1WbxwI6@~kz zVww8@DjCROX9$Qf3yyhokGj|KZ-MKv9JG>E%PhRb^H;*s?do$bI41(RuAF}qpe-Z! z?1K*H`mP5q(}eQQU%^2EsR;%-FC{ntPZ7%~vO$aEA+)(Ky>IMMpMHA&vPDqF1>AXQ z6Q9L)0vmZvSaEWitVlouCCvNN{(}g5Dupv~eX8xPQQ5#>0{lD0)4KGE!3SUA6P4>a zG^PYwRSBC-UV;VFl0sPV9|MIfU?MW6`Q(kQ4byqRnm}yyYxP0*U~$bXgZI;QgE#~U z^^lHwTx~Nq%2sVq{dA&Vt2E=TiCFJgtXFzk%Wo1r7hx;Rg$ZQMt#ipBpMw1)1y3p~ zwrfknjMEJPQD~?OrsK_Y;&G?C)qnJurgSN1?&YZVuR_joC`z7? z!6hLFucN8lhq>mf>~_LAzQ45znmOa#knNBEJQ>&SVWwF8{Wmbs~%{AFCTryU4y$j+~JZZl^m1bdk&W zdv@zb<=wF?>5CtdkNp#hnur~ce#=6%o_dSsGiI`d(3O)4X1596Z0nbt_L0%0(yHss zgk6IS>0&=z!In{jd@>$56#Rfba|x zSP1NKz?KhqRdl#PL1yJn!Hiiqm}*~lql#KM>$pZe)O{Zv`0oHNouj87FyMod3-BD` zw8Wz;6jOzK=91{@Ek3d@^_j5+3C_S$V9V;z^YCu4=)tTcL}DytdT>Z4Ouwt#!Io3M zzl+rgb~t>kCLysjC%|^9;nUpPWa&6gse$dtrQ>J6hZ2gwNb7WWs%@}gI=klbSi3Xk z{NilgflTgU)8K;{d`k8JSRT7JbqJi3+}{vIh`R9VW~CyV$H3AjA<#xGn`M3z-sv`; z)%*3ad*y}AA6O6Q32<5lrDgi~$$D5F!J?m?)eKHR{7p}ZY0DlC*8TV<)m zv4MBO$X$#74ZJOQt!3a)?qLTH6Iv0OI3;8W8L7GGrAk4hf~<8{``YOVp5(UX?ms$^ z+x9lJEB%ePn}mo+M6DV}+0ySSZX}52w@AxC)ZZ++f1lH1CdY1Kd!hXZp&=hg~YW5Qt1(NpGd@V?;ECOrVH5 z1c3|A!l9x3OZM~>4c1N8=yhc$5_*OWmbUGMmc(Z^9vVw?GvrklOw#zLUAXr` zYhbgoSzeDtj~i?{<3%kN@E*PAz`E7i=98VsoO@>W9H5g&l=1hJCyUS>w%?R~c4Yb= z?xT6bED2qd?in1{JAJAV?s1?qB*@3>NE=5&dXY21mi-hYhJMGDIH_C-#sMXEzkc{F z{E%0nCfQE99yOuxa?|n=2^bp6Ee^C+|EO9$>*DVn_tDf30DA+!K>E{H+ZkBL^{0Vy zQY>aB7y=Gt7$WmHq1Ltpx9pq-TTCQ9rhzYyMt{2ttHZP@7naFayCT z!?@LM4MTVVD~O9dM%s6$)4Q!pvk{EZnVge$Y+D@Vm|hL!S-8Uv%VJD~U$?jRcImsl ze0X>KGuY$fIHl4;+}0%oo}+vy0fM*@{?YpL>Kw6MYG+idW-nv;-(dODLIk>c_}pH% zTF7PSY!k1W40(s=EL=MC7_ip?yPSC98gwUiajnAow$g*BJ9b;AS-@%vEP0LQ^>DE0 z;28G|+vi)Ft(^zfq^&7mJBtKmEvQ%eP{+Y7y5o@9737{qzroH4O06u%JhGud-w9am z<5;eolsO6Gz?{JsSSQ;Sod}mCpBX?5MrXjF)Z{!&XRvRmF3hAQ=+=8j#7c-_#!ud* z-F-b#JaV!R)fBP&WzX`}uc4L_Gv?%i7c=X9d7h99cNTK%s{ zH>Cj?D75T|R2&0c|dGi2Yu0tW@^%66!g?Vk5Pci6KuZ@UgJ$pnnXaX92MhX6>#e zH+KLz7P^+!Z$CP&4RYIz0=QaW`;hbdU)|f0O6{!Wdi06%(MWa}cu7`t9OS~&FT|RW zDQC}GvhOISyA?Fc(9&O88KD1N1{#;AWFhO9eR+N@vVrc%nrt6!b8ELdAhp=@!?+kc57q~H5mQ%jl~-f zwhzLhrD0l$qA&-U_1w;@2wUV-cC$p?&oGTun@9uFdP?~yhN*ENv!r3F_M$7o$)bRC zhvVa039Z>m&gScQab6`5J%O9hwZt7T*2Tr7^6)MN>FLOA_?xCX)OuM1a%)A6$_Jqi ze&St55H((LaY5Rm#DT%3wSBs`h!rdAn#^fe9V@*BMEQPCAMYXN2lE;x;2*+dYWJd&u9IQDAZDeJW z(&>OmC}_X8b=3NKHWCVJ$9@XniMTNG9u9g_f}63eC^NOxqK^h&m>WA=bRP`ZKLJ)+ z(gSE>q*|OcJD(WAl6f2b3lenW3`w1%0C_DvX4bvXCNq(?>AE%=r;`34*9?G_f*kO2J*D!UWMRvI+xy(<>=z z7wC3ZZ5K0{5kwnEl0}bcU=%_mb;mFyQ}oDP&PjoE{}G*f$`Gl+5^YyKjjENv7zmN) zXzM#hNG2w)0g$Cz*p57aoBj(+SDG7S@%j1cYb&6b)NnTP7u22YfkQdmX1;O!*HDD4 z5973LONefN;l%b?<=3o3yd0%e z>B{RX^EZ$plTmuy`-F#b+Gh>`_n4aER9eSks8MDR->rd%WJxHeB+z2s{&Lta9>pvr zyf)93fH6<*v=!7pnT#5?+TS_S6?2@EbM22|by#Py7@I8b^N9tN;Kcs86x;#-6tfk^v4T=Op3E!~|0?X}~%Sx;~R-RW%=J7E0A#rA28R_i9m z(fVLvRlakBfhwKv-thOi?ha(U6FJ2)3W>_1$!*q}hbwee1vEWy>dYIip5+P7UFV>i z+7}X64t?l0SeZ#dmIPfUKqf2y%*h3)ls0v@(zY*M??`ZY6u4C`--&+^Q+e}MEGe4E zVlhHY{pPQQLqmITUStS#Wuqenh65U6{=&Q*aR`IJE@ICu{D9IEy^YaRI))YESFmMY zI-;9CaqzuCJp5=3zJJB)7zhcrt{YECaC?o~~5Frh(o+zL9qx7iztA zMbkbr$N;9_jFai3-V0x+@Jj02^F)A01x3KerdTt5kCMQbY{|tct=@rz?bt+l^^foJ zB$)aazR0+t_TK3Sj0w70``b)ICN2}KL{A2TH+xg{wV@X}A*Ep=8L`UHz4WY_9tuV7l0zoQ87+Xw$S_SD zdSjhBDN(V&AU-a5|o{ z%H)9~4mb{DG#o+`7x`oy^<1HH06n{9S1TriIFwW}9$y_bp!igXI}kPjbG@vEwp0rx zZ`H2xk$9961~yn)xO~iD=1{>{Deig?^Q~eM)jme$HU-YqiTwI`=odvtoOHmgZ=r|e zp>@`Tfua0(b|~UsejuXvHD$gAIMCoc$k1Av&?6j4P|Hs Date: Mon, 8 Nov 2021 08:09:23 -0700 Subject: [PATCH 103/105] Latest Changes from Saturday Evening Not sure Git is syncing these anymore for whatever reason. Only major correction was the code fix for meta tags in sources that was crashing a week ago. --- lyrics+.lua | 291 +++++++++++++++++++++++++--------------------------- 1 file changed, 142 insertions(+), 149 deletions(-) diff --git a/lyrics+.lua b/lyrics+.lua index 7be0167..3582414 100644 --- a/lyrics+.lua +++ b/lyrics+.lua @@ -2,9 +2,9 @@ -- / / | | / / / ___ / /_ __/ / ___\ / ___\ __/ /_ -- / / | |_/ / / /__/ / / / / / \ \ /_ __/ -- / /___ |_ _/ / __ | __/ /__ | |__ __\ \ /__/ --- /______/ /_/ /_/ |_|/______/ |____\ /____/ --- --- +-- /______/ /_/ /_/ |_|/______/ |____\ /____/ +-- +-- --- Copyright 2020 amirchev/wzaggle -- Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,14 +20,14 @@ -- limitations under the License. --------------------------------------------------------------------------------------------------------------------- -- Lyrics or Lyrics+ is a joint effort started by amirchev and joined by wzaggle (DC Strato) --- The lua script breaks up text stored in files to be shown as pages in an OBS stream and is designed to be used +-- The lua script breaks up text stored in files to be shown as pages in an OBS stream and is designed to be used -- with songs, scripture, responsive reading, or any text that needs to have managed pages. -- Songs or Text files can be created and edited within the script or with an external text editor -- Files can be either A) Previewed to the scene, B) Pre-loaded into a "Prepared List" queue to be displayed in -- the order of the list, or C) loaded on the fly when a scene loads, using a Prepare Lyric source within the scene. --- The general function of the script is to modify the contents, visibility, and opacity levels for text sources +-- The general function of the script is to modify the contents, visibility, and opacity levels for text sources -- that have been created within OBS scenes. Currently only a single Global set of text sources are supported for -- Text Source -------> The text source that will contain the Pages of song lyrics @@ -40,7 +40,7 @@ -- NOTES ON INTERNAL DOCUMENTATION -- Effort has been made to try and lay out the general function of the script for those wishing to contribute or just -- follow its operation. The terms Text File, Song, or Lyric all refer to the text that is the content to be --- paged by the script, and are used interchangeably within the internal documentation, whatever the purpose or +-- paged by the script, and are used interchangeably within the internal documentation, whatever the purpose or -- intent of that text might be to the user. @@ -129,12 +129,15 @@ mon_alt = "" mon_nextalt = "" mon_nextsong = "" meta_tags = "" -source_meta_tags = "" +-- META TAG VARIABLES +source_meta_tags = "" -- Current Filter Tags +use_meta_tags = false -- flag to use/not use filter +filtering = false -- set to keep user informed of background process -- FLAGS USED IN USER INTERFACE expandcollapse = true -- flag used in UI progressive disclosure -showhelp = false -- flag used to open and close markup HELP button text +showhelp = false -- flag used to open and close markup HELP button text -- TEXT STATUS & FADE TEXT_VISIBLE = 0 -- text is visible @@ -146,7 +149,7 @@ TEXT_TRANSITION_IN = 6 -- fade in transition after lyric change TEXT_HIDE = 7 -- turn off the text and ignore fade if selected TEXT_SHOW = 8 -- turn on the text and ignore fade if selected --- GENERAL TEXT FADING +-- GENERAL TEXT FADING text_fade_enabled = false -- fading effect enabled (if false then source visibility is toggled rather than opacity changed) all_sources_fade = false -- Title and Static fade when lyrics are changing or during show/hide text_status = TEXT_VISIBLE -- current state of desired source visibility, one of above states VISIBLE thru SHOW @@ -156,7 +159,7 @@ text_fade_speed = 5 -- speed used to fade text -- FLAGS TO CONTROL BACKGROUND FADING allow_back_fade = false -- overall fading is performed for text source background colors use100percent = true -- Fading is 0-100% of opacity or 0 to Marked opacity -fade_text_back = false -- Text Background should fade +fade_text_back = false -- Text Background should fade fade_title_back = false -- Title Background should fade fade_alternate_back = false -- Alternate Lyric Background should fade fade_static_back = false -- Static Text Background should fade @@ -165,10 +168,10 @@ fade_extra_back = false -- Extra Linked Source Background should fade (if text --[[ transitions are a work in progress to support duplicate source mode (not very stable) in this mode OBS keeps separate sources in preview and active windows. Only pointers to the preview window are accessable to the API. Changing the Active window requires both changing the Preview Window Text sources then -transitioning those changes to the Active Window. Fading is disabled because its effects are not visible in +transitioning those changes to the Active Window. Fading is disabled because its effects are not visible in the active window. --]] - -transition_enabled = false + +transition_enabled = false transition_completed = false ------------------------------------------------------------------------------------------------------------------------- @@ -191,8 +194,8 @@ help = "Optional comma delimited meta tags follow '//meta ' on 1st line" -- SIMPLE DEBUGGING/PRINT MECHANISM -DEBUG = true -- on switch for entire debugging mechanism -DEBUG_METHODS = true -- print method names +--DEBUG = true -- on switch for entire debugging mechanism +--DEBUG_METHODS = true -- print method names --DEBUG_INNER = true -- print inner method breakpoints --DEBUG_CUSTOM = true -- print custom debugging messages --DEBUG_BOOL = true -- print message with bool state true/false @@ -202,7 +205,7 @@ DEBUG_METHODS = true -- print method names ------------------------ PAGING FUNCTIONS ---------------- -------- --- +-- --------------------------------------------------------------------------------------------------------------------- -- Function to move to Next page (lyric) of a song or text (HOTKEY ENABLED) -- contents of text sources are only changed if they are showing to prevent accidental background paging @@ -265,7 +268,7 @@ function prev_prepared(pressed) if #prepared_songs == 0 then return end - using_preview = false + using_preview = false if using_source then using_source = false prepare_selected(prepared_songs[prepared_index]) @@ -299,7 +302,7 @@ function next_prepared(pressed) if #prepared_songs == 0 then return end - using_preview = false + using_preview = false if using_source then using_source = false dbg_custom("do current prepared") @@ -335,7 +338,7 @@ function home_prepared(pressed) dbg_method("home_prepared") using_source = false page_index = 0 - using_preview = false + using_preview = false local prop_prep_list = obs.obs_properties_get(props, "prop_prepared_list") if #prepared_songs > 0 then obs.obs_data_set_string(script_sets, "prop_prepared_list", prepared_songs[1]) @@ -495,7 +498,7 @@ end ----------------------------------------------------------------------------------------------------------------------- -- PREPARE SONG -- Adds the currently selected song from the directory to the prepared list. Sets index to first song. ------------------------------------------------------------------------------------------------------------------------ +----------------------------------------------------------------------------------------------------------------------- function prepare_song_clicked(props, p) dbg_method("prepare_song_clicked") @@ -509,7 +512,7 @@ function prepare_song_clicked(props, p) obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") if #prepared_songs > 0 then obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") - else + else obs.obs_property_set_description(prop_prep_list, "Prepared") end obs.obs_properties_apply_settings(props, script_sets) @@ -520,17 +523,17 @@ end ------------------------------------------------------------------------------------------------------------------------- -- PREVIEW SONG (Preveiw Mode) -- Prepares the selected song into text sources without adding it to the prepared list -------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------- function preview_clicked(props, p) dbg_method("preview_song_clicked") if using_preview then using_preview = false if source_active then load_source_song(load_source, false) - elseif #prepared_songs > 0 then + elseif #prepared_songs > 0 then prepare_song_by_index(prepared_index) end - transition_lyric_text() + transition_lyric_text() return true end local song = obs.obs_data_get_string(script_sets, "prop_directory_list") @@ -549,7 +552,7 @@ end -- LOAD SONG FROM DIRECTORY -- Loads the text from the selected song into the Lyrics Edit window for editing within properties -- Selects the current song for possible addition to prepared list -------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------- function load_song_from_directory(props, prop, settings) local name = obs.obs_data_get_string(script_sets, "prop_directory_list") @@ -638,8 +641,7 @@ end function refresh_directory() local prop_dir_list = obs.obs_properties_get(script_props, "prop_directory_list") local source_prop = obs.obs_properties_get(props, "prop_source_list") - source_filter = false - load_source_song_directory(true) + load_source_song_directory(use_meta_tags) table.sort(song_directory) obs.obs_property_list_clear(prop_dir_list) -- clear directories for _, name in ipairs(song_directory) do @@ -675,7 +677,7 @@ function clear_prepared_clicked(props, p) prepared_songs = {} -- required for monitor page page_index = 0 -- required for monitor page prepared_index = 0 -- required for monitor page - save_prepared() + save_prepared() if source_active then load_source_song(load_source, false) end @@ -686,7 +688,7 @@ function clear_prepared_clicked(props, p) obs.obs_property_list_clear(prep_prop) obs.obs_property_set_description(obs.obs_properties_get(props, "prop_prepared_list"), "Prepared") obs.obs_property_list_add_string(obs.obs_properties_get(props, "prop_prepared_list"), "*** LIST OF PREPARED SONGS ***", "") - obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") + obs.obs_data_set_string(script_sets, "prop_prepared_list", "*** LIST OF PREPARED SONGS ***") local pp = obs.obs_properties_get(props, "edit_grp") if obs.obs_property_visible(pp) then obs.obs_property_set_visible(pp, false) @@ -761,7 +763,7 @@ end ----------------------------------------------------------------------------------------------------------------------- -- DO LINKED Option (Progressive disclosure) --- Shows list to allow linking extra sources +-- Shows list to allow linking extra sources ------------------------------------------------------------------------------------------------------------------------ function do_linked_clicked(props, p) dbg_method("do_link_clicked") @@ -773,7 +775,7 @@ function do_linked_clicked(props, p) end ----------------------------------------------------------------------------------------------------------------------- --- CLEAR LINKED Option +-- CLEAR LINKED Option -- Removes all additionally linked Show/Hide sources and hides Link Options in UI ------------------------------------------------------------------------------------------------------------------------ function clear_linked_clicked(props, p) @@ -926,26 +928,26 @@ function change_ctrl_visible(props, prop, settings) return true end ----------------------------------------------------------------------------------------------------------------------- --- Change state of the Fade Option +-- Change state of the Fade Option -- Fading enables or disables a number of other options like 0-100% and if backgrounds are faded ------------------------------------------------------------------------------------------------------------------------ function change_fade_property(props, prop, settings) local text_fade_set = obs.obs_data_get_bool(settings, "text_fade_enabled") obs.obs_property_set_visible(obs.obs_properties_get(props, "text_fade_speed"), text_fade_set) obs.obs_property_set_visible(obs.obs_properties_get(props, "use100percent"), text_fade_set) - obs.obs_property_set_visible(obs.obs_properties_get(props, "allowBackFade"), text_fade_set) + obs.obs_property_set_visible(obs.obs_properties_get(props, "allowBackFade"), text_fade_set) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_set and allow_back_fade) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_set and allow_back_fade) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_set and allow_back_fade) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_set and allow_back_fade) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_set and allow_back_fade) - obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), text_fade_enabled and not use100percent) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_set and allow_back_fade) + obs.obs_property_set_visible(obs.obs_properties_get(props, "refreshOP"), text_fade_enabled and not use100percent) local transition_set_prop = obs.obs_properties_get(props, "transition_enabled") obs.obs_property_set_enabled(transition_set_prop, not text_fade_set) return true end ----------------------------------------------------------------------------------------------------------------------- --- Change state of the 0-100% option +-- Change state of the 0-100% option ----------------------------------------------------------------------------------------------------------------------- function change_100percent_property(props, prop, settings) use100percent = obs.obs_data_get_bool(settings, "use100percent") @@ -957,18 +959,18 @@ end ----------------------------------------------------------------------------------------------------------------------- function change_back_fade_property(props, prop, settings) allow_back_fade = obs.obs_data_get_bool(settings, "allowBackFade") - if allow_back_fade then + if allow_back_fade then obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), text_fade_enabled) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), text_fade_enabled) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), text_fade_enabled) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), text_fade_enabled) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_enabled) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), text_fade_enabled) else obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_text_back"), false) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_alternate_back"), false) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_title_back"), false) obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_extra_back"), false) - obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), false) + obs.obs_property_set_visible(obs.obs_properties_get(props, "fade_static_back"), false) end return true end @@ -989,17 +991,18 @@ end ----------------------------------------------------------------------------------------------------------------------- -- Allow specifying meta tags for filtering songs ----------------------------------------------------------------------------------------------------------------------- + function filter_songs_clicked(props, p) local pp = obs.obs_properties_get(props, "meta") if not obs.obs_property_visible(pp) then obs.obs_property_set_visible(pp, true) local mpb = obs.obs_properties_get(props, "filter_songs_button") - obs.obs_property_set_description(mpb, "Clear Filters") -- change button function - meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") - refresh_directory() + source_meta_tags = obs.obs_data_get_string(script_sets, "prop_edit_metatags") + use_meta_tags = true + obs.obs_property_set_description(mpb, "Clear Filters") -- change button function else obs.obs_property_set_visible(pp, false) - meta_tags = "" -- clear meta tags + use_meta_tags = false refresh_directory() local mpb = obs.obs_properties_get(props, "filter_songs_button") -- obs.obs_property_set_description(mpb, "Filter Titles by Meta Tags") -- reset button function @@ -1070,10 +1073,10 @@ function save_edits_clicked(props, p) obs.obs_property_set_visible(pp, false) local mpb = obs.obs_properties_get(props, "prop_manage_button") obs.obs_property_set_description(mpb, "Edit Prepared Songs List") - + if #prepared_songs > 0 then obs.obs_property_set_description(prop_prep_list, "Prepared (" .. #prepared_songs .. ")") - else + else obs.obs_property_set_description(prop_prep_list, "Prepared") end obs.obs_properties_apply_settings(props, script_sets) @@ -1145,21 +1148,21 @@ end ------------------------------------------------------------------------------------------------------------------------- function setSourceOpacity(sourceName, fadeBackground) dbg_method("set_Opacity") - if sourceName ~= nil and sourceName ~= "" then + if sourceName ~= nil and sourceName ~= "" then if text_fade_enabled then - local settings = obs.obs_data_create() + local settings = obs.obs_data_create() if use100percent then -- try to honor preset maximum opacities obs.obs_data_set_int(settings, "opacity", text_opacity) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", text_opacity) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity + obs.obs_data_set_int(settings, "gradient_opacity", text_opacity) -- Set new gradient opacity if fadeBackground then - obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity + obs.obs_data_set_int(settings, "bk_opacity", text_opacity) -- Set new background opacity end else adj_text_opacity = text_opacity /100 obs.obs_data_set_int(settings, "opacity", adj_text_opacity * max_opacity[sourceName]["opacity"]) -- Set new text opacity to zero obs.obs_data_set_int(settings, "outline_opacity", adj_text_opacity * max_opacity[sourceName]["outline"]) -- Set new text outline opacity to zero - obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity + obs.obs_data_set_int(settings, "gradient_opacity", adj_text_opacity * max_opacity[sourceName]["gradient"]) -- Set new gradient opacity if fadeBackground then obs.obs_data_set_int(settings, "bk_opacity", adj_text_opacity * max_opacity[sourceName]["background"]) -- Set new background opacity end @@ -1169,10 +1172,10 @@ function setSourceOpacity(sourceName, fadeBackground) obs.obs_source_update(source, settings) end obs.obs_source_release(source) - obs.obs_data_release(settings) + obs.obs_data_release(settings) else dbg_inner("use on/off") - -- do preview scene item + -- do preview scene item local sceneSource = obs.obs_frontend_get_current_preview_scene() local sceneObj = obs.obs_scene_from_source(sceneSource) local sceneItem = obs.obs_scene_find_source_recursive(sceneObj, sourceName) @@ -1183,13 +1186,12 @@ function setSourceOpacity(sourceName, fadeBackground) obs.obs_sceneitem_set_visible(sceneItem, false) end end --- update_monitor() end end ------------------------------------------------------------------------------------------------------------------------- -- APPLY SOURCE OPACITY --- Uses SET SOURCE OPACITY to manage source opacities according to current options +-- Uses SET SOURCE OPACITY to manage source opacities according to current options ------------------------------------------------------------------------------------------------------------------------- function apply_source_opacity() dbg_method("Apply Opacity") @@ -1213,12 +1215,12 @@ dbg_method("Apply Opacity") else -- check for filter named "Color Correction" local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") if color_filter ~= nil and text_fade_enabled then -- update filters opacity - local filter_settings = obs.obs_data_create() + local filter_settings = obs.obs_data_create() if use100percent then - obs.obs_data_set_double(filter_settings, "opacity", text_opacity/100) + obs.obs_data_set_double(filter_settings, "opacity", text_opacity/100) else obs.obs_data_set_double(filter_settings, "opacity", (text_opacity/100) * max_opacity[sourceName]["CC-opacity"]) - end + end obs.obs_source_update(color_filter, filter_settings) obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) @@ -1231,7 +1233,7 @@ dbg_method("Apply Opacity") obs.obs_sceneitem_set_visible(sceneItem, true) else obs.obs_sceneitem_set_visible(sceneItem, false) - end + end end end end @@ -1246,24 +1248,25 @@ end -- This function reads the current opacity levels in settings. ------------------------------------------------------------------------------------------------------------------------- function getSourceOpacity(sourceName) - if sourceName ~= nil and sourceName ~= "" then + if sourceName ~= nil and sourceName ~= "" then local source = obs.obs_get_source_by_name(sourceName) local settings = obs.obs_source_get_settings(source) max_opacity[sourceName]={} max_opacity[sourceName]["opacity"] = obs.obs_data_get_int(settings, "opacity") -- text opacity max_opacity[sourceName]["outline"] = obs.obs_data_get_int(settings, "outline_opacity") -- outline opacity - max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity - max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity - obs.obs_source_release(source) + max_opacity[sourceName]["gradient"] = obs.obs_data_get_int(settings, "gradient_opacity") -- gradient opacity + max_opacity[sourceName]["background"] = obs.obs_data_get_int(settings, "bk_opacity") -- background opacity obs.obs_data_release(settings) + obs.obs_source_release(source) + end end ------------------------------------------------------------------------------------------------------------------------- -- READ SOURCE OPACITY --- Lyrics tries to honor maximum opacities that might have been set for effects. If 0-100% is disabled then Lyrics +-- Lyrics tries to honor maximum opacities that might have been set for effects. If 0-100% is disabled then Lyrics -- Will try to mark current maximum opacity levels of sources using getSourceOpacity and use that number --- as the sources maximum opacity when fading sources out and back. +-- as the sources maximum opacity when fading sources out and back. ------------------------------------------------------------------------------------------------------------------------- function read_source_opacity() dbg_method("read_source_opacity") @@ -1282,11 +1285,11 @@ function read_source_opacity() if source_id == "text_gdiplus" or source_id == "text_ft2_source" then -- just another text object getSourceOpacity(sourceName) else -- check for filter named "Color Correction" - + local color_filter = obs.obs_source_get_filter_by_name(extra_source, "Color Correction") if color_filter ~= nil then -- update filters opacity local filter_settings = obs.obs_source_get_settings(color_filter) - max_opacity[sourceName]={} + max_opacity[sourceName]={} max_opacity[sourceName]["CC-opacity"] = obs.obs_data_get_double(filter_settings, "opacity") obs.obs_data_release(filter_settings) obs.obs_source_release(color_filter) @@ -1300,15 +1303,14 @@ end ------------------------------------------------------------------------------------------------------------------------- -- SET TEXT VISIBILITY --- Manages visibility of the text sources from Hidden, through fading in or out, to Visible. -------------------------------------------------------------------------------------------------------------------------- +-- Manages visibility of the text sources from Hidden, through fading in or out, to Visible. +------------------------------------------------------------------------------------------------------------------------- function set_text_visibility(end_status) dbg_method("set_text_visibility") -- if already at desired visibility, then exit if text_status == end_status then return end - print("text_status: " .. text_status) if end_status == TEXT_HIDE then text_opacity = 0 text_status = TEXT_HIDDEN @@ -1348,7 +1350,7 @@ end ------------------------------------------------------------------------------------------------------------------------- -- TRANSITION LYRIC TEXT --- This function hides current lyric (with or without fading), changes to next page of lyrics, and then re-displays +-- This function hides current lyric (with or without fading), changes to next page of lyrics, and then re-displays -- the new page (with or without fading). ------------------------------------------------------------------------------------------------------------------------- function transition_lyric_text(force_show) @@ -1516,7 +1518,7 @@ end ------------------------------------------------------------------------------------------------------------------------- -- FADE CALLBACK -- Gradually increments or decrements target source object opacities towards the target visibility state, stopping --- the callback timer when the target visibility is reached. +-- the callback timer when the target visibility is reached. ------------------------------------------------------------------------------------------------------------------------- function fade_callback() -- if not in a transitory state, exit callback @@ -1559,7 +1561,7 @@ end ------------------------------------------------------------------------------------------------------------------------- -- PREPARE SONG BY NUMBER --- Prepares a song by its position in the prepared lyrics list +-- Prepares a song by its position in the prepared lyrics list ------------------------------------------------------------------------------------------------------------------------- function prepare_song_by_index(index) dbg_method("prepare_song_by_index") @@ -1571,8 +1573,8 @@ end --------------------------------------------------------------------------------------------------------------------- -- PREPARE SONG BY NAME -- Function to parse and process markups within the lyrics and break the text into defined pages and verses --- The first line of song/text files can contain an optional list of meta tags that organize the files into --- user defined genre or categories for later filtering during selection +-- The first line of song/text files can contain an optional list of meta tags that organize the files into +-- user defined genre or categories for later filtering during selection -- Currently supported markups all start with # or // @@ -1691,14 +1693,13 @@ function prepare_song_by_name(name) end local static_index = line:find("#S:") -- Single Static if static_index ~= nil then - line = line:sub(static_index + 3) + line = line:sub(static_index+3) static_text = line new_lines = 0 end local title_index = line:find("#T:") -- Set Title if title_index ~= nil then - local title_indexEnd = line:find("%s+", title_index + 1) - line = line:sub(title_indexEnd + 1) + line = line:sub(title_index+3) alt_title = line new_lines = 0 end @@ -1850,8 +1851,8 @@ function prepare_song_by_name(name) end end singleAlternate = false - else -- Text goes to Refrain or Verse Lyrics - if recordRefrain then + else -- Text goes to Refrain or Verse Lyrics + if recordRefrain then displaySize = refrain_display_lines -- display lines controlled by Refrain Size else displaySize = adjusted_display_lines -- display lines controlled by Lyric Size @@ -1900,7 +1901,7 @@ function prepare_song_by_name(name) end end end - if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then -- pad lines + if ensure_lines and lyrics[#lyrics] ~= nil and cur_line > 1 then -- pad lines for i = cur_line, displaySize, 1 do cur_line = i if use_alternate then @@ -1923,8 +1924,8 @@ end ------------------------------------------------------------------------------------------------------------------------- -- GET INDEX IN LIST --- Finds a specifically named song in the prepared list ---------------------------------------------------------------------------------------------------------------------------- +-- Finds a specifically named song in the prepared list +--------------------------------------------------------------------------------------------------------------------------- function get_index_in_list(list, q_item) for index, item in ipairs(list) do if item == q_item then @@ -1952,7 +1953,6 @@ function delete_song(name) end os.remove(path) -- delete from OS table.remove(song_directory, get_index_in_list(song_directory, name)) -- delete from table - source_filter = false load_source_song_directory(false) end @@ -1960,23 +1960,20 @@ end -- LOAD SONG DIRECTORY -- Loads the current song directory into a song_directory table -- optionally uses a filter based on a list of desired meta keys to include --- If using a filter then only files that have those selected meta keys on their first line will be +-- If using a filter then only files that have those selected meta keys on their first line will be -- included in the directory table ------------------------------------------------------------------------------------------------------------------------- function load_source_song_directory(use_filter) dbg_method("load_source_song_directory") local keytext = meta_tags - if source_filter then + if use_filter then keytext = source_meta_tags end - dbg_inner(keytext) local keys = ParseCSVLine(keytext) - song_directory = {} local filenames = {} local tags = {} local dir = obs.os_opendir(get_songs_folder_path()) - -- get_songs_folder_path()) local entry local songExt local songTitle @@ -1990,9 +1987,9 @@ function load_source_song_directory(use_filter) then songExt = obs.os_get_path_extension(entry.d_name) songTitle = string.sub(entry.d_name, 0, string.len(entry.d_name) - string.len(songExt)) - tags = readTags(songTitle) goodEntry = true if use_filter and #keys > 0 then -- need to check files + tags = readTags(songTitle) for k = 1, #keys do if keys[k] == "*" then goodEntry = true -- okay to show untagged files @@ -2055,8 +2052,8 @@ function readTags(name) file:close() end local meta_index = meta:find("//meta ") -- Look for meta block Set - if meta_index ~= nil then - meta = meta:sub(meta_index + 7) + if meta_index ~= nil then + meta = meta:sub(meta_index + 6) return ParseCSVLine(meta) end return {} @@ -2117,7 +2114,7 @@ local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" -- -- Encode invalid filename -- Encodes title/filename if it contains invalid filename characters -- Note: User should use valid filenames to prevent encoding and the override the title with '#t: title' in markup ---------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------- function enc(data) return ((data:gsub( ".", @@ -2222,7 +2219,7 @@ function save_song(name, text) end ------------------------------------------------------------------------------------------------------------------------- --- SAVE PREPARED SONGS to PREPARED.DAT +-- SAVE PREPARED SONGS to PREPARED.DAT -- User has option to store the List of Prepared songs in an external dat file with songs. ------------------------------------------------------------------------------------------------------------------------- @@ -2242,27 +2239,27 @@ end -- function get_text() local source = obs.obs_get_source_by_name(source_name) - if source ~= nil then + if source ~= nil then local settings = obs.obs_source_get_settings(source) - if settings ~= nil then + if settings ~= nil then mon_lyric = obs.obs_data_get_string(settings,"text"):gsub("\n", "
• ") obs.obs_data_release(settings) end obs.obs_source_release(source) end local alt_source = obs.obs_get_source_by_name(alternate_source_name) - if alt_source ~= nil then + if alt_source ~= nil then local settings = obs.obs_source_get_settings(alt_source) - if settings ~= nil then + if settings ~= nil then mon_alt = obs.obs_data_get_string(settings,"text"):gsub("\n", "
• ") obs.obs_data_release(settings) end obs.obs_source_release(alt_source) - end - local title_source = obs.obs_get_source_by_name(title_source_name) - if title_source ~= nil then + end + local title_source = obs.obs_get_source_by_name(title_source_name) + if title_source ~= nil then local settings = obs.obs_source_get_settings(title_source) - if settings ~= nil then + if settings ~= nil then mon_song = obs.obs_data_get_string(settings,"text") obs.obs_data_release(settings) end @@ -2274,7 +2271,7 @@ end ------------------------------------------------------------------------------------------------------------------------- -- UPDATE MONITOR --- Lyrics maintains an external Monitor.htm file in the songs directory that can be viewed externally or docked within +-- Lyrics maintains an external Monitor.htm file in the songs directory that can be viewed externally or docked within -- OBS as a browser option. This function keeps that files HTML text updated. ------------------------------------------------------------------------------------------------------------------------- function update_monitor() @@ -2297,10 +2294,10 @@ function update_monitor() text = text .. "