From e1b12327fde3bf7eee857cb4a5d9fc213a6d2244 Mon Sep 17 00:00:00 2001 From: alby128 Date: Mon, 8 Jan 2018 15:25:29 +0100 Subject: [PATCH 1/6] Added automatic version numbering in Travis --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9995dd2..ee035e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,8 @@ before_deploy: - mv resources/macos_vlc_install.command resources/.macos_vlc_install.command - mv resources/lua/intf/syncplay.lua resources/lua/intf/.syncplay.lua - mv resources/macOS_readme.pdf resources/.macOS_readme.pdf -- dmgbuild -s appdmg.py "Syncplay" dist_dmg/Syncplay_$(ver)_macOS.dmg +- export VER="$(cat syncplay/__init__.py | awk '/version/ {gsub("\047", "", $3); print $NF}')" +- dmgbuild -s appdmg.py "Syncplay" dist_dmg/Syncplay_${VER}_macOS.dmg deploy: on: master From 79c34f823e78d02d2f9b69c1a5ce9fe90723b7f2 Mon Sep 17 00:00:00 2001 From: Etoh Date: Sat, 13 Jan 2018 14:45:43 +0000 Subject: [PATCH 2/6] Avoid error on unexpected last update check format when changing between Python versions (#170) --- syncplay/ui/GuiConfiguration.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 386326d..7a62c22 100755 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -289,13 +289,16 @@ class ConfigDialog(QtWidgets.QDialog): def loadLastUpdateCheckDate(self): settings = QSettings("Syncplay", "Interface") settings.beginGroup("Update") - self.lastCheckedForUpdates = settings.value("lastCheckedQt", None) - if self.lastCheckedForUpdates: - if self.config["lastCheckedForUpdates"] is not None and self.config["lastCheckedForUpdates"] is not "": - if self.lastCheckedForUpdates.toPython() > datetime.strptime(self.config["lastCheckedForUpdates"], "%Y-%m-%d %H:%M:%S.%f"): + try: + self.lastCheckedForUpdates = settings.value("lastCheckedQt", None) + if self.lastCheckedForUpdates: + if self.config["lastCheckedForUpdates"] is not None and self.config["lastCheckedForUpdates"] is not "": + if self.lastCheckedForUpdates.toPython() > datetime.strptime(self.config["lastCheckedForUpdates"], "%Y-%m-%d %H:%M:%S.%f"): + self.config["lastCheckedForUpdates"] = self.lastCheckedForUpdates.toString("yyyy-MM-d HH:mm:ss.z") + else: self.config["lastCheckedForUpdates"] = self.lastCheckedForUpdates.toString("yyyy-MM-d HH:mm:ss.z") - else: - self.config["lastCheckedForUpdates"] = self.lastCheckedForUpdates.toString("yyyy-MM-d HH:mm:ss.z") + except: + self.lastCheckedForUpdates = None def loadSavedPublicServerList(self): settings = QSettings("Syncplay", "Interface") From f5c6b8fcf998835f6976f06d2b0ebd0a2fa768cc Mon Sep 17 00:00:00 2001 From: Etoh Date: Sun, 14 Jan 2018 17:54:18 +0000 Subject: [PATCH 3/6] Make repl.lua copyright notice more prominent --- GNUmakefile | 1 + buildPy2exe.py | 4 +- resources/error.png | Bin 0 -> 666 bytes resources/syncplayintf.lua | 979 +++++++++++++++++++++++++++++ resources/user_comment.png | Bin 0 -> 743 bytes syncplay/__init__.py | 4 +- syncplay/client.py | 169 +++-- syncplay/constants.py | 51 +- syncplay/messages.py | 4 +- syncplay/messages_de.py | 65 +- syncplay/messages_en.py | 71 ++- syncplay/messages_ru.py | 73 ++- syncplay/players/basePlayer.py | 8 +- syncplay/players/mpc.py | 123 ++-- syncplay/players/mplayer.py | 44 +- syncplay/players/mpv.py | 55 +- syncplay/players/vlc.py | 10 +- syncplay/protocols.py | 47 +- syncplay/server.py | 11 +- syncplay/ui/ConfigurationGetter.py | 67 +- syncplay/ui/GuiConfiguration.py | 171 ++++- syncplay/ui/consoleUI.py | 12 +- syncplay/ui/gui.py | 70 ++- syncplay/utils.py | 37 +- 24 files changed, 1858 insertions(+), 218 deletions(-) create mode 100644 resources/error.png create mode 100644 resources/syncplayintf.lua create mode 100644 resources/user_comment.png 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 0000000000000000000000000000000000000000..628cf2dae3d419ae220c8928ac71393b480745a3 GIT binary patch literal 666 zcmV;L0%iS)P)eOSYYtbpBV}~vsBnU!_?2tr-P=|^T zED%wc9ezHgW@NMb!^uT_|SvCpFLJylbx zY%bpaTGI8IYXMN$9w<3j9VkA~NYOKEQXsj?6a9_hcwfU$acAhJhB)zb_w@MVUEy@S zX&I>K-R!bhu3?(6bHWIg$HEl7{9g>>&l_qdd+UYb(1~BCo9LptNq&8>!yoJ3Ui(i5 zRJ|XnYBklL!{@$-7=3mJ>P@1c=7Oc79e-V7yf+%lD2!I;Y&nXBZ>=B!5?CB>LvEx6 znI%n)qqi$#X#wKB(U7XP2P=+4{b@j#r%9-K(8UqtSDk>0UKzf*HM9yqMZ1D!$2MdZ zR=`U>0zhOH1XqN?nY@AQqB7)Fp4{v&dKXvb43hZKvnN8;Po;+jY*}~*Z|W9Q0W%{D z^T}Cc<|r(Su=1K=P5>Z4 zg`et&Va}tdzBS-G-ZcO)zCWpJvGQwrHZ`@wpM420ac@bI5~KkTFfGEM3sPWO8co4^fI6lPnA)Y{ef%@{+SnoUk0+dW+*{8WvF8}}l07*qoM6N<$g7cXs A&j0`b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e54ebebafb5072fabac9a0f3d8a79fcee3265f9f GIT binary patch literal 743 zcmV?P)vCi#|P&Xm-dkucwL z3)87{8iWe96huvPHfK`KOdC2Z({T6vJ9pwDx$D4>d(Pqff6w7Lmj{5i6;ZyPPpPN; zroaW=6d#@oL2Fa53F~$Su10(RG%K0p3VTuP3?Z=nBA8z$uq+XLUL^QrC74`bU|!e| zr>hK{)%Q!vdmIO5Z3JIvaOyjOX`X@c8-ua03`Q&)f&%p*{(A$q`ZTTjk%q_T7>v^J zu!R-a9fFLScYlKkNBP_Cob=9m9JLVoC-?c{)eOtMnh7qNN{ejy2sM{pS^mgFHJm@(buuM4>=<5Vr$&Kzw{B?uPr; z(1Yf=#g)zADkWnx=MR%ykl| z3Ui42k+O2{bCn)01-s5Sxp|z{G2di&KT(_M6;$EI zDL57JFf}cw4bP1P$pgTRKH$0@h|~aA>j`qZ2*kU5t2EVD5#~@VNhqx{vz8ethDD-=+1vnemftUBA zF;N!Q%PBB5B=KLB#QO(CHe?;R+-C8M?ppDW>R$5`cCPq@YpusFRTaH1i9Kv;l<>I( Ze*oTy+;kdDB`N>_002ovPDHLkV1l3CM+g7_ literal 0 HcmV?d00001 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): From 9b54123458249723c20ccd57f44e5bac90c246c7 Mon Sep 17 00:00:00 2001 From: Etoh Date: Sun, 14 Jan 2018 17:54:18 +0000 Subject: [PATCH 4/6] Merge chat-OSD --- syncplay/players/mpv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncplay/players/mpv.py b/syncplay/players/mpv.py index d8d13c4..4872544 100644 --- a/syncplay/players/mpv.py +++ b/syncplay/players/mpv.py @@ -314,4 +314,4 @@ class NewMpvPlayer(OldMpvPlayer): if self.fileLoaded == True and self.lastLoadedTime != None and time.time() > (self.lastLoadedTime + constants.MPV_NEWFILE_IGNORE_TIME): return True else: - return False \ No newline at end of file + return False From 43622a402d8c2ed4f62185e55f7887308aa69823 Mon Sep 17 00:00:00 2001 From: Etoh Date: Sun, 14 Jan 2018 18:22:32 +0000 Subject: [PATCH 5/6] Reflect current Syncplay situation in readme --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c172838..77d9ca1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Syncplay -Solution to synchronize video playback across multiple instances of mplayer2, mpv, Media Player Classic (MPC-HC) and VLC over the Internet. +Solution to synchronize video playback across multiple instances of mpv, VLC, MPC-HC, MPC-BE and mplayer2 over the Internet. ## Official website http://syncplay.pl @@ -12,13 +12,14 @@ http://syncplay.pl/download/ Syncplay synchronises the position and play state of multiple media players so that the viewers can watch the same thing at the same time. This means that when one person pauses/unpauses playback or seeks (jumps position) within their media player then this will be replicated across all media players connected to the same server and in the same 'room' (viewing session). -When a new person joins they will also be synchronised. +When a new person joins they will also be synchronised. Syncplay also includes text-base chat so you can discuss a video as you watch it (or you could use third-party Voice over IP softtware to talk over a video). ## What it doesn't do -Syncplay does not use video streaming or file sharing so each user must have their own copy of the media to be played. Syncplay does not synchronise player configuration, audio/subtitle track choice, playback rate, volume or filters. Furthermore, users must manually choose what file to play as Syncplay does not synchronise which file is open. Finally, Syncplay does not provide a voice or text-based chat platform to allow for discussion during playback as Syncplay is intended to be used in conjunction with third-party communication solutions such as IRC and Mumble. +Syncplay is not a file sharing service. ## Authors -* *Concept and principal Syncplay developer* - Uriziel. -* *Other Syncplay coders* - daniel-123, Et0h. +* *Initial concept and core internals developer* - Uriziel. +* *GUI design and current lead developer* - Et0h. * *Original SyncPlay code* - Tomasz Kowalczyk (Fluxid), who developed SyncPlay at https://github.com/fluxid/syncplay +* *Other contributors* - See http://syncplay.pl/about/development/ From d73204870ffc4eb2e52dd250e86e10a7e5cf739c Mon Sep 17 00:00:00 2001 From: Etoh Date: Sun, 14 Jan 2018 20:11:20 +0000 Subject: [PATCH 6/6] Typofix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77d9ca1..173b7e2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ http://syncplay.pl/download/ Syncplay synchronises the position and play state of multiple media players so that the viewers can watch the same thing at the same time. This means that when one person pauses/unpauses playback or seeks (jumps position) within their media player then this will be replicated across all media players connected to the same server and in the same 'room' (viewing session). -When a new person joins they will also be synchronised. Syncplay also includes text-base chat so you can discuss a video as you watch it (or you could use third-party Voice over IP softtware to talk over a video). +When a new person joins they will also be synchronised. Syncplay also includes text-based chat so you can discuss a video as you watch it (or you could use third-party Voice over IP software to talk over a video). ## What it doesn't do