diff --git a/GNUmakefile b/GNUmakefile index 2612551..be70750 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -47,6 +47,7 @@ common: chmod 755 $(LIB_PATH)/syncplay/ cp -r resources/hicolor $(SHARE_PATH)/icons/ cp -r resources/*.png $(LIB_PATH)/syncplay/resources/ + cp -r resources/*.lua $(LIB_PATH)/syncplay/resources/ cp -r resources/lua/intf/*.lua $(LIB_PATH)/syncplay/resources/lua/intf/ cp resources/hicolor/48x48/apps/syncplay.png $(SHARE_PATH)/app-install/icons/ cp resources/hicolor/48x48/apps/syncplay.png $(SHARE_PATH)/pixmaps/ diff --git a/buildPy2exe.py b/buildPy2exe.py index 6b2100c..4f8f8d9 100755 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -671,6 +671,8 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock 'resources/tick.png', 'resources/lock_open.png', 'resources/empty_checkbox.png', 'resources/tick_checkbox.png', 'resources/world_explore.png', 'resources/application_get.png', 'resources/cog.png', 'resources/arrow_switch.png', 'resources/film_go.png', 'resources/world_go.png', 'resources/arrow_refresh.png', 'resources/bullet_right_grey.png', + 'resources/user_comment.png', + 'resources/error.png', 'resources/film_folder_edit.png', 'resources/film_edit.png', 'resources/folder_film.png', @@ -679,7 +681,7 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock 'resources/email_go.png', 'resources/world_add.png', 'resources/film_add.png', 'resources/delete.png', 'resources/spinner.mng' ] -resources = ["resources/icon.ico", "resources/syncplay.png", "resources/license.rtf", "resources/third-party-notices.rtf"] +resources = ["resources/icon.ico", "resources/syncplay.png", "resources/syncplayintf.lua", "resources/license.rtf", "resources/third-party-notices.rtf"] resources.extend(guiIcons) intf_resources = ["resources/lua/intf/syncplay.lua"] diff --git a/resources/error.png b/resources/error.png new file mode 100644 index 0000000..628cf2d Binary files /dev/null and b/resources/error.png differ diff --git a/resources/syncplayintf.lua b/resources/syncplayintf.lua new file mode 100644 index 0000000..db20e90 --- /dev/null +++ b/resources/syncplayintf.lua @@ -0,0 +1,979 @@ +-- syncplayintf.lua -- An interface for communication between mpv and Syncplay +-- Author: Etoh, utilising repl.lua code by James Ross-Gowan (see below) +-- Thanks: RiCON, James Ross-Gowan, Argon-, wm4, uau + +-- Includes code copied/adapted from repl.lua -- A graphical REPL for mpv input commands +-- +-- c 2016, James Ross-Gowan +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-- See https://github.com/rossy/mpv-repl for a copy of repl.lua + +local CANVAS_WIDTH = 1920 +local CANVAS_HEIGHT = 1080 +local ROW_HEIGHT = 100 +local chat_format = "{\\fs50}{\an1}" +local max_scrolling_rows = 100 +local MOVEMENT_PER_SECOND = 200 +local TICK_INTERVAL = 0.01 +local CHAT_MODE_CHATROOM = "Chatroom" +local CHAT_MODE_SUBTITLE = "Subtitle" +local CHAT_MODE_SCROLLING = "Scrolling" +local last_chat_time = 0 +local use_alpha_rows_for_chat = true +local MOOD_NEUTRAL = 0 +local MOOD_BAD = 1 +local MOOD_GOOD = 2 +local WORDWRAPIFY_MAGICWORD = "{\\\\fscx0} {\\\\fscx100}" +local SCROLLING_ADDITIONAL_BOTTOM_MARGIN = 75 +local default_oscvisibility_state = "never" + +local ALPHA_WARNING_TEXT_COLOUR = "FF00FF" -- RBG +local HINT_TEXT_COLOUR = "AAAAAA" -- RBG +local NEUTRAL_ALERT_TEXT_COLOUR = "FFFFFF" -- RBG +local BAD_ALERT_TEXT_COLOUR = "0000FF" -- RBG +local GOOD_ALERT_TEXT_COLOUR = "00FF00" -- RBG +local NOTIFICATION_TEXT_COLOUR = "FFFF00" -- RBG + +local FONT_SIZE_MULTIPLIER = 2 + +local chat_log = {} + +local assdraw = require "mp.assdraw" + +local opt = require 'mp.options' + +local repl_active = false +local insert_mode = false +local line = '' +local cursor = 1 +local key_hints_enabled = false + +function format_scrolling(xpos, ypos, text) + local chat_message = "\n"..chat_format .. "{\\pos("..xpos..","..ypos..")\\q2}"..text.."\\N\\n" + return string.format(chat_message) +end + +function format_chatroom(text) + local chat_message = chat_format .. text .."\\N\\n" + return string.format(chat_message) +end + +function clear_chat() + chat_log = {} +end + +local alert_osd = "" +local last_alert_osd_time = nil +local alert_osd_mood = MOOD_NEUTRAL + +local notification_osd = "" +local last_notification_osd_time = nil +local notification_osd_mood = MOOD_NEUTRAL + +function set_alert_osd(osd_message, mood) + alert_osd = osd_message + last_alert_osd_time = mp.get_time() + alert_osd_mood = mood +end + +function set_notification_osd(osd_message, mood) + notification_osd = osd_message + last_notification_osd_time = mp.get_time() + notification_osd_mood = mood +end + +function add_chat(chat_message, mood) + last_chat_time = mp.get_time() + local entry = #chat_log+1 + for i = 1, #chat_log do + if chat_log[i].text == '' then + entry = i + break + end + end + local row = ((entry-1) % max_scrolling_rows)+1 + if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then + if entry > opts['chatMaxLines'] then + table.remove(chat_log, 1) + entry = entry - 1 + end + end + chat_log[entry] = { xpos=CANVAS_WIDTH, timecreated=mp.get_time(), text=tostring(chat_message), row=row } +end + +function chat_update() + local ass = assdraw.ass_new() + local chat_ass = '' + local rowsAdded = 0 + local to_add = '' + local incrementRow = 0 + if opts['chatOutputMode'] == CHAT_MODE_CHATROOM and chat_log ~= {} then + local timedelta = mp.get_time() - last_chat_time + if timedelta >= opts['chatTimeout'] then + clear_chat() + end + end + rowsAdded,to_add = process_alert_osd() + if to_add ~= nil and to_add ~= "" then + chat_ass = to_add + end + incrementRow,to_add = process_notification_osd(rowsAdded) + rowsAdded = rowsAdded + incrementRow + if to_add ~= nil and to_add ~= "" then + chat_ass = chat_ass .. to_add + end + + if #chat_log > 0 then + for i = 1, #chat_log do + local to_add = process_chat_item(i,rowsAdded) + if to_add ~= nil and to_add ~= "" then + chat_ass = chat_ass .. to_add + end + end + end + + local xpos = opts['chatLeftMargin'] + local ypos = opts['chatTopMargin'] + chat_ass = "\n".."{\\pos("..xpos..","..ypos..")}".. chat_ass + + if use_alpha_rows_for_chat == false and opts['chatDirectInput'] == true then + local alphawarning_ass = assdraw.ass_new() + alphawarning_ass = "{\\a6}{\\1c&H"..ALPHA_WARNING_TEXT_COLOUR.."}"..opts['alphakey-mode-warning-first-line'].."\n{\\a6}{\\1c&H"..ALPHA_WARNING_TEXT_COLOUR.."}"..opts['alphakey-mode-warning-second-line'] + ass:append(alphawarning_ass) + elseif opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then + ass:append(chat_ass) + ass:append(input_ass()) + else + ass:append(input_ass()) + ass:append(chat_ass) + end + mp.set_osd_ass(CANVAS_WIDTH,CANVAS_HEIGHT, ass.text) +end + +function process_alert_osd() + local rowsCreated = 0 + local stringToAdd = "" + if alert_osd ~= "" and mp.get_time() - last_alert_osd_time < opts['alertTimeout'] and last_alert_osd_time ~= nil then + local messageColour + if alert_osd_mood == MOOD_NEUTRAL then + messageColour = "{\\1c&H"..NEUTRAL_ALERT_TEXT_COLOUR.."}" + elseif alert_osd_mood == MOOD_BAD then + messageColour = "{\\1c&H"..BAD_ALERT_TEXT_COLOUR.."}" + elseif alert_osd_mood == MOOD_GOOD then + messageColour = "{\\1c&H"..GOOD_ALERT_TEXT_COLOUR.."}" + end + local messageString = wordwrapify_string(alert_osd) + local startRow = 0 + if messageString ~= '' and messageString ~= nil then + local toDisplay + rowsCreated = rowsCreated + 1 + messageString = messageColour..messageString + if stringToAdd ~= "" then + stringToAdd = stringToAdd .. format_chatroom(messageString) + else + stringToAdd = format_chatroom(messageString) + end + end + end + return rowsCreated, stringToAdd +end + +function process_notification_osd(startRow) + local rowsCreated = 0 + local startRow = startRow + local stringToAdd = "" + if notification_osd ~= "" and mp.get_time() - last_notification_osd_time < opts['alertTimeout'] and last_notification_osd_time ~= nil then + local messageColour + messageColour = "{\\1c&H"..NOTIFICATION_TEXT_COLOUR.."}" + local messageString + messageString = wordwrapify_string(notification_osd) + messageString = messageColour..messageString + messageString = format_chatroom(messageString) + stringToAdd = messageString + rowsCreated = 1 + end + return rowsCreated, stringToAdd +end + + +function process_chat_item(i, rowsAdded) + if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then + return process_chat_item_chatroom(i, rowsAdded) + elseif opts['chatOutputMode'] == CHAT_MODE_SCROLLING then + return process_chat_item_scrolling(i) + end +end + +function process_chat_item_scrolling(i) + local timecreated = chat_log[i].timecreated + local timedelta = mp.get_time() - timecreated + local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND) + local text = chat_log[i].text + if text ~= '' then + local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) * 1.5 + if xpos > (-1*roughlen) then + local row = chat_log[i].row-1+opts['scrollingFirstRowOffset'] + local ypos = opts['chatTopMargin']+(row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) + return format_scrolling(xpos,ypos,text) + else + chat_log[i].text = '' + end + end +end + +function process_chat_item_chatroom(i, startRow) + local text = chat_log[i].text + if text ~= '' then + local text = wordwrapify_string(text) + local rowNumber = i+startRow-1 + return(format_chatroom(text)) + end +end + +function process_chat_item_subtitle(i) + local timecreated = chat_log[i].timecreated + local timedelta = mp.get_time() - timecreated + local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND) + local text = chat_log[i].text + if text ~= '' then + local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) + if xpos > (-1*roughlen) then + local row = chat_log[i].row + local ypos = row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) + return(format_scrolling(xpos,ypos,text)) + else + chat_log[i].text = '' + end + end +end + +chat_timer=mp.add_periodic_timer(TICK_INTERVAL, chat_update) + +mp.register_script_message('chat', function(e) + add_chat(e) +end) + +-- Chat OSD + +mp.register_script_message('chat-osd-neutral', function(e) + add_chat(e,MOOD_NEUTRAL) +end) + +mp.register_script_message('chat-osd-bad', function(e) + add_chat(e,MOOD_BAD) +end) + +mp.register_script_message('chat-osd-good', function(e) + add_chat(e,MOOD_GOOD) +end) + +-- Alert OSD + +mp.register_script_message('alert-osd-neutral', function(e) + set_alert_osd(e,MOOD_NEUTRAL) +end) + +mp.register_script_message('alert-osd-bad', function(e) + set_alert_osd(e,MOOD_BAD) +end) + +mp.register_script_message('alert-osd-good', function(e) + set_alert_osd(e,MOOD_GOOD) +end) + +-- Notification OSD + +mp.register_script_message('notification-osd-neutral', function(e) + set_notification_osd(e,MOOD_NEUTRAL) +end) + +mp.register_script_message('notification-osd-bad', function(e) + set_notification_osd(e,MOOD_BAD) +end) + +mp.register_script_message('notification-osd-good', function(e) + set_notification_osd(e,MOOD_GOOD) +end) + +-- + +mp.register_script_message('set_syncplayintf_options', function(e) + set_syncplayintf_options(e) +end) + +-- Default options +local utils = require 'mp.utils' +local options = require 'mp.options' +opts = { + + -- All drawing is scaled by this value, including the text borders and the + -- cursor. Change it if you have a high-DPI display. + scale = 1, + -- Set the font used for the REPL and the console. This probably doesn't + -- have to be a monospaced font. + ['chatInputFontFamily'] = 'monospace', + -- Enable/Disable + ['chatInputEnabled'] = true, + ['chatOutputEnabled'] = true, + ['OscVisibilityChangeCompatible'] = false, + -- Set the font size used for the REPL and the console. This will be + -- multiplied by "scale." + ['chatInputRelativeFontSize'] = 14, + ['chatInputFontWeight'] = 1, + ['chatInputFontUnderline'] = false, + ['chatInputFontColor'] = "#000000", + ['chatInputPosition'] = "Top", + ['MaxChatMessageLength'] = 500, + ['chatOutputFontFamily'] = "sans serif", + ['chatOutputFontSize'] = 50, + ['chatOutputFontWeight'] = 1, + ['chatOutputFontUnderline'] = false, + ['chatOutputFontColor'] = "#FFFFFF", + ['chatOutputMode'] = "Chatroom", + ['scrollingFirstRowOffset'] = 2, + -- Can be "Chatroom", "Subtitle" or "Scrolling" style + ['chatMaxLines'] = 7, + ['chatTopMargin'] = 25, + ['chatLeftMargin'] = 20, + ['chatDirectInput'] = true, + -- + ['notificationTimeout'] = 3, + ['alertTimeout'] = 5, + ['chatTimeout'] = 7, + -- + ['inputPromptStartCharacter'] = ">", + ['inputPromptEndCharacter'] = "<", + ['backslashSubstituteCharacter'] = "|", + --Lang: + ['mpv-key-tab-hint'] = "[TAB] to toggle access to alphabet row key shortcuts.", + ['mpv-key-hint'] = "[ENTER] to send message. [ESC] to escape chat mode.", + ['alphakey-mode-warning-first-line'] = "You can temporarily use old mpv bindings with a-z keys.", + ['alphakey-mode-warning-second-line'] = "Press [TAB] to return to Syncplay chat mode.", +} + +function detect_platform() + local o = {} + -- Kind of a dumb way of detecting the platform but whatever + if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then + return 'windows' + elseif mp.get_property_native('options/input-app-events', o) ~= o then + return 'macos' + end + return 'linux' +end + +-- Pick a better default font for Windows and macOS +local platform = detect_platform() +if platform == 'windows' then + opts.font = 'Consolas' +elseif platform == 'macos' then + opts.font = 'Menlo' +end + +-- Apply user-set options +options.read_options(opts) + +-- Escape a string for verbatim display on the OSD +function ass_escape(str) + -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if + -- it isn't followed by a recognised character, so add a zero-width + -- non-breaking space + str = str:gsub('\\', '\\\239\187\191') + str = str:gsub('{', '\\{') + str = str:gsub('}', '\\}') + -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of + -- consecutive newlines + str = str:gsub('\n', '\239\187\191\n') + return str +end + +function update() + return +end + +function input_ass() + if not repl_active then + return "" + end + last_chat_time = mp.get_time() -- to keep chat messages showing while entering input + local bold + if opts['chatInputFontWeight'] < 75 then + bold = 0 + else + bold = 1 + end + local underline = opts['chatInputFontUnderline'] and 1 or 0 + local red = string.sub(opts['chatInputFontColor'],2,3) + local green = string.sub(opts['chatInputFontColor'],4,5) + local blue = string.sub(opts['chatInputFontColor'],6,7) + local fontColor = blue .. green .. red + local style = '{\\r' .. + '\\1a&H00&\\3a&H00&\\4a&H99&' .. + '\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' .. + '\\fn' .. opts['chatInputFontFamily'] .. '\\fs' .. (opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold .. + '\\bord2\\xshad0\\yshad1\\fsp0\\q1}' + + local after_style = '{\\u' .. underline .. '}' + local cheight = opts['chatInputRelativeFontSize'] * FONT_SIZE_MULTIPLIER * 8 + local cglyph = '_' + local before_cur = wordwrapify_string(ass_escape(line:sub(1, cursor - 1))) + local after_cur = wordwrapify_string(ass_escape(line:sub(cursor))) + local secondary_pos = "10,"..tostring(10+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) + + local alignment = 7 + local position = "5,5" + local start_marker = opts['inputPromptStartCharacter'] + local end_marker = "" + if opts['chatInputPosition'] == "Middle" then + alignment = 5 + position = tostring(CANVAS_WIDTH/2)..","..tostring(CANVAS_HEIGHT/2) + secondary_pos = tostring(CANVAS_WIDTH/2)..","..tostring((CANVAS_HEIGHT/2)+20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) + end_marker = "{\\u0}"..opts['inputPromptEndCharacter'] + elseif opts['chatInputPosition'] == "Bottom" then + alignment = 1 + position = tostring(5)..","..tostring(CANVAS_HEIGHT-5) + secondary_pos = "10,"..tostring(CANVAS_HEIGHT-(20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER))) + end + + local osd_help_message = opts['mpv-key-hint'] + if opts['chatDirectInput'] then + osd_help_message = opts['mpv-key-tab-hint'] .. " " .. osd_help_message + end + local help_prompt = '\\N\\n{\\an'..alignment..'\\pos('..secondary_pos..')\\fn' .. opts['chatOutputFontFamily'] .. '\\fs' .. ((opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)/1.25) .. '\\1c&H'..HINT_TEXT_COLOUR..'}' .. osd_help_message + + local firststyle = "{\\an"..alignment.."}{\\pos("..position..")}" + if opts['chatOutputEnabled'] and opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then + firststyle = get_output_style().."{'\\1c&H'"..fontColor.."}" + before_cur = before_cur .. firststyle + after_cur = after_cur .. firststyle + help_prompt = '\\N\\n'..firststyle..'{\\1c&H'..HINT_TEXT_COLOUR..'}' .. osd_help_message .. '\\N\\n' + end + if key_hints_enabled == false then help_prompt = "" end + + return firststyle..style..start_marker.." "..after_style..before_cur..style..cglyph..style..after_style..after_cur..end_marker..help_prompt + +end + +function get_output_style() + local bold + if opts['chatOutputFontWeight'] < 75 then + bold = 0 + else + bold = 1 + end + local underline = opts['chatOutputFontUnderline'] and 1 or 0 + local red = string.sub(opts['chatOutputFontColor'],2,3) + local green = string.sub(opts['chatOutputFontColor'],4,5) + local blue = string.sub(opts['chatOutputFontColor'],6,7) + local fontColor = blue .. green .. red + local style = '{\\r' .. + '\\1a&H00&\\3a&H00&\\4a&H99&' .. + '\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' .. + '\\fn' .. opts['chatOutputFontFamily'] .. '\\fs' .. (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold .. + '\\u' .. underline .. '\\a5\\MarginV=500' .. '}' + + --mp.osd_message("",0) + return style + +end + +function escape() + set_active(false) + clear() +end + +-- Set the REPL visibility (`, Esc) +function set_active(active) + if use_alpha_rows_for_chat == false then active = false end + if active == repl_active then return end + if active then + repl_active = true + insert_mode = false + mp.enable_key_bindings('repl-input', 'allow-hide-cursor+allow-vo-dragging') + else + repl_active = false + mp.disable_key_bindings('repl-input') + end + if default_oscvisibility_state ~= "never" and opts['OscVisibilityChangeCompatible'] == true then + if active then + mp.commandv("script-message", "osc-visibility","never", "no-osd") + else + mp.commandv("script-message", "osc-visibility",default_oscvisibility_state, "no-osd") + end + end +end + +-- Show the repl if hidden and replace its contents with 'text' +-- (script-message-to repl type) +function show_and_type(text) + text = text or '' + + line = text + cursor = line:len() + 1 + insert_mode = false + if repl_active then + update() + else + set_active(true) + end +end + +-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' +-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. +function next_utf8(str, pos) + if pos > str:len() then return pos end + repeat + pos = pos + 1 + until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf + return pos +end + +-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' +-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. + + +-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos' +function prev_utf8(str, pos) + if pos <= 1 then return pos end + repeat + pos = pos - 1 + until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf + return pos +end + +function trim_string(line,maxCharacters) +-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' +-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. + + local str = line + if str == nil or str == "" or str:len() <= maxCharacters then + return str, "" + end + local pos = 0 + local oldPos = -1 + local chars = 0 + + repeat + oldPos = pos + pos = next_utf8(str, pos) + chars = chars + 1 + until pos == oldPos or chars > maxCharacters + return str:sub(1,pos-1), str:sub(pos) +end + +function wordwrapify_string(line) +-- Used to ensure characters wrap on a per-character rather than per-word basis +-- to avoid issues with long filenames, etc. + + local str = line + if str == nil or str == "" then + return "" + end + local newstr = "" + local currentChar = 0 + local nextChar = 0 + local chars = 0 + local maxChars = str:len() + + repeat + nextChar = next_utf8(str, currentChar) + if nextChar == currentChar then + return newstr + end + local charToTest = str:sub(currentChar,nextChar-1) + if charToTest ~= "\\" and charToTest ~= "{" and charToTest ~= "}" and charToTest ~= "%" then + newstr = newstr .. WORDWRAPIFY_MAGICWORD .. str:sub(currentChar,nextChar-1) + else + newstr = newstr .. str:sub(currentChar,nextChar-1) + end + currentChar = nextChar + until currentChar > maxChars + newstr = string.gsub(newstr,opts['backslashSubstituteCharacter'], '\\\239\187\191') -- Workaround for \ escape issues + return newstr +end + + +function trim_input() +-- Naive helper function to find the next UTF-8 character in 'str' after 'pos' +-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. + + local str = line + if str == nil or str == "" or str:len() <= opts['MaxChatMessageLength'] then + return + end + local pos = 0 + local oldPos = -1 + local chars = 0 + + repeat + oldPos = pos + pos = next_utf8(str, pos) + chars = chars + 1 + until pos == oldPos or chars > opts['MaxChatMessageLength'] + line = line:sub(1,pos-1) + if cursor > pos then + cursor = pos + end + return +end + +-- Insert a character at the current cursor position (' '-'~', Shift+Enter) +function handle_char_input(c) + if c == nil then return end + if c == "\\" then c = opts['backslashSubstituteCharacter'] end + if key_hints_enabled and (string.len(line) > 0 or opts['chatDirectInput'] == false) then + key_hints_enabled = false + end + set_active(true) + if insert_mode then + line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor)) + else + line = line:sub(1, cursor - 1) .. c .. line:sub(cursor) + end + cursor = cursor + c:len() + trim_input() + update() +end + +-- Remove the character behind the cursor (Backspace) +function handle_backspace() + if cursor <= 1 then return end + local prev = prev_utf8(line, cursor) + line = line:sub(1, prev - 1) .. line:sub(cursor) + cursor = prev + update() +end + +-- Remove the character in front of the cursor (Del) +function handle_del() + if cursor > line:len() then return end + line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor)) + update() +end + +-- Toggle insert mode (Ins) +function handle_ins() + insert_mode = not insert_mode +end + +--local was_active_before_tab = false + +function handle_tab() + use_alpha_rows_for_chat = not use_alpha_rows_for_chat + if use_alpha_rows_for_chat then + mp.enable_key_bindings('repl-alpha-input') + --set_active(was_active_before_tab) + else + mp.disable_key_bindings('repl-alpha-input') + --was_active_before_tab = repl_active + --set_active(false) + escape() + end +end + +-- Move the cursor to the next character (Right) +function next_char(amount) + cursor = next_utf8(line, cursor) + update() +end + +-- Move the cursor to the previous character (Left) +function prev_char(amount) + cursor = prev_utf8(line, cursor) + update() +end + +-- Clear the current line (Ctrl+C) +function clear() + line = '' + cursor = 1 + insert_mode = false + update() +end + +-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D) +function maybe_exit() + if line == '' then + set_active(false) + end +end + +-- Run the current command and clear the line (Enter) +function handle_enter() + if not repl_active then + set_active(true) + return + end + set_active(false) + + if line == '' then + return + end + key_hints_enabled = false + line = string.gsub(line,"\\", "\\\\") + line = string.gsub(line,"\"", "\\\"") + mp.command('print-text "'..line..'"') + clear() +end + +-- Move the cursor to the beginning of the line (HOME) +function go_home() + cursor = 1 + update() +end + +-- Move the cursor to the end of the line (END) +function go_end() + cursor = line:len() + 1 + update() +end + +-- Delete from the cursor to the end of the line (Ctrl+K) +function del_to_eol() + line = line:sub(1, cursor - 1) + update() +end + +-- Delete from the cursor back to the start of the line (Ctrl+U) +function del_to_start() + line = line:sub(cursor) + cursor = 1 + update() +end + +-- Returns a string of UTF-8 text from the clipboard (or the primary selection) +function get_clipboard(clip) + if platform == 'linux' then + local res = utils.subprocess({ args = { + 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' + } }) + if not res.error then + return res.stdout + end + elseif platform == 'windows' then + local res = utils.subprocess({ args = { + 'powershell', '-NoProfile', '-Command', [[& { + Trap { + Write-Error -ErrorRecord $_ + Exit 1 + } + + $clip = "" + if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) { + $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText + } else { + Add-Type -AssemblyName PresentationCore + $clip = [Windows.Clipboard]::GetText() + } + + $clip = $clip -Replace "`r","" + $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip) + [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length) + }]] + } }) + if not res.error then + return res.stdout + end + elseif platform == 'macos' then + local res = utils.subprocess({ args = { 'pbpaste' } }) + if not res.error then + return res.stdout + end + end + return '' +end + +-- Paste text from the window-system's clipboard. 'clip' determines whether the +-- clipboard or the primary selection buffer is used (on X11 only.) +function paste(clip) + local text = get_clipboard(clip) + local before_cur = line:sub(1, cursor - 1) + local after_cur = line:sub(cursor) + line = before_cur .. text .. after_cur + cursor = cursor + text:len() + trim_input() + update() +end + +-- The REPL has pretty specific requirements for key bindings that aren't +-- really satisified by any of mpv's helper methods, since they must be in +-- their own input section, but they must also raise events on key-repeat. +-- Hence, this function manually creates an input section and puts a list of +-- bindings in it. +function add_repl_bindings(bindings) + local cfg = '' + for i, binding in ipairs(bindings) do + local key = binding[1] + local fn = binding[2] + local name = '__repl_binding_' .. i + mp.add_forced_key_binding(nil, name, fn, 'repeatable') + cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' .. + name .. '\n' + end + mp.commandv('define-section', 'repl-input', cfg, 'force') +end + +function add_repl_alpharow_bindings(bindings) + local cfg = '' + for i, binding in ipairs(bindings) do + local key = binding[1] + local fn = binding[2] + local name = '__repl_alpha_binding_' .. i + mp.add_forced_key_binding(nil, name, fn, 'repeatable') + cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' .. + name .. '\n' + end + mp.commandv('define-section', 'repl-alpha-input', cfg, 'force') + mp.enable_key_bindings('repl-alpha-input') +end + +-- Mapping from characters to mpv key names +local binding_name_map = { + [' '] = 'SPACE', + ['#'] = 'SHARP', +} + +-- List of input bindings. This is a weird mashup between common GUI text-input +-- bindings and readline bindings. +local bindings = { + { 'esc', function() escape() end }, + { 'bs', handle_backspace }, + { 'shift+bs', handle_backspace }, + { 'del', handle_del }, + { 'shift+del', handle_del }, + { 'ins', handle_ins }, + { 'left', function() prev_char() end }, + { 'right', function() next_char() end }, + { 'up', function() clear() end }, + { 'home', go_home }, + { 'end', go_end }, + { 'ctrl+c', clear }, + { 'ctrl+d', maybe_exit }, + { 'ctrl+k', del_to_eol }, + { 'ctrl+l', clear_log_buffer }, + { 'ctrl+u', del_to_start }, + { 'ctrl+v', function() paste(true) end }, + { 'meta+v', function() paste(true) end }, +} +local alpharowbindings = {} +-- Add bindings for all the printable US-ASCII characters from ' ' to '~' +-- inclusive. Note, this is a pretty hacky way to do text input. mpv's input +-- system was designed for single-key key bindings rather than text input, so +-- things like dead-keys and non-ASCII input won't work. This is probably okay +-- though, since all mpv's commands and properties can be represented in ASCII. +for b = (' '):byte(), ('~'):byte() do + local c = string.char(b) + local binding = binding_name_map[c] or c + bindings[#bindings + 1] = {binding, function() handle_char_input(c) end} +end + +function add_alpharowbinding(firstchar,lastchar) + for b = (firstchar):byte(), (lastchar):byte() do + local c = string.char(b) + local alphabinding = binding_name_map[c] or c + alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(c) end} + end +end + +function add_specialalphabindings(charinput) + local alphabindingarray = charinput + for i, alphabinding in ipairs(alphabindingarray) do + alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(alphabinding) end } + end +end + +add_alpharowbinding('a','z') +add_alpharowbinding('A','Z') +add_alpharowbinding('/','/') +add_alpharowbinding(':',':') +add_alpharowbinding('(',')') +add_alpharowbinding('{','}') +add_alpharowbinding(':',';') +add_alpharowbinding('<','>') +add_alpharowbinding(',','.') +add_alpharowbinding('|','|') +add_alpharowbinding('\\','\\') +add_alpharowbinding('?','?') +add_alpharowbinding('[',']') +add_alpharowbinding('#','#') +add_alpharowbinding('~','~') +add_alpharowbinding('\'','\'') +add_alpharowbinding('@','@') + +add_specialalphabindings({'à','è','ì','ò','ù','À','È','Ì','Ò','Ù'}) +add_specialalphabindings({'á', 'é', 'í', 'ó', 'ú', 'ý', 'Á', 'É', 'Í', 'Ó', 'Ú', 'Ý'}) +add_specialalphabindings({'â', 'ê', 'î', 'ô', 'û', 'Â', 'Ê', 'Î', 'Ô', 'Û'}) +add_specialalphabindings({'ã', 'ñ', 'õ', 'Ã', 'Ñ', 'Õ'}) +add_specialalphabindings({'ä', 'ë', 'ï', 'ö', 'ü', 'ÿ', 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Ÿ'}) +add_specialalphabindings({'å', 'Å','æ','Æ','œ','Œ','ç','Ç','ð','Ð','ø','Ø','¿','¡','ß'}) +add_specialalphabindings({'¤','†','×','÷','‡','±','—','–','¶','§','ˆ','˜','«','»','¦','‰','©','®','™'}) +add_specialalphabindings({'ž','Ž'}) +add_specialalphabindings({'ª','Þ','þ','ƒ','µ','°','º','•','„','“','…','¬','¥','£','€','¢','¹','²','³','½','¼','¾'}) +add_specialalphabindings({'·','Ĉ','ĉ','Ĝ','ĝ','Ĥ','ĥ','Ĵ','ĵ','Ŝ','ŝ','Ŭ','ŭ'}) +add_specialalphabindings({'Б','б','В','в','Г','г','Д','д','Е','е','Ё','ё','Ж','ж','З','з'}) +add_specialalphabindings({'И','и','Й','й','К','к','Л','л','М','м','Н','н','О','о','П','п'}) +add_specialalphabindings({'Р','р','С','с','Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ч','ч'}) +add_specialalphabindings({'Ш','ш','Щ','щ','Ъ','ъ','Ы','ы','Ь','ь','Э','э','Ю','ю','Я','я'}) +add_specialalphabindings({'≥','≠'}) + +add_repl_bindings(bindings) + +-- Add a script-message to show the REPL and fill it with the provided text +mp.register_script_message('type', function(text) + show_and_type(text) +end) + +local syncplayintfSet = false +mp.command('print-text ""') + +function readyMpvAfterSettingsKnown() + if syncplayintfSet == false then + local vertical_output_area = CANVAS_HEIGHT-(opts['chatTopMargin']+opts['chatBottomMargin']+((opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)*opts['scrollingFirstRowOffset'])+SCROLLING_ADDITIONAL_BOTTOM_MARGIN) + max_scrolling_rows = math.floor(vertical_output_area/(opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)) + local user_opts = { visibility = "auto", } + opt.read_options(user_opts, "osc") + default_oscvisibility_state = user_opts.visibility + if opts['chatInputEnabled'] == true then + key_hints_enabled = true + mp.add_forced_key_binding('enter', handle_enter) + mp.add_forced_key_binding('kp_enter', handle_enter) + if opts['chatDirectInput'] == true then + add_repl_alpharow_bindings(alpharowbindings) + mp.add_forced_key_binding('tab', handle_tab) + end + end + syncplayintfSet = true + end +end + +function set_syncplayintf_options(input) + --mp.command('print-text "...'..input..'"') + for option, value in string.gmatch(input, "([^ ,=]+)=([^,]+)") do + local valueType = type(opts[option]) + if valueType == "number" then + value = tonumber(value) + elseif valueType == "boolean" then + if value == "True" then + value = true + else + value = false + end + end + opts[option] = value + --mp.command('print-text "'..option.."="..tostring(value).." - "..valueType..'"') + end + chat_format = get_output_style() + readyMpvAfterSettingsKnown() +end \ No newline at end of file diff --git a/resources/user_comment.png b/resources/user_comment.png new file mode 100644 index 0000000..e54ebeb Binary files /dev/null and b/resources/user_comment.png differ diff --git a/syncplay/__init__.py b/syncplay/__init__.py index 2cf5466..d487e0f 100644 --- a/syncplay/__init__.py +++ b/syncplay/__init__.py @@ -1,4 +1,4 @@ -version = '1.5.1' +version = '1.5.2' milestone = 'Yoitsu' -release_number = '51' +release_number = '55' projectURL = 'http://syncplay.pl/' diff --git a/syncplay/client.py b/syncplay/client.py index fa73d9e..8a1b3cd 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -145,8 +145,8 @@ class SyncplayClient(object): def initPlayer(self, player): self._player = player - if not self._player.secondaryOSDSupported: - constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_SECONDARY_OSD_WARNING_DURATION + if not self._player.alertOSDSupported: + constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_ALERT_OSD_WARNING_DURATION self.scheduleAskPlayer() self.__playerReady.callback(player) @@ -205,7 +205,7 @@ class SyncplayClient(object): if pauseChange and paused and currentLength > constants.PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH\ and abs(position - currentLength ) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD: self.playlist.advancePlaylistCheck() - elif pauseChange and self.serverFeatures["readiness"]: + elif pauseChange and self.serverFeatures.has_key("readiness") and self.serverFeatures["readiness"]: if currentLength == 0 or currentLength == -1 or\ not (not self.playlist.notJustChangedPlaylist() and abs(position - currentLength ) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD): pauseChange = self._toggleReady(pauseChange, paused) @@ -546,15 +546,32 @@ class SyncplayClient(object): "sharedPlaylists": utils.meetsMinVersion(self.serverVersion, constants.SHARED_PLAYLIST_MIN_VERSION), "chat": utils.meetsMinVersion(self.serverVersion, constants.CHAT_MIN_VERSION), "readiness": utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION), - "managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION) + "managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION), + "maxChatMessageLength": constants.FALLBACK_MAX_CHAT_MESSAGE_LENGTH, + "maxUsernameLength": constants.FALLBACK_MAX_USERNAME_LENGTH, + "maxRoomNameLength": constants.FALLBACK_MAX_ROOM_NAME_LENGTH, + "maxFilenameLength": constants.FALLBACK_MAX_FILENAME_LENGTH } + if featureList: self.serverFeatures.update(featureList) if not utils.meetsMinVersion(self.serverVersion, constants.SHARED_PLAYLIST_MIN_VERSION): self.ui.showErrorMessage(getMessage("shared-playlists-not-supported-by-server-error").format(constants.SHARED_PLAYLIST_MIN_VERSION, self.serverVersion)) elif not self.serverFeatures["sharedPlaylists"]: self.ui.showErrorMessage(getMessage("shared-playlists-disabled-by-server-error")) + # TODO: Have messages for all unsupported & disabled features + constants.MAX_CHAT_MESSAGE_LENGTH = self.serverFeatures["maxChatMessageLength"] + constants.MAX_USERNAME_LENGTH = self.serverFeatures["maxUsernameLength"] + constants.MAX_ROOM_NAME_LENGTH = self.serverFeatures["maxRoomNameLength"] + constants.MAX_FILENAME_LENGTH = self.serverFeatures["maxFilenameLength"] + constants.MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND = ["MaxChatMessageLength={}".format(constants.MAX_CHAT_MESSAGE_LENGTH), + u"inputPromptStartCharacter={}".format(constants.MPV_INPUT_PROMPT_START_CHARACTER), + u"inputPromptEndCharacter={}".format(constants.MPV_INPUT_PROMPT_END_CHARACTER), + u"backslashSubstituteCharacter={}".format( + constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER)] self.ui.setFeatures(self.serverFeatures) + if self._player: + self._player.setFeatures(self.serverFeatures) def getSanitizedCurrentUserFile(self): if self.userlist.currentUser.file: @@ -583,15 +600,28 @@ class SyncplayClient(object): def getUsername(self): return self.userlist.currentUser.username + def chatIsEnabled(self): + return True + # TODO: Allow chat to be disabled + + def getFeatures(self): + features = dict() + + # Can change during runtime: + features["sharedPlaylists"] = self.sharedPlaylistIsEnabled() # Can change during runtime + features["chat"] = self.chatIsEnabled() # Can change during runtime + + # Static for this version/release of Syncplay: + features["featureList"] = True + features["readiness"] = True + features["managedRooms"] = True + + return features + def setRoom(self, roomName, resetAutoplay=False): self.userlist.currentUser.room = roomName if resetAutoplay: self.resetAutoPlayState() - - def sendChat(self,message): - if self._protocol and self._protocol.logged: - message = utils.truncateText(message,constants.MAX_CHAT_MESSAGE_LENGTH) - self._protocol.sendChatMessage(message) def sendRoom(self): room = self.userlist.currentUser.room @@ -632,8 +662,8 @@ class SyncplayClient(object): if self._protocol and self._protocol.logged: self._protocol.sendList() - def showUserList(self): - self.userlist.showUserList() + def showUserList(self, altUI=None): + self.userlist.showUserList(altUI) def getPassword(self): if self.thisIsPublicServer(): @@ -700,19 +730,30 @@ class SyncplayClient(object): if promptForAction: self.ui.promptFor(getMessage("enter-to-exit-prompt")) - def requireMinServerVersion(minVersion): - def requireMinVersionDecorator(f): + def requireServerFeature(featureRequired): + def requireServerFeatureDecorator(f): @wraps(f) def wrapper(self, *args, **kwds): - if not utils.meetsMinVersion(self.serverVersion,minVersion): - if self.serverVersion != "0.0.0": - self.ui.showErrorMessage(getMessage("not-supported-by-server-error").format(minVersion, self.serverVersion)) - else: - self.ui.showDebugMessage("Tried to check server version too soon (threshold: {})".format(minVersion)) + if self.serverVersion == "0.0.0": + self.ui.showDebugMessage( + "Tried to check server version too soon (testing support for: {})".format(featureRequired)) + return None + if not self.serverFeatures.has_key(featureRequired) or not self.serverFeatures[featureRequired]: + featureName = getMessage(u"feature-{}".format(featureRequired)) + self.ui.showErrorMessage(getMessage("not-supported-by-server-error").format(featureName)) return return f(self, *args, **kwds) return wrapper - return requireMinVersionDecorator + return requireServerFeatureDecorator + + @requireServerFeature("chat") + def sendChat(self,message): + if self._protocol and self._protocol.logged: + message = utils.truncateText(message,constants.MAX_CHAT_MESSAGE_LENGTH) + self._protocol.sendChatMessage(message) + + def sendFeaturesUpdate(self, features): + self._protocol.sendFeaturesUpdate(features) def changePlaylistEnabledState(self, newState): oldState = self.sharedPlaylistIsEnabled() @@ -780,7 +821,7 @@ class SyncplayClient(object): allReadyMessage = getMessage("all-users-ready").format(self.userlist.readyUserCount()) autoplayingMessage = getMessage("autoplaying-notification").format(int(self.autoplayTimeLeft)) countdownMessage = u"{}{}{}".format(allReadyMessage,self._player.osdMessageSeparator, autoplayingMessage) - self.ui.showOSDMessage(countdownMessage, 1, secondaryOSD=True) + self.ui.showOSDMessage(countdownMessage, 1, OSDType=constants.OSD_ALERT, mood=constants.MESSAGE_GOODNEWS) if self.autoplayTimeLeft <= 0: self.setPaused(False) self.stopAutoplayCountdown() @@ -792,11 +833,11 @@ class SyncplayClient(object): self.ui.updateAutoPlayState(False) self.stopAutoplayCountdown() - @requireMinServerVersion(constants.USER_READY_MIN_VERSION) + @requireServerFeature("readiness") def toggleReady(self, manuallyInitiated=True): self._protocol.setReady(not self.userlist.currentUser.isReady(), manuallyInitiated) - @requireMinServerVersion(constants.USER_READY_MIN_VERSION) + @requireServerFeature("readiness") def changeReadyState(self, newState, manuallyInitiated=True): oldState = self.userlist.currentUser.isReady() if newState != oldState: @@ -811,7 +852,12 @@ class SyncplayClient(object): if oldReadyState != isReady: self._warnings.checkReadyStates() - @requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION) + @requireServerFeature("managedRooms") + def setUserFeatures(self, username, features): + self.userlist.setFeatures(username, features) + self.ui.userListChange() + + @requireServerFeature("managedRooms") def createControlledRoom(self, roomName): controlPassword = utils.RandomStringGenerator.generate_room_password() self.lastControlPasswordAttempt = controlPassword @@ -830,7 +876,7 @@ class SyncplayClient(object): else: return "" - @requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION) + @requireServerFeature("managedRooms") def identifyAsController(self, controlPassword): controlPassword = self.stripControlPassword(controlPassword) self.ui.showMessage(getMessage("identifying-as-controller-notification").format(controlPassword)) @@ -924,7 +970,7 @@ class SyncplayClient(object): def _checkIfYouReAloneInTheRoom(self, OSDOnly): if self._userlist.areYouAloneInRoom(): - self._ui.showOSDMessage(getMessage("alone-in-the-room"), constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, secondaryOSD=True) + self._ui.showOSDMessage(getMessage("alone-in-the-room"), constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, OSDType=constants.OSD_ALERT, mood=constants.MESSAGE_BADNEWS) if not OSDOnly: self._ui.showMessage(getMessage("alone-in-the-room"), True) if constants.SHOW_OSD_WARNINGS and not self._warnings["alone-in-the-room"]['timer'].running: @@ -954,8 +1000,10 @@ class SyncplayClient(object): if not self._client._player or self._client.autoplayTimerIsRunning(): return osdMessage = None + messageMood = constants.MESSAGE_GOODNEWS fileDifferencesForRoom = self._userlist.getFileDifferencesForRoom() if not self._userlist.areAllFilesInRoomSame() and fileDifferencesForRoom is not None: + messageMood = constants.MESSAGE_BADNEWS fileDifferencesMessage = getMessage("room-file-differences").format(fileDifferencesForRoom) if self._userlist.currentUser.canControl() and self._userlist.isReadinessSupported(): if self._userlist.areAllUsersInRoomReady(): @@ -970,9 +1018,10 @@ class SyncplayClient(object): if self._userlist.areAllUsersInRoomReady(): osdMessage = getMessage("all-users-ready").format(self._userlist.readyUserCount()) else: + messageMood = constants.MESSAGE_BADNEWS osdMessage = getMessage("not-all-ready").format(self._userlist.usersInRoomNotReady()) if osdMessage: - self._ui.showOSDMessage(osdMessage, constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, secondaryOSD=True) + self._ui.showOSDMessage(osdMessage, constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, OSDType=constants.OSD_ALERT, mood=messageMood) def __displayMessageOnOSD(self, warningName, warningFunction): if constants.OSD_WARNING_MESSAGE_DURATION > self._warnings[warningName]["displayedFor"]: @@ -1001,6 +1050,7 @@ class SyncplayUser(object): self.room = room self.file = file_ self._controller = False + self._features = {} def setFile(self, filename, duration, size, path=None): file_ = { @@ -1054,6 +1104,9 @@ class SyncplayUser(object): def setReady(self, ready): self.ready = ready + def setFeatures(self, features): + self._features = features + class SyncplayUserlist(object): def __init__(self, ui, client): self.currentUser = SyncplayUser() @@ -1132,7 +1185,7 @@ class SyncplayUserlist(object): if differentDuration: differences.append(getMessage("file-difference-duration")) return ", ".join(differences) - def addUser(self, username, room, file_, noMessage=False, isController=None, isReady=None): + def addUser(self, username, room, file_, noMessage=False, isController=None, isReady=None, features={}): if username == self.currentUser.username: if isController is not None: self.currentUser.setControllerStatus(isController) @@ -1143,7 +1196,7 @@ class SyncplayUserlist(object): user.setControllerStatus(isController) self._users[username] = user user.setReady(isReady) - + user.setFeatures(features) if not noMessage: self.__showUserChangeMessage(username, room, file_) self.userListChange(room) @@ -1310,7 +1363,7 @@ class SyncplayUserlist(object): def hasRoomStateChanged(self): return self._roomUsersChanged - def showUserList(self): + def showUserList(self, altUI=None): rooms = {} for user in self._users.itervalues(): if user.room not in rooms: @@ -1320,7 +1373,10 @@ class SyncplayUserlist(object): rooms[self.currentUser.room] = [] rooms[self.currentUser.room].append(self.currentUser) rooms = self.sortList(rooms) - self.ui.showUserList(self.currentUser, rooms) + if altUI: + altUI.showUserList(self.currentUser, rooms) + else: + self.ui.showUserList(self.currentUser, rooms) self._client.autoplayCheck() def clearList(self): @@ -1336,10 +1392,10 @@ class UiManager(object): def __init__(self, client, ui): self._client = client self.__ui = ui - self.lastPrimaryOSDMessage = None - self.lastPrimaryOSDEndTime = None - self.lastSecondaryOSDMessage = None - self.lastSecondaryOSDEndTime = None + self.lastNotificatinOSDMessage = None + self.lastNotificationOSDEndTime = None + self.lastAlertOSDMessage = None + self.lastAlertOSDEndTime = None self.lastError = "" def setPlaylist(self, newPlaylist, newIndexFilename=None): @@ -1358,8 +1414,16 @@ class UiManager(object): if constants.DEBUG_MODE and message.rstrip(): sys.stderr.write("{}{}\n".format(time.strftime(constants.UI_TIME_FORMAT, time.localtime()).decode('utf-8'),message.rstrip())) - def showMessage(self, message, noPlayer=False, noTimestamp=False, secondaryOSD=False): - if not noPlayer: self.showOSDMessage(message, duration=constants.OSD_DURATION, secondaryOSD=secondaryOSD) + def showChatMessage(self, username, userMessage): + messageString = u"<{}> {}".format(username, userMessage) + if self._client._player.chatOSDSupported and self._client._config["chatOutputEnabled"]: + self._client._player.displayChatMessage(username,userMessage) + else: + self.showOSDMessage(messageString, duration=constants.OSD_DURATION) + self.__ui.showMessage(messageString) + + def showMessage(self, message, noPlayer=False, noTimestamp=False, OSDType=constants.OSD_NOTIFICATION,mood=constants.MESSAGE_NEUTRAL): + if not noPlayer: self.showOSDMessage(message, duration=constants.OSD_DURATION, OSDType=OSDType, mood=mood) self.__ui.showMessage(message, noTimestamp) def updateAutoPlayState(self, newState): @@ -1368,28 +1432,28 @@ class UiManager(object): def showUserList(self, currentUser, rooms): self.__ui.showUserList(currentUser, rooms) - def showOSDMessage(self, message, duration=constants.OSD_DURATION, secondaryOSD=False): + def showOSDMessage(self, message, duration=constants.OSD_DURATION, OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL): autoplayConditionsMet = self._client.autoplayConditionsMet() - if secondaryOSD and not constants.SHOW_OSD_WARNINGS and not self._client.autoplayTimerIsRunning(): + if OSDType == constants.OSD_ALERT and not constants.SHOW_OSD_WARNINGS and not self._client.autoplayTimerIsRunning(): return if not self._client._player: return if constants.SHOW_OSD and self._client and self._client._player: - if not self._client._player.secondaryOSDSupported: - if secondaryOSD: - self.lastSecondaryOSDMessage = message + if not self._client._player.alertOSDSupported: + if OSDType == constants.OSD_ALERT: + self.lastAlertOSDMessage = message if autoplayConditionsMet: - self.lastSecondaryOSDEndTime = time.time() + 1.0 + self.lastAlertOSDEndTime = time.time() + 1.0 else: - self.lastSecondaryOSDEndTime = time.time() + constants.NO_SECONDARY_OSD_WARNING_DURATION - if self.lastPrimaryOSDEndTime and time.time() < self.lastPrimaryOSDEndTime: - message = u"{}{}{}".format(message, self._client._player.osdMessageSeparator, self.lastPrimaryOSDMessage) + self.lastAlertOSDEndTime = time.time() + constants.NO_ALERT_OSD_WARNING_DURATION + if self.lastNotificationOSDEndTime and time.time() < self.lastNotificationOSDEndTime: + message = u"{}{}{}".format(message, self._client._player.osdMessageSeparator, self.lastNotificatinOSDMessage) else: - self.lastPrimaryOSDMessage = message - self.lastPrimaryOSDEndTime = time.time() + constants.OSD_DURATION - if self.lastSecondaryOSDEndTime and time.time() < self.lastSecondaryOSDEndTime: - message = u"{}{}{}".format(self.lastSecondaryOSDMessage, self._client._player.osdMessageSeparator, message) - self._client._player.displayMessage(message, int(duration * 1000), secondaryOSD) + self.lastNotificatinOSDMessage = message + self.lastNotificationOSDEndTime = time.time() + constants.OSD_DURATION + if self.lastAlertOSDEndTime and time.time() < self.lastAlertOSDEndTime: + message = u"{}{}{}".format(self.lastAlertOSDMessage, self._client._player.osdMessageSeparator, message) + self._client._player.displayMessage(message, int(duration * 1000), OSDType, mood) def setControllerStatus(self, username, isController): self.__ui.setControllerStatus(username, isController) @@ -1411,6 +1475,9 @@ class UiManager(object): def updateRoomName(self, room=""): self.__ui.updateRoomName(room) + def executeCommand(self, command): + self.__ui.executeCommand(command) + def drop(self): self.__ui.drop() @@ -1799,7 +1866,7 @@ class FileSwitchManager(object): if os.path.isfile(filepath): return filepath - if highPriority and self.folderSearchEnabled: + if highPriority and self.folderSearchEnabled and self.mediaDirectories is not None: directoryList = self.mediaDirectories # Spin up hard drives to prevent premature timeout randomFilename = u"RandomFile"+unicode(random.randrange(10000, 99999))+u".txt" diff --git a/syncplay/constants.py b/syncplay/constants.py index 02913f4..f5f0f41 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -1,8 +1,9 @@ +# coding:utf8 # You might want to change these DEFAULT_PORT = 8999 OSD_DURATION = 3.0 OSD_WARNING_MESSAGE_DURATION = 5.0 -NO_SECONDARY_OSD_WARNING_DURATION = 13.0 +NO_ALERT_OSD_WARNING_DURATION = 13.0 MPC_OSD_POSITION = 1 #Right corner, 1 for left MPLAYER_OSD_LEVEL = 1 UI_TIME_FORMAT = "[%X] " @@ -56,12 +57,23 @@ PLAYLIST_MAX_CHARACTERS = 10000 PLAYLIST_MAX_ITEMS = 250 MAXIMUM_TAB_WIDTH = 350 TAB_PADDING = 30 +DEFAULT_WINDOWS_MONOSPACE_FONT = "Consolas" +DEFAULT_OSX_MONOSPACE_FONT = "Menlo" +FALLBACK_MONOSPACE_FONT = "Monospace" +DEFAULT_CHAT_FONT_SIZE = 24 +DEFAULT_CHAT_INPUT_FONT_COLOR = "#FFFF00" +DEFAULT_CHAT_OUTPUT_FONT_COLOR = "#FFFF00" +DEFAULT_CHAT_FONT_WEIGHT = 1 -# Maximum character lengths (for client and server) -MAX_CHAT_MESSAGE_LENGTH = 50 # Number of displayed characters -MAX_USERNAME_LENGTH = 16 # Number of displayed characters +# Max numbers are used by server (and client pre-connection). Once connected client gets values from server featureList (or uses 'fallback' versions for old servers) +MAX_CHAT_MESSAGE_LENGTH = 125 # Number of displayed characters +MAX_USERNAME_LENGTH = 20 # Number of displayed characters MAX_ROOM_NAME_LENGTH = 35 # Number of displayed characters MAX_FILENAME_LENGTH = 250 # Number of displayed characters +FALLBACK_MAX_CHAT_MESSAGE_LENGTH = 50 # Number of displayed characters +FALLBACK_MAX_USERNAME_LENGTH = 16 # Number of displayed characters +FALLBACK_MAX_ROOM_NAME_LENGTH = 35 # Number of displayed characters +FALLBACK_MAX_FILENAME_LENGTH = 250 # Number of displayed characters # Options for the File Switch feature: FOLDER_SEARCH_FIRST_FILE_TIMEOUT = 25.0 # Secs - How long to wait to find the first file in folder search (to take account of HDD spin up) @@ -133,7 +145,7 @@ MPC_ICONPATH = "mpc-hc.png" MPC64_ICONPATH = "mpc-hc64.png" MPC_BE_ICONPATH = "mpc-be.png" -MPV_ERROR_MESSAGES_TO_REPEAT = ['[ytdl_hook] Your version of youtube-dl is too old', '[ytdl_hook] youtube-dl failed', 'Failed to recognize file format.'] +MPV_ERROR_MESSAGES_TO_REPEAT = ['[ytdl_hook] Your version of youtube-dl is too old', '[ytdl_hook] youtube-dl failed', 'Failed to recognize file format.', '[syncplayintf] Lua error'] #Changing these is usually not something you're looking for PLAYER_ASK_DELAY = 0.1 @@ -179,6 +191,19 @@ MPV_ARGS = ['--force-window', '--idle', '--hr-seek=always', '--keep-open'] MPV_SLAVE_ARGS = ['--msg-level=all=error,cplayer=info,term-msg=info', '--input-terminal=no', '--input-file=/dev/stdin'] MPV_SLAVE_ARGS_NEW = ['--term-playing-msg=\nANS_filename=${filename}\nANS_length=${=duration:${=length:0}}\nANS_path=${path}\n', '--terminal=yes'] MPV_NEW_VERSION = False +MPV_OSC_VISIBILITY_CHANGE_VERSION = False +MPV_INPUT_PROMPT_START_CHARACTER = u"〉" +MPV_INPUT_PROMPT_END_CHARACTER = u" 〈" +MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER = u"\" +MPV_SYNCPLAYINTF_OPTIONS_TO_SEND = ["chatInputEnabled","chatInputFontFamily", "chatInputRelativeFontSize", "chatInputFontWeight","chatInputFontUnderline", + "chatInputFontColor", "chatInputPosition","chatOutputFontFamily","chatOutputRelativeFontSize", + "chatOutputFontWeight","chatOutputFontUnderline","chatOutputMode","chatMaxLines", + "chatTopMargin","chatLeftMargin","chatBottomMargin","chatDirectInput", + "notificationTimeout","alertTimeout","chatTimeout","chatOutputEnabled"] + +MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND = ["MaxChatMessageLength={}".format(MAX_CHAT_MESSAGE_LENGTH),u"inputPromptStartCharacter={}".format(MPV_INPUT_PROMPT_START_CHARACTER),u"inputPromptEndCharacter={}".format(MPV_INPUT_PROMPT_END_CHARACTER),u"backslashSubstituteCharacter={}".format(MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER)] +# Note: Constants updated in client.py->checkForFeatureSupport +MPV_SYNCPLAYINTF_LANGUAGE_TO_SEND = ["mpv-key-tab-hint","mpv-key-hint", "alphakey-mode-warning-first-line", "alphakey-mode-warning-second-line"] VLC_SLAVE_ARGS = ['--extraintf=luaintf', '--lua-intf=syncplay', '--no-quiet', '--no-input-fast-seek', '--play-and-pause', '--start-time=0'] VLC_SLAVE_MACOS_ARGS = ['--verbose=2', '--no-file-logging'] @@ -200,6 +225,9 @@ UNPAUSE_IFALREADYREADY_MODE = "IfAlreadyReady" UNPAUSE_IFOTHERSREADY_MODE = "IfOthersReady" UNPAUSE_IFMINUSERSREADY_MODE = "IfMinUsersReady" UNPAUSE_ALWAYS_MODE = "Always" +INPUT_POSITION_TOP = "Top" +INPUT_POSITION_MIDDLE = "Middle" +INPUT_POSITION_BOTTOM = "Bottom" VLC_EOF_DURATION_THRESHOLD = 2.0 @@ -217,6 +245,17 @@ FILEITEM_SWITCH_FILE_SWITCH = 1 FILEITEM_SWITCH_STREAM_SWITCH = 2 PLAYLISTITEM_CURRENTLYPLAYING_ROLE = 3 +MESSAGE_NEUTRAL = "neutral" +MESSAGE_BADNEWS = "bad" +MESSAGE_GOODNEWS = "good" + +OSD_NOTIFICATION = "notification" # Also known as PrimaryOSD +OSD_ALERT = "alert" # Also known as SecondaryOSD +OSD_CHAT = "chat" + +CHATROOM_MODE = "Chatroom" +SCROLLING_MODE = "Scrolling" + SYNCPLAY_UPDATE_URL = u"http://syncplay.pl/checkforupdate?{}" # Params SYNCPLAY_DOWNLOAD_URL = "http://syncplay.pl/download/" SYNCPLAY_PUBLIC_SERVER_LIST_URL = u"http://syncplay.pl/listpublicservers?{}" # Params @@ -230,4 +269,4 @@ OS_WINDOWS = "win" OS_LINUX = "linux" OS_MACOS = "darwin" OS_BSD = "freebsd" -OS_DRAGONFLY = "dragonfly" \ No newline at end of file +OS_DRAGONFLY = "dragonfly" diff --git a/syncplay/messages.py b/syncplay/messages.py index a62326d..4ef13b2 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -66,4 +66,6 @@ def getMessage(type_, locale=None): if messages["en"].has_key(type_): return unicode(messages["en"][type_]) else: - raise KeyError(type_) + print u"WARNING: Cannot find message '{}'!".format(type_) + return "!{}".format(type_) # TODO: Remove + #raise KeyError(type_) \ No newline at end of file diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index 819ca79..5f729b9 100644 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -17,23 +17,23 @@ de = { "connected-successful-notification" : u"Erfolgreich mit Server verbunden", "retrying-notification" : u"%s, versuche erneut in %d Sekunden...", # Seconds - "rewind-notification" : u"Zurückgespult wegen Zeitdifferenz mit <{}>", # User - "fastforward-notification" : u"Vorgespult wegen Zeitdifferenz mit <{}>", # User - "slowdown-notification" : u"Verlangsamt wegen Zeitdifferenz mit <{}>", # User + "rewind-notification" : u"Zurückgespult wegen Zeitdifferenz mit {}", # User + "fastforward-notification" : u"Vorgespult wegen Zeitdifferenz mit {}", # User + "slowdown-notification" : u"Verlangsamt wegen Zeitdifferenz mit {}", # User "revert-notification" : u"Normalgeschwindigkeit", - "pause-notification" : u"<{}> pausierte", # User - "unpause-notification" : u"<{}> startete", # User - "seek-notification" : u"<{}> sprang von {} nach {}", # User, from time, to time + "pause-notification" : u"{} pausierte", # User + "unpause-notification" : u"{} startete", # User + "seek-notification" : u"{} sprang von {} nach {}", # User, from time, to time "current-offset-notification" : u"Aktueller Offset: {} Sekunden", # Offset "media-directory-list-updated-notification" : u"Syncplay media directories have been updated.", # TODO: Translate - "room-join-notification" : u"<{}> hat den Raum '{}' betreten", # User - "left-notification" : u"<{}> ist gegangen", # User - "left-paused-notification" : u"<{}> ist gegangen, <{}> pausierte", # User who left, User who paused - "playing-notification" : u"<{}> spielt '{}' ({})", # User, file, duration + "room-join-notification" : u"{} hat den Raum '{}' betreten", # User + "left-notification" : u"{} ist gegangen", # User + "left-paused-notification" : u"{} ist gegangen, {} pausierte", # User who left, User who paused + "playing-notification" : u"{} spielt '{}' ({})", # User, file, duration "playing-notification/room-addendum" : u" in Raum: '{}'", # Room "not-all-ready" : u"Noch nicht bereit: {}", # Usernames @@ -44,11 +44,11 @@ de = { "autoplaying-notification" : u"Starte in {}...", # Number of seconds until playback will start "identifying-as-controller-notification" : u"Identifiziere als Raumleiter mit Passwort '{}'...", # TODO: find a better translation to "room operator" - "failed-to-identify-as-controller-notification" : u"<{}> konnte sich nicht als Raumleiter identifizieren.", - "authenticated-as-controller-notification" : u"<{}> authentifizierte sich als Raumleiter", + "failed-to-identify-as-controller-notification" : u"{} konnte sich nicht als Raumleiter identifizieren.", + "authenticated-as-controller-notification" : u"{} authentifizierte sich als Raumleiter", "created-controlled-room-notification" : u"Gesteuerten Raum '{}' mit Passwort '{}' erstellt. Bitte diese Informationen für die Zukunft aufheben!", # RoomName, operatorPassword - "file-different-notification" : u"Deine Datei scheint sich von <{}>s zu unterscheiden", # User + "file-different-notification" : u"Deine Datei scheint sich von {}s zu unterscheiden", # User "file-differences-notification" : u"Deine Datei unterscheidet sich auf folgende Art: {}", "room-file-differences" : u"Unterschiedlich in: {}", # File differences (filename, size, and/or duration) "file-difference-filename" : u"Name", @@ -225,6 +225,7 @@ de = { "messages-label" : u"Nachrichten", "messages-osd-title" : u"OSD-(OnScreenDisplay)-Einstellungen", "messages-other-title" : u"Weitere Display-Einstellungen", + "chat-label" : u"Chat", # TODO: Translate "privacy-label" : u"Privatsphäre", "privacy-title" : u"Privatsphäreneinstellungen", "unpause-title" : u"Wenn du Play drückst, auf Bereit setzen und:", @@ -234,6 +235,27 @@ de = { "unpause-always" : u"Immer wiedergeben", "syncplay-trusteddomains-title": u"Trusted domains (for streaming services and hosted content)", # TODO: Translate into German + "chat-title": u"Chat message input", # TODO: Translate + "chatinputenabled-label": u"Enable chat input via mpv (using enter key)", # TODO: Translate + "chatdirectinput-label" : u"Allow instant chat input (bypass having to press enter key to chat)", # TODO: Translate + "chatinputfont-label": u"Chat input font", # TODO: Translate + "chatfont-label": u"Set font", # TODO: Translate + "chatcolour-label": u"Set colour", # TODO: Translate + "chatinputposition-label": u"Position of message input area in mpv", # TODO: Translate + "chat-top-option": u"Top", # TODO: Translate + "chat-middle-option": u"Middle", # TODO: Translate + "chat-bottom-option": u"Bottom", # TODO: Translate + "chatoutputfont-label": u"Chat output font", # TODO: Translate + "chatoutputenabled-label": u"Enable chat output in media player (mpv only for now)", # TODO: Translate + "chatoutputposition-label": u"Output mode", # TODO: Translate + "chat-chatroom-option": u"Chatroom style", # TODO: Translate + "chat-scrolling-option": u"Scrolling style", # TODO: Translate + + "mpv-key-tab-hint": u"[TAB] to toggle access to alphabet row key shortcuts.", # TODO: Translate + "mpv-key-hint": u"[ENTER] to send message. [ESC] to escape chat mode.", # TODO: Translate + "alphakey-mode-warning-first-line": u"You can temporarily use old mpv bindings with a-z keys.", # TODO: Translate + "alphakey-mode-warning-second-line": u"Press [TAB] to return to Syncplay chat mode.", # TODO: Translate + "help-label" : u"Hilfe", "reset-label" : u"Standardwerte zurücksetzen", "run-label" : u"Syncplay starten", @@ -357,6 +379,22 @@ de = { "unpause-ifminusersready-tooltip" : u"Wenn du Play drückst und nicht bereit bist, wird nur gestartet, wenn die minimale Anzahl anderer Benutzer bereit ist.", "trusteddomains-arguments-tooltip" : u"Domains that it is okay for Syncplay to automatically switch to when shared playlists is enabled.", # TODO: Translate into German + "chatinputenabled-tooltip": u"Enable chat input in mpv (press enter to chat, enter to send, escape to cancel)", # TODO: Translate + "chatdirectinput-tooltip" : u"Skip having to press 'enter' to go into chat input mode in mpv. Press TAB in mpv to temporarily disable this feature.", # TODO: Translate + "font-label-tooltip": u"Font used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", # TODO: Translate + "set-input-font-tooltip": u"Font family used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", # TODO: Translate + "set-input-colour-tooltip": u"Font colour used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", # TODO: Translate + "chatinputposition-tooltip": u"Location in mpv where chat input text will appear when you press enter and type.", # TODO: Translate + "chatinputposition-top-tooltip": u"Place chat input at top of mpv window.", # TODO: Translate + "chatinputposition-middle-tooltip": u"Place chat input in dead centre of mpv window.", # TODO: Translate + "chatinputposition-bottom-tooltip": u"Place chat input at bottom of mpv window.", # TODO: Translate + "chatoutputenabled-tooltip": u"Show chat messages in OSD (if supported by media player).", # TODO: Translate + "font-output-label-tooltip": u"Chat output font.", # TODO: Translate + "set-output-font-tooltip": u"Font used for when displaying chat messages.", # TODO: Translate + "chatoutputmode-tooltip": u"How chat messages are displayed.", # TODO: Translate + "chatoutputmode-chatroom-tooltip": u"Display new lines of chat directly below previous line.", # TODO: Translate + "chatoutputmode-scrolling-tooltip": u"Scroll chat text from right to left.", # TODO: Translate + "help-tooltip" : u"Öffnet Hilfe auf syncplay.pl [Englisch]", "reset-tooltip" : u"Alle Einstellungen auf Standardwerte zurücksetzen.", "update-server-list-tooltip" : u"Mit syncplay.pl verbinden um die Liste öffentlicher Server zu aktualisieren.", @@ -420,6 +458,7 @@ de = { "addurlstoplaylist-menu-label" : u"Add URL(s) to bottom of playlist", "editplaylist-menu-label": u"Edit playlist", + "open-containing-folder": u"Open folder containing this file", "addusersfiletoplaylist-menu-label" : u"Add {} file to playlist", # item owner indicator "addusersstreamstoplaylist-menu-label" : u"Add {} stream to playlist", # item owner indicator "openusersstream-menu-label" : u"Open {} stream", # [username]'s diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index 18476cb..f1926b4 100644 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -17,23 +17,23 @@ en = { "connected-successful-notification" : "Successfully connected to server", "retrying-notification" : "%s, Retrying in %d seconds...", # Seconds - "rewind-notification" : "Rewinded due to time difference with <{}>", # User - "fastforward-notification" : "Fast-forwarded due to time difference with <{}>", # User - "slowdown-notification" : "Slowing down due to time difference with <{}>", # User + "rewind-notification" : "Rewinded due to time difference with {}", # User + "fastforward-notification" : "Fast-forwarded due to time difference with {}", # User + "slowdown-notification" : "Slowing down due to time difference with {}", # User "revert-notification" : "Reverting speed back to normal", - "pause-notification" : u"<{}> paused", # User - "unpause-notification" : u"<{}> unpaused", # User - "seek-notification" : u"<{}> jumped from {} to {}", # User, from time, to time + "pause-notification" : u"{} paused", # User + "unpause-notification" : u"{} unpaused", # User + "seek-notification" : u"{} jumped from {} to {}", # User, from time, to time "current-offset-notification" : "Current offset: {} seconds", # Offset "media-directory-list-updated-notification" : u"Syncplay media directories have been updated.", - "room-join-notification" : u"<{}> has joined the room: '{}'", # User - "left-notification" : u"<{}> has left", # User - "left-paused-notification" : u"<{}> left, <{}> paused", # User who left, User who paused - "playing-notification" : u"<{}> is playing '{}' ({})", # User, file, duration + "room-join-notification" : u"{} has joined the room: '{}'", # User + "left-notification" : u"{} has left", # User + "left-paused-notification" : u"{} left, {} paused", # User who left, User who paused + "playing-notification" : u"{} is playing '{}' ({})", # User, file, duration "playing-notification/room-addendum" : u" in room: '{}'", # Room "not-all-ready" : u"Not ready: {}", # Usernames @@ -44,11 +44,11 @@ en = { "autoplaying-notification" : u"Auto-playing in {}...", # Number of seconds until playback will start "identifying-as-controller-notification" : u"Identifying as room operator with password '{}'...", - "failed-to-identify-as-controller-notification" : u"<{}> failed to identify as a room operator.", - "authenticated-as-controller-notification" : u"<{}> authenticated as a room operator", + "failed-to-identify-as-controller-notification" : u"{} failed to identify as a room operator.", + "authenticated-as-controller-notification" : u"{} authenticated as a room operator", "created-controlled-room-notification" : u"Created managed room '{}' with password '{}'. Please save this information for future reference!", # RoomName, operatorPassword - "file-different-notification" : "File you are playing appears to be different from <{}>'s", # User + "file-different-notification" : "File you are playing appears to be different from {}'s", # User "file-differences-notification" : u"Your file differs in the following way(s): {}", # Differences "room-file-differences" : u"File differences: {}", # File differences (filename, size, and/or duration) "file-difference-filename" : u"name", @@ -133,7 +133,12 @@ en = { "vlc-failed-noscript": "VLC has reported that the syncplay.lua interface script has not been installed. Please refer to http://syncplay.pl/LUA/ for instructions.", "vlc-failed-versioncheck": "This version of VLC is not supported by Syncplay.", - "not-supported-by-server-error" : "This feature is not supported by the server. The feature requires a server running Syncplay {}+, but the server is running Syncplay {}.", #minVersion, serverVersion + "feature-sharedPlaylists" : u"shared playlists", # used for not-supported-by-server-error + "feature-chat" : u"chat", # used for not-supported-by-server-error + "feature-readiness" : u"readiness", # used for not-supported-by-server-error + "feature-managedRooms" : u"managed rooms", # used for not-supported-by-server-error + + "not-supported-by-server-error" : u"The {} feature is not supported by this server..", #feature "shared-playlists-not-supported-by-server-error" : "The shared playlists feature may not be supported by the server. To ensure that it works correctly requires a server running Syncplay {}+, but the server is running Syncplay {}.", #minVersion, serverVersion "shared-playlists-disabled-by-server-error" : "The shared playlist feature has been disabled in the server configuration. To use this feature you will need to connect to a different server.", @@ -225,6 +230,7 @@ en = { "messages-label" : "Messages", "messages-osd-title" : "On-screen Display settings", "messages-other-title" : "Other display settings", + "chat-label" : u"Chat", "privacy-label" : "Privacy", # Currently unused, but will be brought back if more space is needed in Misc tab "privacy-title" : "Privacy settings", "unpause-title" : u"If you press play, set as ready and:", @@ -233,6 +239,27 @@ en = { "unpause-ifminusersready-option" : u"Unpause if already ready or if all others ready and min users ready", "unpause-always" : u"Always unpause", "syncplay-trusteddomains-title": u"Trusted domains (for streaming services and hosted content)", + + "chat-title" : u"Chat message input", + "chatinputenabled-label" : u"Enable chat input via mpv", + "chatdirectinput-label" : u"Allow instant chat input (bypass having to press enter key to chat)", + "chatinputfont-label" : u"Chat input font", + "chatfont-label" : u"Set font", + "chatcolour-label" : u"Set colour", + "chatinputposition-label" : u"Position of message input area in mpv", + "chat-top-option" : u"Top", + "chat-middle-option" : u"Middle", + "chat-bottom-option" : u"Bottom", + "chatoutputfont-label": u"Chat output font", + "chatoutputenabled-label": u"Enable chat output in media player (mpv only for now)", + "chatoutputposition-label": u"Output mode", + "chat-chatroom-option": u"Chatroom style", + "chat-scrolling-option": u"Scrolling style", + + "mpv-key-tab-hint": u"[TAB] to toggle access to alphabet row key shortcuts.", + "mpv-key-hint": u"[ENTER] to send message. [ESC] to escape chat mode.", + "alphakey-mode-warning-first-line": u"You can temporarily use old mpv bindings with a-z keys.", + "alphakey-mode-warning-second-line": u"Press [TAB] to return to Syncplay chat mode.", "help-label" : "Help", "reset-label" : "Restore defaults", @@ -355,6 +382,22 @@ en = { "unpause-ifminusersready-tooltip" : u"If you press unpause when not ready, it will only unpause if others are ready and minimum users threshold is met.", "trusteddomains-arguments-tooltip" : u"Domains that it is okay for Syncplay to automatically switch to when shared playlists is enabled.", + "chatinputenabled-tooltip" : u"Enable chat input in mpv (press enter to chat, enter to send, escape to cancel)", + "chatdirectinput-tooltip" : u"Skip having to press 'enter' to go into chat input mode in mpv. Press TAB in mpv to temporarily disable this feature.", + "font-label-tooltip" : u"Font used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", + "set-input-font-tooltip" : u"Font family used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", + "set-input-colour-tooltip" : u"Font colour used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", + "chatinputposition-tooltip" : u"Location in mpv where chat input text will appear when you press enter and type.", + "chatinputposition-top-tooltip" : u"Place chat input at top of mpv window.", + "chatinputposition-middle-tooltip" : u"Place chat input in dead centre of mpv window.", + "chatinputposition-bottom-tooltip" : u"Place chat input at bottom of mpv window.", + "chatoutputenabled-tooltip": u"Show chat messages in OSD (if supported by media player).", + "font-output-label-tooltip": u"Chat output font.", + "set-output-font-tooltip": u"Font used for when displaying chat messages.", + "chatoutputmode-tooltip": u"How chat messages are displayed.", + "chatoutputmode-chatroom-tooltip": u"Display new lines of chat directly below previous line.", + "chatoutputmode-scrolling-tooltip": u"Scroll chat text from right to left.", + "help-tooltip" : "Opens the Syncplay.pl user guide.", "reset-tooltip" : "Reset all settings to the default configuration.", "update-server-list-tooltip" : u"Connect to syncplay.pl to update list of public servers.", diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index 8130c95..0311f3c 100644 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -17,23 +17,23 @@ ru = { "connected-successful-notification" : u"Соединение с сервером установлено", "retrying-notification" : u"%s, следующая попытка через %d секунд(ы)...", # Seconds - "rewind-notification" : u"Перемотано из-за разницы во времени с <{}>", # User - "fastforward-notification" : u"Ускорено из-за разницы во времени с <{}>", # User - "slowdown-notification" : u"Воспроизведение замедлено из-за разницы во времени с <{}>", # User + "rewind-notification" : u"Перемотано из-за разницы во времени с {}", # User + "fastforward-notification" : u"Ускорено из-за разницы во времени с {}", # User + "slowdown-notification" : u"Воспроизведение замедлено из-за разницы во времени с {}", # User "revert-notification" : u"Возвращаемся к нормальной скорости воспроизведения", - "pause-notification" : u"<{}> приостановил воспроизведение", # User - "unpause-notification" : u"<{}> возобновил воспроизведение", # User - "seek-notification" : u"<{}> перемотал с {} на {}", # User, from time, to time + "pause-notification" : u"{} приостановил воспроизведение", # User + "unpause-notification" : u"{} возобновил воспроизведение", # User + "seek-notification" : u"{} перемотал с {} на {}", # User, from time, to time "current-offset-notification" : u"Текущее смещение: {} секунд(ы)", # Offset "media-directory-list-updated-notification" : u"Папки воспроизведения обновлены.", - "room-join-notification" : u"<{}> зашел в комнату: '{}'", # User - "left-notification" : u"<{}> покинул комнату", # User - "left-paused-notification" : u"<{}> покинул комнату, <{}> приостановил воспроизведение", # User who left, User who paused - "playing-notification" : u"<{}> включил '{}' ({})", # User, file, duration + "room-join-notification" : u"{} зашел в комнату: '{}'", # User + "left-notification" : u"{} покинул комнату", # User + "left-paused-notification" : u"{} покинул комнату, {} приостановил воспроизведение", # User who left, User who paused + "playing-notification" : u"{} включил '{}' ({})", # User, file, duration "playing-notification/room-addendum" : u" в комнате: '{}'", # Room "not-all-ready" : u"Не готовы: {}", # Usernames @@ -44,11 +44,11 @@ ru = { "autoplaying-notification" : u"Автовоспроизведение через {}...", # Number of seconds until playback will start "identifying-as-controller-notification" : u"Идентификация как оператора комнаты с паролем '{}'...", - "failed-to-identify-as-controller-notification" : u"<{}> не прошел идентификацию в качестве оператора комнаты.", - "authenticated-as-controller-notification" : u"<{}> вошел как оператор комнаты.", + "failed-to-identify-as-controller-notification" : u"{} не прошел идентификацию в качестве оператора комнаты.", + "authenticated-as-controller-notification" : u"{} вошел как оператор комнаты.", "created-controlled-room-notification" : u"Создана управляемая комната '{}' с паролем '{}'. Сохраните эти данные!", # RoomName, operatorPassword - "file-different-notification" : u"Вероятно, файл, который Вы смотрите, отличается от того, который смотрит <{}>.", # User + "file-different-notification" : u"Вероятно, файл, который Вы смотрите, отличается от того, который смотрит {}.", # User "file-differences-notification" : u"Ваш файл отличается: {}", # Differences "room-file-differences" : u"Несовпадения файла: {}", # File differences (filename, size, and/or duration) "file-difference-filename" : u"имя", @@ -135,7 +135,13 @@ ru = { "vlc-failed-versioncheck" : u"Данная версия VLC не поддерживается Syncplay. Пожалуйста, используйте VLC версии 2 или выше.", "vlc-failed-other" : u"Во время загрузки скрипта интерфейса syncplay.lua в VLC произошла следующая ошибка: {}", # Syncplay Error - "not-supported-by-server-error" : u"Эта возможность не поддерживается сервером. Требуется сервер Syncplay {}+, вы подключены к серверу Syncplay {}.", #minVersion, serverVersion + "feature-sharedPlaylists": u"shared playlists", # used for not-supported-by-server-error # TODO: Translate + "feature-chat": u"chat", # used for not-supported-by-server-error # TODO: Translate + "feature-readiness": u"readiness", # used for not-supported-by-server-error # TODO: Translate + "feature-managedRooms": u"managed rooms", # used for not-supported-by-server-error # TODO: Translate + + "not-supported-by-server-error": u"The {} feature is not supported by this server..", # feature # TODO: Translate + #OLD TRANSLATION: "not-supported-by-server-error" : u"Эта возможность не поддерживается сервером. Требуется сервер Syncplay {}+, вы подключены к серверу Syncplay {}.", #minVersion, serverVersion "shared-playlists-not-supported-by-server-error" : u"Общие списки воспроизведения могут не поддерживаться сервером. Для корректной работы требуется сервер Syncplay {}+, вы подключены к серверу Syncplay {}.", #minVersion, serverVersion "shared-playlists-disabled-by-server-error" : "The shared playlist feature has been disabled in the server configuration. To use this feature you will need to connect to a different server.", # TODO: Translate @@ -227,6 +233,7 @@ ru = { "messages-label" : u"Сообщения", "messages-osd-title" : u"Настройки OSD", "messages-other-title" : u"Другие настройки отображения", + "chat-label" : u"Chat", # TODO: Translate "privacy-label" : u"Приватность", "privacy-title" : u"Настройки приватности", "unpause-title" : u"Если вы стартуете, то:", @@ -237,6 +244,27 @@ ru = { "syncplay-trusteddomains-title": u"Доверенные сайты (стрим-сервисы, видеохостинги, файлы в сети)", "addtrusteddomain-menu-label" : u"Добавить {} как доверенный сайт", # Domain + "chat-title": u"Chat message input", # TODO: Translate + "chatinputenabled-label": u"Enable chat input via mpv (using enter key)", # TODO: Translate + "chatdirectinput-label" : u"Allow instant chat input (bypass having to press enter key to chat)", # TODO: Translate + "chatinputfont-label": u"Chat input font", # TODO: Translate + "chatfont-label": u"Set font", # TODO: Translate + "chatcolour-label": u"Set colour", # TODO: Translate + "chatinputposition-label": u"Position of message input area in mpv", # TODO: Translate + "chat-top-option": u"Top", # TODO: Translate + "chat-middle-option": u"Middle", # TODO: Translate + "chat-bottom-option": u"Bottom", # TODO: Translate + "chatoutputfont-label": u"Chat output font", # TODO: Translate + "chatoutputenabled-label": u"Enable chat output in media player (mpv only for now)", # TODO: Translate + "chatoutputposition-label": u"Output mode", # TODO: Translate + "chat-chatroom-option": u"Chatroom style", # TODO: Translate + "chat-scrolling-option": u"Scrolling style", # TODO: Translate + + "mpv-key-tab-hint": u"[TAB] to toggle access to alphabet row key shortcuts.", # TODO: Translate + "mpv-key-hint": u"[ENTER] to send message. [ESC] to escape chat mode.", # TODO: Translate + "alphakey-mode-warning-first-line": u"You can temporarily use old mpv bindings with a-z keys.", # TODO: Translate + "alphakey-mode-warning-second-line": u"Press [TAB] to return to Syncplay chat mode.", # TODO: Translate + "help-label" : u"Помощь", "reset-label" : u"Сброс настроек", "run-label" : u"Запустить", @@ -357,6 +385,22 @@ ru = { "unpause-ifminusersready-tooltip" : u"Когда вы стартуете не готовым, воспроизведение начнется, если остальные готовы и присутствует достаточное число зрителей.", "trusteddomains-arguments-tooltip" : u"Сайты, которые разрешены для автоматического воспроизведения из общего списка воспроизведения.", + "chatinputenabled-tooltip": u"Enable chat input in mpv (press enter to chat, enter to send, escape to cancel)",# TODO: Translate + "chatdirectinput-tooltip" : u"Skip having to press 'enter' to go into chat input mode in mpv. Press TAB in mpv to temporarily disable this feature.", # TODO: Translate + "font-label-tooltip": u"Font used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",# TODO: Translate + "set-input-font-tooltip": u"Font family used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",# TODO: Translate + "set-input-colour-tooltip": u"Font colour used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",# TODO: Translate + "chatinputposition-tooltip": u"Location in mpv where chat input text will appear when you press enter and type.",# TODO: Translate + "chatinputposition-top-tooltip": u"Place chat input at top of mpv window.", # TODO: Translate + "chatinputposition-middle-tooltip": u"Place chat input in dead centre of mpv window.", # TODO: Translate + "chatinputposition-bottom-tooltip": u"Place chat input at bottom of mpv window.", # TODO: Translate + "chatoutputenabled-tooltip": u"Show chat messages in OSD (if supported by media player).", # TODO: Translate + "font-output-label-tooltip": u"Chat output font.", # TODO: Translate + "set-output-font-tooltip": u"Font used for when displaying chat messages.", # TODO: Translate + "chatoutputmode-tooltip": u"How chat messages are displayed.", # TODO: Translate + "chatoutputmode-chatroom-tooltip": u"Display new lines of chat directly below previous line.", # TODO: Translate + "chatoutputmode-scrolling-tooltip": u"Scroll chat text from right to left.", # TODO: Translate + "help-tooltip" : u"Открыть Руководство Пользователя на Syncplay.pl.", "reset-tooltip" : u"Сбрасывает все настройки Syncplay в начальное состояние.", "update-server-list-tooltip" : u"Обновить список публичных серверов от syncplay.pl.", @@ -419,6 +463,7 @@ ru = { "addurlstoplaylist-menu-label" : u"Добавить ссылку в очередь", "editplaylist-menu-label": u"Редактировать список", + "open-containing-folder": u"Open folder containing this file", # TODO: Traslate "addusersfiletoplaylist-menu-label" : u"Добавить файл {} в список воспроизведения", # item owner indicator "addusersstreamstoplaylist-menu-label" : u"Добавить поток {} в список воспроизведения", # item owner indicator "openusersstream-menu-label" : u"Открыть поток от {}", # [username]'s diff --git a/syncplay/players/basePlayer.py b/syncplay/players/basePlayer.py index c8fdcc2..ab93a4c 100644 --- a/syncplay/players/basePlayer.py +++ b/syncplay/players/basePlayer.py @@ -12,7 +12,7 @@ class BasePlayer(object): ''' Display given message on player's OSD or similar means ''' - def displayMessage(self, message, duration = (constants.OSD_DURATION*1000)): + def displayMessage(self, message, duration = (constants.OSD_DURATION*1000), secondaryOSD=False, mood=constants.MESSAGE_NEUTRAL): raise NotImplementedError() ''' @@ -34,6 +34,12 @@ class BasePlayer(object): def setPaused(self, value): raise NotImplementedError() + ''' + @type value: list + ''' + def setFeatures(self, featureList): + raise NotImplementedError() + ''' @type value: float ''' diff --git a/syncplay/players/mpc.py b/syncplay/players/mpc.py index c274ef7..3d40308 100644 --- a/syncplay/players/mpc.py +++ b/syncplay/players/mpc.py @@ -7,7 +7,7 @@ from functools import wraps from syncplay.players.basePlayer import BasePlayer import re from syncplay.utils import retry -from syncplay import constants +from syncplay import constants from syncplay.messages import getMessage import os.path @@ -29,7 +29,7 @@ class MpcHcApi: self.__listener.setDaemon(True) self.__listener.start() self.__locks.listenerStart.wait() - + def waitForFileStateReady(f): #@NoSelf @wraps(f) def wrapper(self, *args, **kwds): @@ -37,35 +37,35 @@ class MpcHcApi: raise self.PlayerNotReadyException() return f(self, *args, **kwds) return wrapper - + def startMpc(self, path, args=()): args = "%s /slave %s" % (" ".join(args), str(self.__listener.hwnd)) win32api.ShellExecute(0, "open", path, args, None, 1) if not self.__locks.mpcStart.wait(constants.MPC_OPEN_MAX_WAIT_TIME): raise self.NoSlaveDetectedException(getMessage("mpc-slave-error")) - self.__mpcExistenceChecking.start() + self.__mpcExistenceChecking.start() def openFile(self, filePath): self.__listener.SendCommand(self.CMD_OPENFILE, filePath) - + def isPaused(self): return self.playState <> self.__MPC_PLAYSTATE.PS_PLAY and self.playState <> None - + def askForVersion(self): self.__listener.SendCommand(self.CMD_GETVERSION) - + @waitForFileStateReady def pause(self): self.__listener.SendCommand(self.CMD_PAUSE) - + @waitForFileStateReady def playPause(self): self.__listener.SendCommand(self.CMD_PLAYPAUSE) - + @waitForFileStateReady def unpause(self): self.__listener.SendCommand(self.CMD_PLAY) - + @waitForFileStateReady def askForCurrentPosition(self): self.__listener.SendCommand(self.CMD_GETCURRENTPOSITION) @@ -84,13 +84,13 @@ class MpcHcApi: ('nMsgPos', ctypes.c_int32), ('nDurationMS', ctypes.c_int32), ('strMsg', ctypes.c_wchar * (len(message) + 1)) - ] - cmessage = __OSDDATASTRUCT() - cmessage.nMsgPos = MsgPos - cmessage.nDurationMS = DurationMs + ] + cmessage = __OSDDATASTRUCT() + cmessage.nMsgPos = MsgPos + cmessage.nDurationMS = DurationMs cmessage.strMsg = message self.__listener.SendCommand(self.CMD_OSDSHOWMESSAGE, cmessage) - + def sendRawCommand(self, cmd, value): self.__listener.SendCommand(cmd, value) @@ -100,7 +100,7 @@ class MpcHcApi: self.__locks.mpcStart.set() if self.callbacks.onConnected: thread.start_new_thread(self.callbacks.onConnected, ()) - + elif cmd == self.CMD_STATE: self.loadState = int(value) fileNotReady = self.loadState == self.__MPC_LOADSTATE.MLS_CLOSING or self.loadState == self.__MPC_LOADSTATE.MLS_LOADING or self.loadState == self.__MPC_LOADSTATE.MLS_CLOSED @@ -111,12 +111,12 @@ class MpcHcApi: self.__locks.fileReady.set() if self.callbacks.onFileStateChange: thread.start_new_thread(self.callbacks.onFileStateChange, (self.loadState,)) - + elif cmd == self.CMD_PLAYMODE: self.playState = int(value) if self.callbacks.onUpdatePlaystate: thread.start_new_thread(self.callbacks.onUpdatePlaystate, (self.playState,)) - + elif cmd == self.CMD_NOWPLAYING: value = re.split(r'(? float(value): #Notify seek is sometimes sent twice self.lastFilePosition = float(value) if self.callbacks.onSeek: thread.start_new_thread(self.callbacks.onSeek, (self.lastFilePosition,)) - + elif cmd == self.CMD_DISCONNECT: if self.callbacks.onMpcClosed: thread.start_new_thread(self.callbacks.onMpcClosed, (None,)) - + elif cmd == self.CMD_VERSION: if self.callbacks.onVersion: self.version = value thread.start_new_thread(self.callbacks.onVersion, (value,)) - + class PlayerNotReadyException(Exception): pass - + class __Callbacks: def __init__(self): self.onConnected = None @@ -166,13 +166,13 @@ class MpcHcApi: self.onFileStateChange = None self.onMpcClosed = None self.onVersion = None - + class __Locks: def __init__(self): self.listenerStart = threading.Event() self.mpcStart = threading.Event() self.fileReady = threading.Event() - + def __mpcReadyInSlaveMode(self): while True: time.sleep(10) @@ -180,7 +180,7 @@ class MpcHcApi: if self.callbacks.onMpcClosed: self.callbacks.onMpcClosed(None) break - + CMD_CONNECT = 0x50000000 CMD_STATE = 0x50000001 CMD_PLAYMODE = 0x50000002 @@ -225,13 +225,13 @@ class MpcHcApi: CMD_PAUSE = 0xA0000005 CMD_GETVERSION = 0xA0003006 CMD_SETSPEED = 0xA0004008 - + class __MPC_LOADSTATE: MLS_CLOSED = 0 MLS_LOADING = 1 MLS_LOADED = 2 MLS_CLOSING = 3 - + class __MPC_PLAYSTATE: PS_PLAY = 0 PS_PAUSE = 1 @@ -244,10 +244,10 @@ class MpcHcApi: self.locks = locks self.mpcHandle = None self.hwnd = None - self.__PCOPYDATASTRUCT = ctypes.POINTER(self.__COPYDATASTRUCT) + self.__PCOPYDATASTRUCT = ctypes.POINTER(self.__COPYDATASTRUCT) threading.Thread.__init__(self, name="MPC Listener") - - def run(self): + + def run(self): message_map = { win32con.WM_COPYDATA: self.OnCopyData } @@ -271,13 +271,13 @@ class MpcHcApi: ) self.locks.listenerStart.set() win32gui.PumpMessages() - - + + def OnCopyData(self, hwnd, msg, wparam, lparam): pCDS = ctypes.cast(lparam, self.__PCOPYDATASTRUCT) #print "API:\tin>\t 0x%X\t" % int(pCDS.contents.dwData), ctypes.wstring_at(pCDS.contents.lpData) self.__mpcApi.handleCommand(pCDS.contents.dwData, ctypes.wstring_at(pCDS.contents.lpData)) - + def SendCommand(self, cmd, message=u''): #print "API:\t self.__client.getGlobalPaused(): self.__setUpStateForNewlyOpenedFile() - - @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) + + @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) def __setUpStateForNewlyOpenedFile(self): self._setPausedAccordinglyToServer() self._mpcApi.seek(self.__client.getGlobalPosition()) - + def __handleUpdatedFilename(self): with self.__fileUpdate: self.__setUpStateForNewlyOpenedFile() @@ -472,18 +475,18 @@ class MPCHCAPIPlayer(BasePlayer): def sendCustomCommand(self, cmd, val): self._mpcApi.sendRawCommand(cmd, val) - + @staticmethod def getDefaultPlayerPathsList(): return constants.MPC_PATHS - + @staticmethod def getIconPath(path): if MPCHCAPIPlayer.getExpandedPath(path).lower().endswith(u'mpc-hc64.exe'.lower()) or MPCHCAPIPlayer.getExpandedPath(path).lower().endswith(u'mpc-hc64_nvo.exe'.lower()): return constants.MPC64_ICONPATH else: return constants.MPC_ICONPATH - + @staticmethod def isValidPlayerPath(path): if MPCHCAPIPlayer.getExpandedPath(path): diff --git a/syncplay/players/mplayer.py b/syncplay/players/mplayer.py index a0a610d..4d5b167 100644 --- a/syncplay/players/mplayer.py +++ b/syncplay/players/mplayer.py @@ -1,3 +1,4 @@ +# coding:utf8 import subprocess import re import threading @@ -11,7 +12,8 @@ from syncplay.utils import isWindows class MplayerPlayer(BasePlayer): speedSupported = True customOpenDialog = False - secondaryOSDSupported = False + alertOSDSupported = False + chatOSDSupported = False osdMessageSeparator = "; " RE_ANSWER = re.compile(constants.MPLAYER_ANSWER_REGEX) @@ -88,8 +90,15 @@ class MplayerPlayer(BasePlayer): def _getProperty(self, property_): self._listener.sendLine("get_property {}".format(property_)) - def displayMessage(self, message, duration=(constants.OSD_DURATION * 1000), secondaryOSD=False): - self._listener.sendLine(u'{} "{!s}" {} {}'.format(self.OSD_QUERY, self._stripNewlines(message), duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8')) + def displayMessage(self, message, duration=(constants.OSD_DURATION * 1000), OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL): + messageString = self._sanitizeText(message.replace("\\n", "")).replace("", "\\n") + self._listener.sendLine(u'{} "{!s}" {} {}'.format(self.OSD_QUERY, messageString, duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8')) + + def displayChatMessage(self, username, message): + messageString = u"<{}> {}".format(username, message) + messageString = self._sanitizeText(messageString.replace("\\n", "")).replace("", "\\n") + duration = int(constants.OSD_DURATION * 1000) + self._listener.sendLine(u'{} "{!s}" {} {}'.format(self.OSD_QUERY, messageString, duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8')) def setSpeed(self, value): self._setProperty('speed', "{:.2f}".format(value)) @@ -105,6 +114,9 @@ class MplayerPlayer(BasePlayer): self.setPaused(self._client.getGlobalPaused()) self.setPosition(self._client.getGlobalPosition()) + def setFeatures(self, featureList): + pass + def setPosition(self, value): self._position = max(value,0) self._setProperty(self.POSITION_QUERY, "{}".format(value)) @@ -130,9 +142,16 @@ class MplayerPlayer(BasePlayer): def _getPosition(self): self._getProperty(self.POSITION_QUERY) - def _stripNewlines(self, text): + def _sanitizeText(self, text): text = text.replace("\r", "") text = text.replace("\n", "") + text = text.replace("\\\"", "") + text = text.replace("\"", "") + text = text.replace("%", "%%") + text = text.replace("\\", "\\\\") + text = text.replace("{", "\\\\{") + text = text.replace("}", "\\\\}") + text = text.replace("","\\\"") return text def _quoteArg(self, arg): @@ -158,6 +177,8 @@ class MplayerPlayer(BasePlayer): def lineReceived(self, line): if line: self._client.ui.showDebugMessage("player << {}".format(line)) + line = line.replace("[cplayer] ", "") # -v workaround + line = line.replace("[term-msg] ", "") # -v workaround line = line.replace(" cplayer: ","") # --msg-module workaround line = line.replace(" term-msg: ", "") if "Failed to get value of property" in line or "=(unavailable)" in line or line == "ANS_filename=" or line == "ANS_length=" or line == "ANS_path=": @@ -277,6 +298,9 @@ class MplayerPlayer(BasePlayer): self.lastSendTime = None self.lastNotReadyTime = None self.__playerController = playerController + if not self.__playerController._client._config["chatOutputEnabled"]: + self.__playerController.alertOSDSupported = False + self.__playerController.chatOSDSupported = False if self.__playerController.getPlayerPathErrors(playerPath,filePath): raise ValueError() if filePath and '://' not in filePath: @@ -332,6 +356,18 @@ class MplayerPlayer(BasePlayer): self.__playerController.lineReceived(line) self.__playerController.drop() + def sendChat(self, message): + if message: + if message[:1] == "/" and message <> "/": + command = message[1:] + if command and command[:1] == "/": + message = message[1:] + else: + self.__playerController.reactor.callFromThread(self.__playerController._client.ui.executeCommand, + command) + return + self.__playerController.reactor.callFromThread(self.__playerController._client.sendChat, message) + def isReadyForSend(self): self.checkForReadinessOverride() return self.readyToSend diff --git a/syncplay/players/mpv.py b/syncplay/players/mpv.py index 88bd7a3..d8d13c4 100644 --- a/syncplay/players/mpv.py +++ b/syncplay/players/mpv.py @@ -1,14 +1,16 @@ +# coding:utf8 import re import subprocess from syncplay.players.mplayer import MplayerPlayer from syncplay.messages import getMessage from syncplay import constants -from syncplay.utils import isURL +from syncplay.utils import isURL, findResourcePath import os, sys, time class MpvPlayer(MplayerPlayer): RE_VERSION = re.compile('.*mpv (\d+)\.(\d+)\.\d+.*') osdMessageSeparator = "\\n" + osdMessageSeparator = "; " # TODO: Make conditional @staticmethod def run(client, playerPath, filePath, args): @@ -17,6 +19,9 @@ class MpvPlayer(MplayerPlayer): except: ver = None constants.MPV_NEW_VERSION = ver is None or int(ver.group(1)) > 0 or int(ver.group(2)) >= 6 + constants.MPV_OSC_VISIBILITY_CHANGE_VERSION = False if ver is None else int(ver.group(1)) > 0 or int(ver.group(2)) >= 28 + if not constants.MPV_OSC_VISIBILITY_CHANGE_VERSION: + client.ui.showDebugMessage(u"This version of mpv is not known to be compatible with changing the OSC visibility. Please use mpv >=0.28.0.") if constants.MPV_NEW_VERSION: return NewMpvPlayer(client, MpvPlayer.getExpandedPath(playerPath), filePath, args) else: @@ -30,6 +35,7 @@ class MpvPlayer(MplayerPlayer): args.extend(constants.MPV_SLAVE_ARGS) if constants.MPV_NEW_VERSION: args.extend(constants.MPV_SLAVE_ARGS_NEW) + args.extend([u"--script={}".format(findResourcePath("syncplayintf.lua"))]) return args @staticmethod @@ -108,6 +114,25 @@ class OldMpvPlayer(MpvPlayer): class NewMpvPlayer(OldMpvPlayer): lastResetTime = None lastMPVPositionUpdate = None + alertOSDSupported = True + chatOSDSupported = True + + def displayMessage(self, message, duration=(constants.OSD_DURATION * 1000), OSDType=constants.OSD_NOTIFICATION, + mood=constants.MESSAGE_NEUTRAL): + if not self._client._config["chatOutputEnabled"]: + super(self.__class__, self).displayMessage(message=message,duration=duration,OSDType=OSDType,mood=mood) + return + messageString = self._sanitizeText(message.replace("\\n", "")).replace("\\\\",constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER).replace("", "\\n") + self._listener.sendLine(u'script-message-to syncplayintf {}-osd-{} "{}"'.format(OSDType, mood, messageString)) + + def displayChatMessage(self, username, message): + if not self._client._config["chatOutputEnabled"]: + super(self.__class__, self).displayChatMessage(username,message) + return + username = self._sanitizeText(username.replace("\\",constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER)) + message = self._sanitizeText(message.replace("\\",constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER)) + messageString = u"<{}> {}".format(username, message) + self._listener.sendLine(u'script-message-to syncplayintf chat "{}"'.format(messageString)) def setPaused(self, value): if self._paused == value: @@ -199,6 +224,9 @@ class NewMpvPlayer(OldMpvPlayer): self._clearFileLoaded() self._listener.sendLine(u'loadfile {}'.format(self._quoteArg(filePath)), notReadyAfterThis=True) + def setFeatures(self, featureList): + self.sendMpvOptions() + def setPosition(self, value): if value < constants.DO_NOT_RESET_POSITION_THRESHOLD and self._recentlyReset(): self._client.ui.showDebugMessage("Did not seek as recently reset and {} below 'do not reset position' threshold".format(value)) @@ -222,9 +250,29 @@ class NewMpvPlayer(OldMpvPlayer): else: self._storePosition(0) + def sendMpvOptions(self): + options = [] + for option in constants.MPV_SYNCPLAYINTF_OPTIONS_TO_SEND: + options.append(u"{}={}".format(option, self._client._config[option])) + for option in constants.MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND: + options.append(option) + for option in constants.MPV_SYNCPLAYINTF_LANGUAGE_TO_SEND: + options.append(u"{}={}".format(option, getMessage(option))) + options.append(u"OscVisibilityChangeCompatible={}".format(constants.MPV_OSC_VISIBILITY_CHANGE_VERSION)) + options_string = ", ".join(options) + self._listener.sendLine(u'script-message-to syncplayintf set_syncplayintf_options "{}"'.format(options_string)) + self._setOSDPosition() + def _handleUnknownLine(self, line): self.mpvErrorCheck(line) + if "" in line: + line = line.decode("utf-8").replace(constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER, "\\").encode("utf-8") + self._listener.sendChat(line[6:-7]) + + if "" in line: + self.sendMpvOptions() + if line == "" or "Playing:" in line: self._listener.setReadyToSend(False) self._clearFileLoaded() @@ -236,6 +284,11 @@ class NewMpvPlayer(OldMpvPlayer): elif "Failed" in line or "failed" in line or "No video or audio streams selected" in line or "error" in line: self._listener.setReadyToSend(True) + def _setOSDPosition(self): + if self._client._config['chatMoveOSD'] and (self._client._config['chatOutputEnabled'] or (self._client._config['chatInputEnabled'] and self._client._config['chatInputPosition'] == constants.INPUT_POSITION_TOP)): + self._setProperty("osd-align-y", "bottom") + self._setProperty("osd-margin-y", int(self._client._config['chatOSDMargin'])) + def _recentlyReset(self): if not self.lastResetTime: return False diff --git a/syncplay/players/vlc.py b/syncplay/players/vlc.py index bf3826d..316b3a9 100755 --- a/syncplay/players/vlc.py +++ b/syncplay/players/vlc.py @@ -16,7 +16,8 @@ from syncplay.utils import isBSD, isLinux, isWindows, isMacOS class VlcPlayer(BasePlayer): speedSupported = True customOpenDialog = False - secondaryOSDSupported = True + chatOSDSupported = False + alertOSDSupported = True osdMessageSeparator = "; " RE_ANSWER = re.compile(constants.VLC_ANSWER_REGEX) @@ -118,9 +119,9 @@ class VlcPlayer(BasePlayer): else: return self._position - def displayMessage(self, message, duration=constants.OSD_DURATION * 1000, secondaryOSD=False): + def displayMessage(self, message, duration=constants.OSD_DURATION * 1000, OSDType=constants.OSD_DURATION, mood=constants.MESSAGE_NEUTRAL): duration /= 1000 - if secondaryOSD == False: + if OSDType != constants.OSD_ALERT: self._listener.sendLine('display-osd: {}, {}, {}'.format('top-right', duration, message.encode('utf8'))) else: self._listener.sendLine('display-secondary-osd: {}, {}, {}'.format('center', duration, message.encode('utf8'))) @@ -128,6 +129,9 @@ class VlcPlayer(BasePlayer): def setSpeed(self, value): self._listener.sendLine("set-rate: {:.2n}".format(value)) + def setFeatures(self, featureList): + pass + def setPosition(self, value): self._lastVLCPositionUpdate = time.time() self._listener.sendLine("set-position: {}".format(value).replace(".",self.radixChar)) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 59948b9..ba0e187 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -5,8 +5,8 @@ import syncplay from functools import wraps import time from syncplay.messages import getMessage -from syncplay.constants import PING_MOVING_AVERAGE_WEIGHT - +from syncplay.constants import PING_MOVING_AVERAGE_WEIGHT, CONTROLLED_ROOMS_MIN_VERSION, USER_READY_MIN_VERSION, SHARED_PLAYLIST_MIN_VERSION, CHAT_MIN_VERSION +from syncplay.utils import meetsMinVersion class JSONCommandProtocol(LineReceiver): def handleMessages(self, messages): @@ -107,6 +107,7 @@ class SyncClientProtocol(JSONCommandProtocol): if room: hello["room"] = {"name" :room} hello["version"] = "1.2.255" # Used so newer clients work on 1.2.X server hello["realversion"] = syncplay.version + hello["features"] = self._client.getFeatures() self.sendMessage({"Hello": hello}) def _SetUser(self, users): @@ -147,6 +148,11 @@ class SyncClientProtocol(JSONCommandProtocol): self._client.playlist.changeToPlaylistIndex(values['index'], values['user']) elif command == "playlistChange": self._client.playlist.changePlaylist(values['files'], values['user']) + elif command == "features": + self._client.setUserFeatures(values["username"],values['features']) + + def sendFeaturesUpdate(self, features): + self.sendSet({"features": features}) def sendSet(self, setting): self.sendMessage({"Set": setting}) @@ -173,7 +179,8 @@ class SyncClientProtocol(JSONCommandProtocol): file_ = user[1]['file'] if user[1]['file'] <> {} else None isController = user[1]['controller'] if 'controller' in user[1] else False isReady = user[1]['isReady'] if 'isReady' in user[1] else None - self._client.userlist.addUser(userName, roomName, file_, noMessage=True, isController=isController, isReady=isReady) + features = user[1]['features'] if 'features' in user[1] else None + self._client.userlist.addUser(userName, roomName, file_, noMessage=True, isController=isController, isReady=isReady, features=features) self._client.userlist.showUserList() def sendList(self): @@ -249,9 +256,9 @@ class SyncClientProtocol(JSONCommandProtocol): } }) def handleChat(self,message): + username = message['username'] userMessage = message['message'] - messageString = u"<{}> {}".format(message['username'], userMessage) - self._client.ui.showMessage(messageString) + self._client.ui.showChatMessage(username, userMessage) def setReady(self, isReady, manuallyInitiated=True): self.sendSet({ @@ -286,6 +293,7 @@ class SyncServerProtocol(JSONCommandProtocol): def __init__(self, factory): self._factory = factory self._version = None + self._features = None self._logged = False self.clientIgnoringOnTheFly = 0 self.serverIgnoringOnTheFly = 0 @@ -319,6 +327,16 @@ class SyncServerProtocol(JSONCommandProtocol): def connectionLost(self, reason): self._factory.removeWatcher(self._watcher) + def getFeatures(self): + if not self._features: + self._features = {} + self._features["sharedPlaylists"] = meetsMinVersion(self._version, SHARED_PLAYLIST_MIN_VERSION) + self._features["chat"] = meetsMinVersion(self._version, CHAT_MIN_VERSION) + self._features["featureList"] = False + self._features["readiness"] = meetsMinVersion(self._version, USER_READY_MIN_VERSION) + self._features["managedRooms"] = meetsMinVersion(self._version, CONTROLLED_ROOMS_MIN_VERSION) + return self._features + def isLogged(self): return self._logged @@ -345,7 +363,8 @@ class SyncServerProtocol(JSONCommandProtocol): roomName = None version = hello["version"] if hello.has_key("version") else None version = hello["realversion"] if hello.has_key("realversion") else version - return username, serverPassword, roomName, version + features = hello["features"] if hello.has_key("features") else None + return username, serverPassword, roomName, version, features def _checkPassword(self, serverPassword): if self._factory.password: @@ -358,7 +377,7 @@ class SyncServerProtocol(JSONCommandProtocol): return True def handleHello(self, hello): - username, serverPassword, roomName, version = self._extractHelloArguments(hello) + username, serverPassword, roomName, version, features = self._extractHelloArguments(hello) if not username or not roomName or not version: self.dropWithError(getMessage("hello-server-error")) return @@ -366,14 +385,22 @@ class SyncServerProtocol(JSONCommandProtocol): if not self._checkPassword(serverPassword): return self._version = version + self.setFeatures(features) self._factory.addWatcher(self, username, roomName) self._logged = True self.sendHello(version) + @requireLogged def handleChat(self,chatMessage): if not self._factory.disableChat: self._factory.sendChat(self._watcher,chatMessage) + def setFeatures(self, features): + self._features = features + + def sendFeaturesUpdate(self): + self.sendSet({"features": self.getFeatures()}) + def setWatcher(self, watcher): self._watcher = watcher @@ -410,6 +437,9 @@ class SyncServerProtocol(JSONCommandProtocol): self._factory.setPlaylist(self._watcher, set_[1]['files']) elif command == "playlistIndex": self._factory.setPlaylistIndex(self._watcher, set_[1]['index']) + elif command == "features": + #TODO: Check + self._watcher.setFeatures(set_[1]) def sendSet(self, setting): self.sendMessage({"Set": setting}) @@ -476,7 +506,8 @@ class SyncServerProtocol(JSONCommandProtocol): "position": 0, "file": watcher.getFile() if watcher.getFile() else {}, "controller": watcher.isController(), - "isReady": watcher.isReady() + "isReady": watcher.isReady(), + "features": watcher.getFeatures() } userlist[room.getName()][watcher.getName()] = userFile diff --git a/syncplay/server.py b/syncplay/server.py index 4ce44a0..4eb5ee8 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -48,6 +48,11 @@ class SyncFactory(Factory): features["readiness"] = not self.disableReady features["managedRooms"] = True features["chat"] = not self.disableChat + features["maxChatMessageLength"] = constants.MAX_CHAT_MESSAGE_LENGTH + features["maxUsernameLength"] = constants.MAX_USERNAME_LENGTH + features["maxRoomNameLength"] = constants.MAX_ROOM_NAME_LENGTH + features["maxFilenameLength"] = constants.MAX_FILENAME_LENGTH + return features def getMotd(self, userIp, username, room, clientVersion): @@ -108,7 +113,7 @@ class SyncFactory(Factory): self._roomManager.broadcast(watcher, l) def sendJoinMessage(self, watcher): - l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True, "version": watcher.getVersion()}) if w != watcher else None + l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True, "version": watcher.getVersion(), "features": watcher.getFeatures()}) if w != watcher else None self._roomManager.broadcast(watcher, l) self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False)) @@ -413,6 +418,10 @@ class Watcher(object): def setReady(self, ready): self._ready = ready + def getFeatures(self): + features = self._connector.getFeatures() + return features + def isReady(self): if self._server.disableReady: return None diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index b23814f..d1bbe81 100755 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -8,6 +8,7 @@ from syncplay.messages import getMessage, setLanguage, isValidLanguage from syncplay.players.playerFactory import PlayerFactory from syncplay.utils import isMacOS import codecs +import re class InvalidConfigValue(Exception): def __init__(self, message): @@ -64,6 +65,29 @@ class ConfigurationGetter(object): "showNonControllerOSD" : False, "showContactInfo" : True, "showDurationNotification" : True, + "chatInputEnabled" : True, + "chatInputFontFamily" : 'sans-serif', + "chatInputRelativeFontSize" : constants.DEFAULT_CHAT_FONT_SIZE, + "chatInputFontWeight" : constants.DEFAULT_CHAT_FONT_WEIGHT, + "chatInputFontUnderline": False, + "chatInputFontColor": constants.DEFAULT_CHAT_INPUT_FONT_COLOR, + "chatInputPosition": constants.INPUT_POSITION_TOP, + "chatDirectInput": False, + "chatOutputEnabled": True, + "chatOutputFontFamily": 'sans-serif', + "chatOutputRelativeFontSize": constants.DEFAULT_CHAT_FONT_SIZE, + "chatOutputFontWeight": constants.DEFAULT_CHAT_FONT_WEIGHT, + "chatOutputFontUnderline": False, + "chatOutputMode": constants.CHATROOM_MODE, + "chatMaxLines": 7, + "chatTopMargin": 25, + "chatLeftMargin": 20, + "chatBottomMargin": 30, + "chatMoveOSD": True, + "chatOSDMargin": 110, + "notificationTimeout": 3, + "alertTimeout": 5, + "chatTimeout": 7, "publicServers" : [] } @@ -106,7 +130,13 @@ class ConfigurationGetter(object): "sharedPlaylistEnabled", "loopAtEndOfPlaylist", "loopSingleFiles", - "onlySwitchToTrustedDomains" + "onlySwitchToTrustedDomains", + "chatInputEnabled", + "chatInputFontUnderline", + "chatDirectInput", + "chatMoveOSD", + "chatOutputEnabled", + "chatOutputFontUnderline" ] self._tristate = [ "checkForUpdatesAutomatically", @@ -125,6 +155,22 @@ class ConfigurationGetter(object): "rewindThreshold", "fastforwardThreshold", "autoplayMinUsers", + "chatInputRelativeFontSize", + "chatInputFontWeight", + "chatOutputFontWeight", + "chatOutputRelativeFontSize", + "chatMaxLines", + "chatTopMargin", + "chatLeftMargin", + "chatBottomMargin", + "chatOSDMargin", + "notificationTimeout", + "alertTimeout", + "chatTimeout" + ] + + self._hexadecimal = [ + "chatInputFontColor" ] self._iniStructure = { @@ -143,7 +189,19 @@ class ConfigurationGetter(object): "onlySwitchToTrustedDomains", "trustedDomains","publicServers"], "gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", - "showNonControllerOSD", "showDurationNotification"], + "showNonControllerOSD", "showDurationNotification", + "chatInputEnabled","chatInputFontUnderline", + "chatInputFontFamily", "chatInputRelativeFontSize", + "chatInputFontWeight", "chatInputFontColor", + "chatInputPosition","chatDirectInput", + "chatOutputFontFamily", "chatOutputRelativeFontSize", + "chatOutputFontWeight", "chatOutputFontUnderline", + "chatOutputMode", "chatMaxLines", + "chatTopMargin", "chatLeftMargin", + "chatBottomMargin", "chatDirectInput", + "chatMoveOSD", "chatOSDMargin", + "notificationTimeout", "alertTimeout", + "chatTimeout","chatOutputEnabled"], "general": ["language", "checkForUpdatesAutomatically", "lastCheckedForUpdates"] } @@ -197,6 +255,11 @@ class ConfigurationGetter(object): for key in self._numeric: self._config[key] = float(self._config[key]) + for key in self._hexadecimal: + match = re.search(r'^#(?:[0-9a-fA-F]){6}$', self._config[key]) + if not match: + self._config[key] = u"#FFFFFF" + for key in self._required: if key == "playerPath": player = None diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 7a62c22..b5ef9d7 100755 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -12,7 +12,7 @@ import sys import threading from syncplay.messages import getMessage, getLanguages, setLanguage, getInitialLanguage from syncplay import constants -from syncplay.utils import isBSD, isLinux, isMacOS +from syncplay.utils import isBSD, isLinux, isMacOS, isWindows from syncplay.utils import resourcespath, posixresourcespath class GuiConfiguration: def __init__(self, config, error=None, defaultConfig=None): @@ -518,7 +518,7 @@ class ConfigDialog(QtWidgets.QDialog): def connectChildren(self, widget): widgetName = str(widget.objectName()) - if self.subitems.has_key(widgetName) and isinstance(widget, QCheckBox): + if self.subitems.has_key(widgetName): widget.stateChanged.connect(lambda: self.updateSubwidgets(self, widget)) self.updateSubwidgets(self, widget) @@ -872,6 +872,155 @@ class ConfigDialog(QtWidgets.QDialog): self.syncSettingsLayout.setAlignment(Qt.AlignTop) self.stackedLayout.addWidget(self.syncSettingsFrame) + def addChatTab(self): + self.chatFrame = QtWidgets.QFrame() + self.chatLayout = QtWidgets.QVBoxLayout() + self.chatLayout.setAlignment(Qt.AlignTop) + + # Input + self.chatInputGroup = QtWidgets.QGroupBox(getMessage("chat-title")) + self.chatInputLayout = QtWidgets.QGridLayout() + self.chatLayout.addWidget(self.chatInputGroup) + self.chatInputGroup.setLayout(self.chatInputLayout) + self.chatInputEnabledCheckbox = QCheckBox(getMessage("chatinputenabled-label")) + self.chatInputEnabledCheckbox.setObjectName("chatInputEnabled") + self.chatInputLayout.addWidget(self.chatInputEnabledCheckbox, 1, 0, 1,1, Qt.AlignLeft) + + self.chatDirectInputCheckbox = QCheckBox(getMessage("chatdirectinput-label")) + self.chatDirectInputCheckbox.setObjectName("chatDirectInput") + self.chatDirectInputCheckbox.setStyleSheet( + constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png")) + self.chatInputLayout.addWidget(self.chatDirectInputCheckbox, 2, 0, 1,1, Qt.AlignLeft) + + self.inputFontLayout = QtWidgets.QHBoxLayout() + self.inputFontLayout.setContentsMargins(0, 0, 0, 0) + self.inputFontFrame = QtWidgets.QFrame() + self.inputFontFrame.setLayout(self.inputFontLayout) + self.inputFontFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + self.chatFontLabel = QLabel(getMessage("chatinputfont-label"), self) + self.chatFontLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png")) + self.chatFontLabel.setObjectName("font-label") + self.chatInputFontButton = QtWidgets.QPushButton(getMessage("chatfont-label")) + self.chatInputFontButton.setObjectName("set-input-font") + self.chatInputFontButtonGroup = QtWidgets.QButtonGroup() + self.chatInputFontButtonGroup.addButton(self.chatInputFontButton) + self.chatInputFontButton.released.connect(lambda: self.fontDialog("chatInput")) + self.chatInputColourButton = QtWidgets.QPushButton(getMessage("chatcolour-label")) + self.chatInputColourButton.setObjectName("set-input-colour") + self.chatInputColourButtonGroup = QtWidgets.QButtonGroup() + self.chatInputColourButtonGroup.addButton(self.chatInputColourButton) + self.chatInputColourButton.released.connect(lambda: self.colourDialog("chatInput")) + self.inputFontLayout.addWidget(self.chatFontLabel, Qt.AlignLeft) + self.inputFontLayout.addWidget(self.chatInputFontButton, Qt.AlignLeft) + self.inputFontLayout.addWidget(self.chatInputColourButton, Qt.AlignLeft) + self.chatInputLayout.addWidget(self.inputFontFrame, 3, 0, 1, 3, Qt.AlignLeft) + + self.chatInputPositionFrame = QtWidgets.QFrame() + self.chatInputPositionLayout = QtWidgets.QHBoxLayout() + self.chatInputPositionLayout.setContentsMargins(0, 0, 0, 0) + self.chatInputPositionFrame.setLayout(self.chatInputPositionLayout) + self.chatInputPositionFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + self.chatInputPositionLabel = QLabel(getMessage("chatinputposition-label"), self) + self.chatInputPositionLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png")) + self.chatInputPositionGroup = QButtonGroup() + self.chatInputTopOption = QRadioButton(getMessage("chat-top-option")) + self.chatInputMiddleOption = QRadioButton(getMessage("chat-middle-option")) + self.chatInputBottomOption = QRadioButton(getMessage("chat-bottom-option")) + self.chatInputPositionGroup.addButton(self.chatInputTopOption) + self.chatInputPositionGroup.addButton(self.chatInputMiddleOption) + self.chatInputPositionGroup.addButton(self.chatInputBottomOption) + + self.chatInputPositionLabel.setObjectName("chatinputposition") + self.chatInputTopOption.setObjectName("chatinputposition-top" + constants.CONFIG_NAME_MARKER + "chatInputPosition" + constants.CONFIG_VALUE_MARKER + constants.INPUT_POSITION_TOP) + self.chatInputMiddleOption.setObjectName("chatinputposition-middle" + constants.CONFIG_NAME_MARKER + "chatInputPosition" + constants.CONFIG_VALUE_MARKER + constants.INPUT_POSITION_MIDDLE) + self.chatInputBottomOption.setObjectName("chatinputposition-bottom" + constants.CONFIG_NAME_MARKER + "chatInputPosition" + constants.CONFIG_VALUE_MARKER + constants.INPUT_POSITION_BOTTOM) + + self.chatInputPositionLayout.addWidget(self.chatInputPositionLabel) + self.chatInputPositionLayout.addWidget(self.chatInputTopOption) + self.chatInputPositionLayout.addWidget(self.chatInputMiddleOption) + self.chatInputPositionLayout.addWidget(self.chatInputBottomOption) + self.chatInputLayout.addWidget(self.chatInputPositionFrame) + + self.subitems['chatInputEnabled'] = [self.chatInputPositionLabel.objectName(), self.chatInputTopOption.objectName(), + self.chatInputMiddleOption.objectName(), self.chatInputBottomOption.objectName(), + self.chatInputFontButton.objectName(), self.chatFontLabel.objectName(), + self.chatInputColourButton.objectName(), self.chatDirectInputCheckbox.objectName()] + # Output + self.chatOutputGroup = QtWidgets.QGroupBox(u"Chat message output") + self.chatOutputLayout = QtWidgets.QGridLayout() + self.chatLayout.addWidget(self.chatOutputGroup) + self.chatOutputGroup.setLayout(self.chatOutputLayout) + self.chatOutputEnabledCheckbox = QCheckBox(getMessage("chatoutputenabled-label")) + self.chatOutputEnabledCheckbox.setObjectName("chatOutputEnabled") + self.chatOutputLayout.addWidget(self.chatOutputEnabledCheckbox, 1, 0, 1,1, Qt.AlignLeft) + + self.outputFontLayout = QtWidgets.QHBoxLayout() + self.outputFontLayout.setContentsMargins(0, 0, 0, 0) + self.outputFontFrame = QtWidgets.QFrame() + self.outputFontFrame.setLayout(self.outputFontLayout) + self.outputFontFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + self.chatOutputFontLabel = QLabel(getMessage("chatoutputfont-label"), self) + self.chatOutputFontLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png")) + self.chatOutputFontLabel.setObjectName("font-output-label") + self.chatOutputFontButton = QtWidgets.QPushButton(getMessage("chatfont-label")) + self.chatOutputFontButton.setObjectName("set-output-font") + self.chatOutputFontButtonGroup = QtWidgets.QButtonGroup() + self.chatOutputFontButtonGroup.addButton(self.chatOutputFontButton) + self.chatOutputFontButton.released.connect(lambda: self.fontDialog("chatOutput")) + self.chatOutputColourButton = QtWidgets.QPushButton(getMessage("chatcolour-label")) + self.outputFontLayout.addWidget(self.chatOutputFontLabel, Qt.AlignLeft) + self.outputFontLayout.addWidget(self.chatOutputFontButton, Qt.AlignLeft) + self.chatOutputLayout.addWidget(self.outputFontFrame, 2, 0, 1, 3, Qt.AlignLeft) + + self.chatOutputModeLabel = QLabel(getMessage("chatoutputposition-label"), self) + self.chatOutputModeLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png")) + self.chatOutputModeGroup = QButtonGroup() + self.chatOutputChatroomOption = QRadioButton(getMessage("chat-chatroom-option")) + self.chatOutputScrollingOption = QRadioButton(getMessage("chat-scrolling-option")) + self.chatOutputModeGroup.addButton(self.chatOutputChatroomOption) + self.chatOutputModeGroup.addButton(self.chatOutputScrollingOption) + + self.chatOutputModeLabel.setObjectName("chatoutputmode") + self.chatOutputChatroomOption.setObjectName("chatoutputmode-chatroom" + constants.CONFIG_NAME_MARKER + "chatOutputMode" + constants.CONFIG_VALUE_MARKER + constants.CHATROOM_MODE) + self.chatOutputScrollingOption.setObjectName("chatoutputmode-scrolling" + constants.CONFIG_NAME_MARKER + "chatOutputMode" + constants.CONFIG_VALUE_MARKER + constants.SCROLLING_MODE) + + self.chatOutputModeFrame = QtWidgets.QFrame() + self.chatOutputModeLayout = QtWidgets.QHBoxLayout() + self.chatOutputModeLayout.setContentsMargins(0, 0, 0, 0) + self.chatOutputModeFrame.setLayout(self.chatOutputModeLayout) + self.chatOutputModeFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + self.chatOutputModeLayout.addWidget(self.chatOutputModeLabel) + self.chatOutputModeLayout.addWidget(self.chatOutputChatroomOption) + self.chatOutputModeLayout.addWidget(self.chatOutputScrollingOption) + self.chatOutputLayout.addWidget(self.chatOutputModeFrame) + + self.subitems['chatOutputEnabled'] = [self.chatOutputModeLabel.objectName(), self.chatOutputChatroomOption.objectName(), + self.chatOutputScrollingOption.objectName(),self.chatOutputFontButton.objectName(), + self.chatOutputFontLabel.objectName()] + # chatFrame + self.chatFrame.setLayout(self.chatLayout) + self.stackedLayout.addWidget(self.chatFrame) + + def fontDialog(self, configName): + font = QtGui.QFont() + font.setFamily(self.config[configName+ u"FontFamily"]) + font.setPointSize(self.config[configName + u"RelativeFontSize"]) + font.setWeight(self.config[configName + u"FontWeight"]) + font.setUnderline(self.config[configName + u"FontUnderline"]) + value, ok = QtWidgets.QFontDialog.getFont(font) + if ok: + self.config[configName + u"FontFamily"] = value.family() + self.config[configName + u"RelativeFontSize"] = value.pointSize() + self.config[configName + u"FontWeight"] = value.weight() + self.config[configName + u"FontUnderline"] = value.underline() + + def colourDialog(self, configName): + oldColour = QtGui.QColor() + oldColour.setNamedColor(self.config[configName+ u"FontColor"]) + colour = QtWidgets.QColorDialog.getColor(oldColour, self) + if colour.isValid(): + self.config[configName + u"FontColor"] = colour.name() + def addMessageTab(self): self.messageFrame = QtWidgets.QFrame() self.messageLayout = QtWidgets.QVBoxLayout() @@ -970,18 +1119,20 @@ class ConfigDialog(QtWidgets.QDialog): self.helpButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'help.png'), getMessage("help-label")) self.helpButton.setObjectName("help") self.helpButton.setMaximumSize(self.helpButton.sizeHint()) - self.helpButton.pressed.connect(self.openHelp) + self.helpButton.released.connect(self.openHelp) self.resetButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'cog_delete.png'),getMessage("reset-label")) self.resetButton.setMaximumSize(self.resetButton.sizeHint()) self.resetButton.setObjectName("reset") - self.resetButton.pressed.connect(self.resetSettings) + self.resetButton.released.connect(self.resetSettings) + self.runButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("run-label")) + self.runButton.released.connect(self._runWithoutStoringConfig) self.runButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("run-label")) self.runButton.pressed.connect(self._runWithoutStoringConfig) self.runButton.setToolTip(getMessage("nostore-tooltip")) self.storeAndRunButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("storeandrun-label")) - self.storeAndRunButton.pressed.connect(self._saveDataAndLeave) + self.storeAndRunButton.released.connect(self._saveDataAndLeave) self.bottomButtonLayout.addWidget(self.helpButton) self.bottomButtonLayout.addWidget(self.resetButton) self.bottomButtonLayout.addWidget(self.runButton) @@ -1011,7 +1162,8 @@ class ConfigDialog(QtWidgets.QDialog): self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"house.png"),getMessage("basics-label"))) self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"control_pause_blue.png"),getMessage("readiness-label"))) self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"film_link.png"),getMessage("sync-label"))) - self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"comments.png"),getMessage("messages-label"))) + self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"user_comment.png"), getMessage("chat-label"))) + self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"error.png"),getMessage("messages-label"))) self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"cog.png"),getMessage("misc-label"))) self.tabListLayout.addWidget(self.tabListWidget) self.tabListFrame.setLayout(self.tabListLayout) @@ -1111,6 +1263,12 @@ class ConfigDialog(QtWidgets.QDialog): self.QtWidgets = QtWidgets self.QtGui = QtGui self.error = error + if isWindows(): + resourcespath = utils.findWorkingDir() + "\\resources\\" + else: + resourcespath = utils.findWorkingDir() + u"/resources/" + self.posixresourcespath = utils.findWorkingDir().replace(u"\\","/") + u"/resources/" + self.resourcespath = resourcespath super(ConfigDialog, self).__init__() @@ -1130,6 +1288,7 @@ class ConfigDialog(QtWidgets.QDialog): self.addBasicTab() self.addReadinessTab() self.addSyncTab() + self.addChatTab() self.addMessageTab() self.addMiscTab() self.tabList() diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 868a43a..332a0ca 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -38,7 +38,7 @@ class ConsoleUI(threading.Thread): self.PromptResult = data self.promptMode.set() elif self._syncplayClient: - self._executeCommand(data) + self.executeCommand(data) except EOFError: pass @@ -136,7 +136,7 @@ class ConsoleUI(threading.Thread): return True return False - def _executeCommand(self, data): + def executeCommand(self, data): command = re.match(constants.UI_COMMAND_REGEX, data) if not command: return @@ -145,7 +145,7 @@ class ConsoleUI(threading.Thread): self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos elif command.group('command') in constants.COMMANDS_LIST: - self._syncplayClient.getUserList() + self.getUserlist() elif command.group('command') in constants.COMMANDS_CHAT: message= command.group('parameter') self._syncplayClient.sendChat(message) @@ -158,8 +158,8 @@ class ConsoleUI(threading.Thread): room = self._syncplayClient.userlist.currentUser.file["name"] else: room = self._syncplayClient.defaultRoom - self._syncplayClient.setRoom(room, resetAutoplay=True) + self._syncplayClient.ui.updateRoomName(room) self._syncplayClient.sendRoom() elif command.group('command') in constants.COMMANDS_CREATE: roombasename = command.group('parameter') @@ -190,4 +190,6 @@ class ConsoleUI(threading.Thread): self.showMessage(getMessage("commandlist-notification/chat"), True) self.showMessage(getMessage("syncplay-version-notification").format(syncplay.version), True) self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True) - + + def getUserlist(self): + self._syncplayClient.getUserList() \ No newline at end of file diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 17ac512..7bd55e6 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -16,10 +16,27 @@ import os from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize, isURL from functools import wraps from twisted.internet import task -if isMacOS() and IsPySide: +from syncplay.ui.consoleUI import ConsoleUI +if isMacOS() and IsPySide: from Foundation import NSURL lastCheckedForUpdates = None +class ConsoleInGUI(ConsoleUI): + def showMessage(self, message, noTimestamp=False): + self._syncplayClient.ui.showMessage(message, True) + + def showDebugMessage(self, message): + self._syncplayClient.ui.showDebugMessage(message) + + def showErrorMessage(self, message, criticalerror=False): + self._syncplayClient.ui.showErrorMessage(message, criticalerror) + + def updateRoomName(self, room=""): + self._syncplayClient.ui.updateRoomName(room) + + def getUserlist(self): + self._syncplayClient.showUserList(self) + class UserlistItemDelegate(QtWidgets.QStyledItemDelegate): def __init__(self): QtWidgets.QStyledItemDelegate.__init__(self) @@ -84,15 +101,14 @@ class UserlistItemDelegate(QtWidgets.QStyledItemDelegate): QtWidgets.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex) class AboutDialog(QtWidgets.QDialog): - - def __init__(self, parent=None): + def __init__(self, parent=None): super(AboutDialog, self).__init__(parent) if isMacOS(): self.setWindowTitle("") else: self.setWindowTitle(getMessage("about-dialog-title")) if isWindows(): - self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) nameLabel = QtWidgets.QLabel("
Syncplay
") nameLabel.setFont(QtGui.QFont("Helvetica", 20)) linkLabel = QtWidgets.QLabel("
syncplay.pl
") @@ -101,7 +117,7 @@ class AboutDialog(QtWidgets.QDialog): licenseLabel = QtWidgets.QLabel("

Copyright © 2017 Syncplay

" + getMessage("about-dialog-license-text") + "

") aboutIconPixmap = QtGui.QPixmap(resourcespath + u"syncplay.png") aboutIconLabel = QtWidgets.QLabel() - aboutIconLabel.setPixmap(aboutIconPixmap.scaled(120, 120, Qt.KeepAspectRatio)) + aboutIconLabel.setPixmap(aboutIconPixmap.scaled(120, 120, Qt.KeepAspectRatio)) aboutLayout = QtWidgets.QGridLayout() aboutLayout.addWidget(aboutIconLabel, 0, 0, 4, 2) aboutLayout.addWidget(nameLabel, 0, 2, 1, 2) @@ -115,22 +131,22 @@ class AboutDialog(QtWidgets.QDialog): dependenciesButton = QtWidgets.QPushButton(getMessage("about-dialog-dependencies")) dependenciesButton.setAutoDefault(False) dependenciesButton.clicked.connect(self.openDependencies) - aboutLayout.addWidget(dependenciesButton, 4, 3) + aboutLayout.addWidget(dependenciesButton, 4, 3) aboutLayout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) - self.setSizeGripEnabled(False) + self.setSizeGripEnabled(False) self.setLayout(aboutLayout) - def openLicense(self): - if isWindows(): - QtGui.QDesktopServices.openUrl(QUrl("file:///" + resourcespath + u"license.rtf")) - else: - QtGui.QDesktopServices.openUrl(QUrl("file://" + resourcespath + u"license.rtf")) + def openLicense(self): + if isWindows(): + QtGui.QDesktopServices.openUrl(QUrl("file:///" + resourcespath + u"license.rtf")) + else: + QtGui.QDesktopServices.openUrl(QUrl("file://" + resourcespath + u"license.rtf")) - def openDependencies(self): + def openDependencies(self): if isWindows(): - QtGui.QDesktopServices.openUrl(QUrl("file:///" + resourcespath + u"third-party-notices.rtf")) + QtGui.QDesktopServices.openUrl(QUrl("file:///" + resourcespath + u"third-party-notices.rtf")) else: - QtGui.QDesktopServices.openUrl(QUrl("file://" + resourcespath + u"third-party-notices.rtf")) + QtGui.QDesktopServices.openUrl(QUrl("file://" + resourcespath + u"third-party-notices.rtf")) class MainWindow(QtWidgets.QMainWindow): insertPosition = None @@ -347,6 +363,8 @@ class MainWindow(QtWidgets.QMainWindow): def addClient(self, client): self._syncplayClient = client + if self.console: + self.console.addClient(client) self.roomInput.setText(self._syncplayClient.getRoom()) self.config = self._syncplayClient.getConfig() try: @@ -385,6 +403,8 @@ class MainWindow(QtWidgets.QMainWindow): self.chatInput.setReadOnly(True) if not featureList["sharedPlaylists"]: self.playlistGroup.setEnabled(False) + self.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH) + self.roomInput.setMaxLength(constants.MAX_ROOM_NAME_LENGTH) def showMessage(self, message, noTimestamp=False): message = unicode(message) @@ -1126,10 +1146,22 @@ class MainWindow(QtWidgets.QMainWindow): self._syncplayClient.playlist.changePlaylist(newPlaylist) self._syncplayClient.fileSwitch.updateInfo() + def executeCommand(self, command): + self.showMessage(u"/{}".format(command)) + self.console.executeCommand(command) + def sendChatMessage(self): - if self.chatInput.text() <> "": - self._syncplayClient.sendChat(self.chatInput.text()) - self.chatInput.setText("") + chatText = self.chatInput.text() + self.chatInput.setText("") + if chatText <> "": + if chatText[:1] == "/" and chatText <> "/": + command = chatText[1:] + if command and command[:1] == "/": + chatText = chatText[1:] + else: + self.executeCommand(command) + return + self._syncplayClient.sendChat(chatText) def addTopLayout(self, window): window.topSplit = self.topSplitter(Qt.Horizontal, self) @@ -1705,6 +1737,8 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self): super(MainWindow, self).__init__() + self.console = ConsoleInGUI() + self.console.setDaemon(True) self.newWatchlist = [] self.publicServerList = [] self.lastCheckedForUpdates = None diff --git a/syncplay/utils.py b/syncplay/utils.py index 402a44d..72fa947 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -130,6 +130,13 @@ def formatSize (bytes, precise=False): def isASCII(s): return all(ord(c) < 128 for c in s) +def findResourcePath(resourceName): + if resourceName == "syncplay.lua": + resourcePath = os.path.join(findWorkingDir(), "lua", "intf" , "resources", resourceName) + else: + resourcePath = os.path.join(findWorkingDir(),"resources", resourceName) + return resourcePath + def findWorkingDir(): frozen = getattr(sys, 'frozen', '') if not frozen: @@ -152,6 +159,14 @@ def getResourcesPath(): resourcespath = getResourcesPath() posixresourcespath = findWorkingDir().replace(u"\\","/") + u"/resources/" +def getDefaultMonospaceFont(): + if platform.system() == "Windows": + return constants.DEFAULT_WINDOWS_MONOSPACE_FONT + elif platform.system() == "Darwin": + return constants.DEFAULT_OSX_MONOSPACE_FONT + else: + return constants.FALLBACK_MONOSPACE_FONT + def limitedPowerset(s, minLength): return itertools.chain.from_iterable(itertools.combinations(s, r) for r in xrange(len(s), minLength, -1)) @@ -189,20 +204,28 @@ def blackholeStdoutForFrozenWindow(): def truncateText(unicodeText, maxLength): try: - unicodeText = unicodedata.normalize('NFC', unicodeText) + unicodeText = unicodeText.decode('utf-8') except: pass try: - maxSaneLength= maxLength*5 - if len(unicodeText) > maxSaneLength: - unicodeText = unicode(unicodeText.encode("utf-8")[:maxSaneLength], "utf-8", errors="ignore") - while len(unicodeText) > maxLength: - unicodeText = unicode(unicodeText.encode("utf-8")[:-1], "utf-8", errors="ignore") - return unicodeText + return(unicode(unicodeText.encode("utf-8"), "utf-8", errors="ignore")[:maxLength]) except: pass return "" +def splitText(unicodeText, maxLength): + try: + unicodeText = unicodeText.decode('utf-8') + except: + pass + try: + unicodeText = unicode(unicodeText.encode("utf-8"), "utf-8", errors="ignore") + unicodeArray = [unicodeText[i:i + maxLength] for i in range(0, len(unicodeText), maxLength)] + return(unicodeArray) + except: + pass + return [""] + # Relate to file hashing / difference checking: def stripfilename(filename, stripURL):