diff --git a/AppApi.cs b/AppApi.cs index 2a0afb4c..24b37ea5 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -229,20 +229,15 @@ namespace VRCX return CpuMonitor.Instance.CpuUsage; } - public void CacheImage(string Base64File) - { - String Icon = Path.Combine(Program.AppDataDirectory, "cache\\toast"); - File.WriteAllBytes(Icon, Convert.FromBase64String(Base64File)); - } - - public void DesktopNotification(string BoldText, string Text, bool Image) + public void DesktopNotification(string BoldText, string Text, string Image) { XmlDocument toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText02); XmlNodeList stringElements = toastXml.GetElementsByTagName("text"); String imagePath = Path.Combine(Program.BaseDirectory, "VRCX.ico"); - if (Image) + if (!String.IsNullOrEmpty(Image)) { imagePath = Path.Combine(Program.AppDataDirectory, "cache\\toast"); + File.WriteAllBytes(imagePath, Convert.FromBase64String(Image)); } stringElements[0].AppendChild(toastXml.CreateTextNode(BoldText)); stringElements[1].AppendChild(toastXml.CreateTextNode(Text)); @@ -268,14 +263,20 @@ namespace VRCX public string sourceApp { get; set; } } - public void XSNotification(string Title, string Content, int Timeout, bool Image) + public void XSNotification(string Title, string Content, int Timeout, string Image) { - bool UseBase64Icon = true; - String Icon = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHaGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo1NTE2MWIyMi1hYzgxLTY3NDYtODAyYi1kODIzYWFmN2RjYjciIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3ZjJjNTA2ZS02YTVhLWRhNGEtOTg5Mi02NDZiMzQ0MGQxZTgiPiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT5hZG9iZTpkb2NpZDpwaG90b3Nob3A6NmJmOGE5MTgtY2QzZS03OTRjLTk3NzktMzM0YjYwZWJiNTYyPC9yZGY6bGk+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N2YyYzUwNmUtNmE1YS1kYTRhLTk4OTItNjQ2YjM0NDBkMWU4IiBzdEV2dDp3aGVuPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmJhM2ZjODI3LTM0ZjQtYjU0OC05ZGFiLTZhMTZlZmQzZjAxMSIgc3RFdnQ6d2hlbj0iMjAyMS0wNC0wOFQxNTowMTozMSsxMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHN0RXZ0OndoZW49IjIwMjEtMDQtMDhUMTY6MzM6MTArMTI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4XAd9sAAAFM0lEQVR42u2aWUhjVxjHjVpf3Iraoh3c4ksFx7ZYahV8EHEBqdQHFdsHQRRxpcyDIDNFpdSK+iBKUcTpmy/iglVrtT4oYsEq7hP3RGXcqqY6invy9Xy3OdPEE5PY5pKb5P7hTyA5y/1+Ofc7y70OAOBgz3YQAYgARAAiABGACEAEIAIQAYgADBT6V4HErcRbxCAwy4nriN/DC+UDADb8swADv++fiN3MDeAJ8be0k9HRUbi4uACUWq22qFFvzt5AZ1enNoSvzJ4DiJ5j412dXSBUVf9QTQH08gHgF2x8b2/P0nGqNGa0ML9AAazyAeA3bPzg4MDoFV5fX8PZ2RlcXl7qGL83JjKsVeT2UpHyaqxzdXXFtUVvOVpMYx3JFfK3CZEPAL9i4/v7+0aDwDL5+fmQl5cHBQUFnHNzc6GsrAzW19cNBQ8dHR3q7OxsFamvxnrFxcWQnp4O4+PjRvtdW1ujANYtCgBVWlqqN0vn5ORw/6o+TU1Nga+vL1MnMTERtre3rQvA3d0dZGZmMsG4ublBW1sbU/7k5ATi4+OZ8uHh4bC5uWlSn4ICQC/I39+fCSo0NBRWV1d1M3h1NVPOw8MDenp6HtWfoACg8N92dnZmgisqKuISI2pkZAS8vLyYMngb3dzcWDcAvBUKCwuZ4FxdXWFwcJDLB1FRUczvcXFxcHx8/Ki+BAkAtbW1BZGRkUyQsbGx3Gzh5OSk831QUJBJWd9qAKD6+/vB29tbJ1CJRMIE7+7uDk1NTf+pD0EDwFuhoqKCC9rQZiYrKwtub29tDwBqZ2cHUlNTwdHRkQkcwURHRxtcKFk9ANTAwAB4enoyAHCmqKys/F9tCx4ATnuY9B4a/mFhYTA3N2e7AFpaWoweaKSkpHCbH5sDMDMzw01vxgC4uLhAfX29oAHo3Yoa0vn5OSQnJzPBZmRkQFpaGjMz+Pn5wdjYmGAB3D0WQG1tLRM8Bjk7OwsKhQICAwOZ3xMSEkw6e7AEANVjAAwPD3ObmvsBVlVVgUr1z8FOQ0MD8zsukMrLyx+1JhBcDtjd3YWIiAgmOLwdtP9dTHpJSUl6d4M4bVolADzdKSkpYYIKCAjgdn/3NT8/Dz4+Pkz5mJgYkw5DBAUAh3ZzczOzDcYVYE1NzYNL5bq6Or1LZVw7nJ6eWg8APMHBRQ0ehkilUggODuaSHp4QGdriHh0dcTMDlsV6ISEhXF0cGb29vRYHMGTqqTCWmZiYgKWlJVheXgaZTMatAw4PD43WVSqVMD09zdVD48kRtiWXy98mzYe0Id+gADb4ADCMjSuPlYJ9MKLYVFAAm3wAaMbGFxcXBQugu7ubAviDDwCfY+N4Ro/DVGjCmUIrcX7P1+PxfdpJ68tWGBoagr7+PrMZH3DiwglnBGPCtQOWxeSIM45W8IvEUr4AfEG8xPcj7sbGRqMAVpZX9NWdIv6Ur/cDqD4k/o64j/h34jEzeUTTHhdMX2+fQQCyVzIa9KXmwe0z4hB6kXwCQL2DLyEQ+xK/byZ7EfsRN1AICwsLDwLAKVZTDkfkZ8RO2hfINwA+9YQ+iUYf/nloDADe80/vN2LNAFCRxGsYx4vnL/QmRS0Ar4g/sjUAqC/pKGhvb7dLAKhyCmFyctIuAbxL3EEhaL+eowVARvyxrQJASYlnKAS6IbInAKg44lMKAYU7Ra1p8BNbB4D6hvgGY8MlMG6PNQBWiCPsAYAL8Y96lr+4ivzAHgDQpPiS+EwikfxFPl8Tf00s4RWA+Lq8CEAEIAIQAYgARAA26b8BaVJkoY+4rDoAAAAASUVORK5CYII="; - if (Image) + bool UseBase64Icon; + string Icon; + if (String.IsNullOrEmpty(Image)) + { + UseBase64Icon = true; + Icon = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHaGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo1NTE2MWIyMi1hYzgxLTY3NDYtODAyYi1kODIzYWFmN2RjYjciIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3ZjJjNTA2ZS02YTVhLWRhNGEtOTg5Mi02NDZiMzQ0MGQxZTgiPiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT5hZG9iZTpkb2NpZDpwaG90b3Nob3A6NmJmOGE5MTgtY2QzZS03OTRjLTk3NzktMzM0YjYwZWJiNTYyPC9yZGY6bGk+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N2YyYzUwNmUtNmE1YS1kYTRhLTk4OTItNjQ2YjM0NDBkMWU4IiBzdEV2dDp3aGVuPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmJhM2ZjODI3LTM0ZjQtYjU0OC05ZGFiLTZhMTZlZmQzZjAxMSIgc3RFdnQ6d2hlbj0iMjAyMS0wNC0wOFQxNTowMTozMSsxMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHN0RXZ0OndoZW49IjIwMjEtMDQtMDhUMTY6MzM6MTArMTI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4XAd9sAAAFM0lEQVR42u2aWUhjVxjHjVpf3Iraoh3c4ksFx7ZYahV8EHEBqdQHFdsHQRRxpcyDIDNFpdSK+iBKUcTpmy/iglVrtT4oYsEq7hP3RGXcqqY6invy9Xy3OdPEE5PY5pKb5P7hTyA5y/1+Ofc7y70OAOBgz3YQAYgARAAiABGACEAEIAIQAYgADBT6V4HErcRbxCAwy4nriN/DC+UDADb8swADv++fiN3MDeAJ8be0k9HRUbi4uACUWq22qFFvzt5AZ1enNoSvzJ4DiJ5j412dXSBUVf9QTQH08gHgF2x8b2/P0nGqNGa0ML9AAazyAeA3bPzg4MDoFV5fX8PZ2RlcXl7qGL83JjKsVeT2UpHyaqxzdXXFtUVvOVpMYx3JFfK3CZEPAL9i4/v7+0aDwDL5+fmQl5cHBQUFnHNzc6GsrAzW19cNBQ8dHR3q7OxsFamvxnrFxcWQnp4O4+PjRvtdW1ujANYtCgBVWlqqN0vn5ORw/6o+TU1Nga+vL1MnMTERtre3rQvA3d0dZGZmMsG4ublBW1sbU/7k5ATi4+OZ8uHh4bC5uWlSn4ICQC/I39+fCSo0NBRWV1d1M3h1NVPOw8MDenp6HtWfoACg8N92dnZmgisqKuISI2pkZAS8vLyYMngb3dzcWDcAvBUKCwuZ4FxdXWFwcJDLB1FRUczvcXFxcHx8/Ki+BAkAtbW1BZGRkUyQsbGx3Gzh5OSk831QUJBJWd9qAKD6+/vB29tbJ1CJRMIE7+7uDk1NTf+pD0EDwFuhoqKCC9rQZiYrKwtub29tDwBqZ2cHUlNTwdHRkQkcwURHRxtcKFk9ANTAwAB4enoyAHCmqKys/F9tCx4ATnuY9B4a/mFhYTA3N2e7AFpaWoweaKSkpHCbH5sDMDMzw01vxgC4uLhAfX29oAHo3Yoa0vn5OSQnJzPBZmRkQFpaGjMz+Pn5wdjYmGAB3D0WQG1tLRM8Bjk7OwsKhQICAwOZ3xMSEkw6e7AEANVjAAwPD3ObmvsBVlVVgUr1z8FOQ0MD8zsukMrLyx+1JhBcDtjd3YWIiAgmOLwdtP9dTHpJSUl6d4M4bVolADzdKSkpYYIKCAjgdn/3NT8/Dz4+Pkz5mJgYkw5DBAUAh3ZzczOzDcYVYE1NzYNL5bq6Or1LZVw7nJ6eWg8APMHBRQ0ehkilUggODuaSHp4QGdriHh0dcTMDlsV6ISEhXF0cGb29vRYHMGTqqTCWmZiYgKWlJVheXgaZTMatAw4PD43WVSqVMD09zdVD48kRtiWXy98mzYe0Id+gADb4ADCMjSuPlYJ9MKLYVFAAm3wAaMbGFxcXBQugu7ubAviDDwCfY+N4Ro/DVGjCmUIrcX7P1+PxfdpJ68tWGBoagr7+PrMZH3DiwglnBGPCtQOWxeSIM45W8IvEUr4AfEG8xPcj7sbGRqMAVpZX9NWdIv6Ur/cDqD4k/o64j/h34jEzeUTTHhdMX2+fQQCyVzIa9KXmwe0z4hB6kXwCQL2DLyEQ+xK/byZ7EfsRN1AICwsLDwLAKVZTDkfkZ8RO2hfINwA+9YQ+iUYf/nloDADe80/vN2LNAFCRxGsYx4vnL/QmRS0Ar4g/sjUAqC/pKGhvb7dLAKhyCmFyctIuAbxL3EEhaL+eowVARvyxrQJASYlnKAS6IbInAKg44lMKAYU7Ra1p8BNbB4D6hvgGY8MlMG6PNQBWiCPsAYAL8Y96lr+4ivzAHgDQpPiS+EwikfxFPl8Tf00s4RWA+Lq8CEAEIAIQAYgARAA26b8BaVJkoY+4rDoAAAAASUVORK5CYII="; + } + else { UseBase64Icon = false; Icon = Path.Combine(Program.AppDataDirectory, "cache\\toast"); + File.WriteAllBytes(Icon, Convert.FromBase64String(Image)); } IPAddress broadcastIP = IPAddress.Parse("127.0.0.1"); @@ -314,13 +315,28 @@ namespace VRCX System.Environment.Exit(0); } - public bool checkForUpdateZip() + public bool CheckForUpdateZip() { if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe"))) return true; return false; } + public void ExecuteAppFunction(string function, string json) + { + MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", json); + } + + public void ExecuteVrFeedFunction(string function, string json) + { + VRCXVR._browser1.ExecuteScriptAsync($"$app.{function}", json); + } + + public void ExecuteVrOverlayFunction(string function, string json) + { + VRCXVR._browser2.ExecuteScriptAsync($"$app.{function}", json); + } + public void SetStartup(bool enabled) { try diff --git a/Discord.cs b/Discord.cs index fca55100..e451a04a 100644 --- a/Discord.cs +++ b/Discord.cs @@ -1,4 +1,4 @@ -// Copyright(c) 2019 pypy. All rights reserved. +// Copyright(c) 2019 pypy. All rights reserved. // // This work is licensed under the terms of the MIT license. // For a copy, see . @@ -17,6 +17,7 @@ namespace VRCX private DiscordRpcClient m_Client; private Timer m_Timer; private bool m_Active; + public static string DiscordAppId; static Discord() { @@ -78,7 +79,7 @@ namespace VRCX { if (m_Client == null) { - m_Client = new DiscordRpcClient("525953831020920832"); + m_Client = new DiscordRpcClient(DiscordAppId); if (m_Client.Initialize() == false) { m_Client.Dispose(); @@ -126,7 +127,7 @@ namespace VRCX } } - public void SetAssets(string largeKey, string largeText, string smallKey, string smallText) + public void SetAssets(string largeKey, string largeText, string smallKey, string smallText, string partyId, int partySize, int partyMax, string buttonText, string buttonUrl, string appId) { m_Lock.EnterWriteLock(); try @@ -139,13 +140,35 @@ namespace VRCX else { if (m_Presence.Assets == null) - { m_Presence.Assets = new Assets(); - } + if (m_Presence.Party == null) + m_Presence.Party = new Party(); m_Presence.Assets.LargeImageKey = largeKey; m_Presence.Assets.LargeImageText = largeText; m_Presence.Assets.SmallImageKey = smallKey; m_Presence.Assets.SmallImageText = smallText; + m_Presence.Party.ID = partyId; + m_Presence.Party.Size = partySize; + m_Presence.Party.Max = partyMax; + Button[] Buttons = { }; + if (!string.IsNullOrEmpty(buttonUrl)) + { + Buttons = new Button[] + { + new Button() { Label = buttonText, Url = buttonUrl } + }; + } + m_Presence.Buttons = Buttons; + if (DiscordAppId != appId) + { + DiscordAppId = appId; + if (m_Client != null) + { + m_Client.Dispose(); + m_Client = null; + } + Update(); + } } } finally diff --git a/LogWatcher.cs b/LogWatcher.cs index b7f23005..10fa67eb 100644 --- a/LogWatcher.cs +++ b/LogWatcher.cs @@ -3,6 +3,7 @@ // This work is licensed under the terms of the MIT license. // For a copy, see . +using CefSharp; using System; using System.Collections.Generic; using System.Globalization; @@ -30,6 +31,8 @@ namespace VRCX private readonly List m_LogList; private Thread m_Thread; private bool m_ResetLog; + private bool m_FirstRun = true; + private static DateTime tillDate = DateTime.Now; // NOTE // FileSystemWatcher() is unreliable @@ -64,6 +67,17 @@ namespace VRCX thread.Interrupt(); thread.Join(); } + + public void Reset() + { + m_ResetLog = true; + m_Thread?.Interrupt(); + } + + public void SetDateTill(string date) + { + tillDate = DateTime.Parse(date); + } private void ThreadLoop() { @@ -86,6 +100,7 @@ namespace VRCX { if (m_ResetLog == true) { + m_FirstRun = true; m_ResetLog = false; m_LogContextMap.Clear(); m_LogListLock.EnterWriteLock(); @@ -109,26 +124,17 @@ namespace VRCX // sort by creation time Array.Sort(fileInfos, (a, b) => a.CreationTimeUtc.CompareTo(b.CreationTimeUtc)); - var utcNow = DateTime.UtcNow; - var minLimitDateTime = utcNow.AddDays(-7d); - var minRefreshDateTime = utcNow.AddMinutes(-3d); - foreach (var fileInfo in fileInfos) { - var lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; - - if (lastWriteTimeUtc < minLimitDateTime) + fileInfo.Refresh(); + if (fileInfo.Exists == false) { continue; } - if (lastWriteTimeUtc >= minRefreshDateTime) + if (DateTime.Compare(fileInfo.LastWriteTime, tillDate) < 0) { - fileInfo.Refresh(); - if (fileInfo.Exists == false) - { - continue; - } + continue; } if (m_LogContextMap.TryGetValue(fileInfo.Name, out LogContext logContext) == true) @@ -155,6 +161,8 @@ namespace VRCX { m_LogContextMap.Remove(name); } + + m_FirstRun = false; } private void ParseLog(FileInfo fileInfo, LogContext logContext) @@ -184,17 +192,33 @@ namespace VRCX continue; } + if (DateTime.TryParseExact( + line.Substring(0, 19), + "yyyy.MM.dd HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out DateTime lineDate + )) + { + if (DateTime.Compare(lineDate, tillDate) <= 0) + { + continue; + } + } + var offset = 34; if (line[offset] == '[') { if (ParseLogOnPlayerJoinedOrLeft(fileInfo, logContext, line, offset) == true || ParseLogLocation(fileInfo, logContext, line, offset) == true || + ParseLogLocationDestination(fileInfo, logContext, line, offset) == true || ParseLogPortalSpawn(fileInfo, logContext, line, offset) == true || ParseLogNotification(fileInfo, logContext, line, offset) == true || ParseLogJoinBlocked(fileInfo, logContext, line, offset) == true || ParseLogAvatarPedestalChange(fileInfo, logContext, line, offset) == true || ParseLogVideoError(fileInfo, logContext, line, offset) == true || - ParseLogVideoPlay(fileInfo, logContext, line, offset) == true) + ParseLogVideoPlay(fileInfo, logContext, line, offset) == true || + ParseLogWorldVRCX(fileInfo, logContext, line, offset) == true) { continue; } @@ -221,6 +245,12 @@ namespace VRCX m_LogListLock.EnterWriteLock(); try { + if (!m_FirstRun) + { + var logLine = System.Text.Json.JsonSerializer.Serialize(item); + if (MainForm.Instance != null && MainForm.Instance.Browser != null) + MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine); + } m_LogList.Add(item); } finally @@ -244,7 +274,7 @@ namespace VRCX dt = DateTime.UtcNow; } - return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}"; + return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'}"; } private bool ParseLogLocation(FileInfo fileInfo, LogContext logContext, string line, int offset) @@ -284,6 +314,29 @@ namespace VRCX return false; } + private bool ParseLogLocationDestination(FileInfo fileInfo, LogContext logContext, string line, int offset) + { + // 2021.09.02 00:02:12 Log - [Behaviour] Destination set: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd:15609~private(usr_032383a7-748c-4fb2-94e4-bcb928e5de6b)~nonce(72CC87D420C1D49AEFFBEE8824C84B2DF0E38678E840661E) + // 2021.09.02 00:49:15 Log - [Behaviour] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd + + if (string.Compare(line, offset, "[Behaviour] Destination fetching: ", 0, 34, StringComparison.Ordinal) == 0) + { + var location = line.Substring(offset + 34); + + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "location-destination", + location + }); + + return true; + } + + return false; + } + private bool ParseLogOnPlayerJoinedOrLeft(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2020.10.31 23:36:58 Log - [NetworkManager] OnPlayerJoined pypy @@ -515,6 +568,28 @@ namespace VRCX return true; } + private bool ParseLogWorldVRCX(FileInfo fileInfo, LogContext logContext, string line, int offset) + { + // [VRCX] VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/-Q3pdlsQxOk.mp4",0.5338666,260.6938,"1339 : Le Freak (Random)" + + if (string.Compare(line, offset, "[VRCX] ", 0, 7, StringComparison.Ordinal) == 0) + { + var data = line.Substring(offset + 7); + + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "vrcx", + data + }); + + return true; + } + + return false; + } + private bool ParseLogSDK2VideoPlay(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2021.04.23 13:12:25 Log - User Natsumi-sama added URL https://www.youtube.com/watch?v=dQw4w9WgXcQ @@ -579,14 +654,10 @@ namespace VRCX return true; } - public void Reset() - { - m_ResetLog = true; - m_Thread?.Interrupt(); - } - public string[][] Get() { + Update(); + if (m_ResetLog == false && m_LogList.Count > 0) { diff --git a/VRCXVR.cs b/VRCXVR.cs index a8e38694..8d5a9dd3 100644 --- a/VRCXVR.cs +++ b/VRCXVR.cs @@ -34,8 +34,8 @@ namespace VRCX private Device _device; private Texture2D _texture1; private Texture2D _texture2; - private OffScreenBrowser _browser1; - private OffScreenBrowser _browser2; + public static OffScreenBrowser _browser1; + public static OffScreenBrowser _browser2; private bool _active; static VRCXVR() diff --git a/WebApi.cs b/WebApi.cs index 24d4fac0..11d88a94 100644 --- a/WebApi.cs +++ b/WebApi.cs @@ -147,6 +147,10 @@ namespace VRCX { request.UserAgent = value; } + else if (string.Compare(key, "Referer", StringComparison.OrdinalIgnoreCase) == 0) + { + request.Referer = value; + } else { request.Headers.Add(key, value); diff --git a/html/src/app.js b/html/src/app.js index fc18d6eb..bd40ba61 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -15,7 +15,6 @@ import locale from 'element-ui/lib/locale/lang/en'; import {v4 as uuidv4} from 'uuid'; import {appVersion} from './constants.js'; -import sharedRepository from './repository/shared.js'; import configRepository from './repository/config.js'; import webApiService from './service/webapi.js'; import gameLogService from './service/gamelog.js'; @@ -245,6 +244,17 @@ speechSynthesis.getVoices(); $appDarkStyle.href = `app.dark.css?_=${Date.now()}`; document.head.appendChild($appDarkStyle); + var getLaunchURL = function (worldId, instanceId) { + if (instanceId) { + return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( + worldId + )}&instanceId=${encodeURIComponent(instanceId)}`; + } + return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( + worldId + )}`; + }; + // // Languages // @@ -463,6 +473,16 @@ speechSynthesis.getVoices(); if (init.method === 'GET' && status === 404) { this.failedGetRequests.set(endpoint, Date.now()); } + if (status === 404 && endpoint.substring(0, 6) === 'users/') { + throw new Error("404: Can't find user!"); + } + if ( + status === 404 && + endpoint.substring(0, 7) === 'invite/' && + init.inviteId + ) { + this.expireNotification(init.inviteId); + } if (data.error === Object(data.error)) { this.$throw( data.error.status_code || status, @@ -1273,7 +1293,6 @@ speechSynthesis.getVoices(); ref }); } - sharedRepository.setString('current_user_status', ref.status); return ref; }; @@ -1498,6 +1517,24 @@ speechSynthesis.getVoices(); }); }; + /* + params: { + username: string + } + */ + API.getUserByUsername = function (params) { + return this.call(`users/${params.username}/name`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('USER', args); + return args; + }); + }; + /* params: { status: string ('active', 'offline', 'busy', 'ask me', 'join me'), @@ -2192,11 +2229,9 @@ speechSynthesis.getVoices(); // API: Notification - API.cachedNotifications = new Map(); API.isNotificationsLoading = false; API.$on('LOGIN', function () { - this.cachedNotifications.clear(); this.isNotificationsLoading = false; }); @@ -2228,13 +2263,19 @@ speechSynthesis.getVoices(); }); API.$on('NOTIFICATION:ACCEPT', function (args) { - var ref = this.cachedNotifications.get(args.params.notificationId); - if (typeof ref === 'undefined' || ref.$isDeleted) { + var array = $app.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { + if (array[i].id === args.params.notificationId) { + var ref = array[i]; + break; + } + } + if (typeof ref === 'undefined') { return; } + ref.$isExpired = true; args.ref = ref; - ref.$isDeleted = true; - this.$emit('NOTIFICATION:@DELETE', { + this.$emit('NOTIFICATION:EXPIRE', { ref, params: { notificationId: ref.id @@ -2248,13 +2289,32 @@ speechSynthesis.getVoices(); }); API.$on('NOTIFICATION:HIDE', function (args) { - var ref = this.cachedNotifications.get(args.params.notificationId); - if (typeof ref === 'undefined' && ref.$isDeleted) { + var array = $app.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { + if (array[i].id === args.params.notificationId) { + var ref = array[i]; + break; + } + } + if (typeof ref === 'undefined') { return; } args.ref = ref; - ref.$isDeleted = true; - this.$emit('NOTIFICATION:@DELETE', { + if ( + ref.type === 'friendRequest' || + ref.type === 'hiddenFriendRequest' + ) { + for (var i = array.length - 1; i >= 0; i--) { + if (array[i].id === ref.id) { + array.splice(i, 1); + break; + } + } + } else { + ref.$isExpired = true; + database.updateNotificationExpired(ref); + } + this.$emit('NOTIFICATION:EXPIRE', { ref, params: { notificationId: ref.id @@ -2263,7 +2323,13 @@ speechSynthesis.getVoices(); }); API.applyNotification = function (json) { - var ref = this.cachedNotifications.get(json.id); + var array = $app.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { + if (array[i].id === json.id) { + var ref = array[i]; + break; + } + } if (typeof ref === 'undefined') { ref = { id: '', @@ -2275,12 +2341,10 @@ speechSynthesis.getVoices(); seen: false, created_at: '', // VRCX - $isDeleted: false, $isExpired: false, // ...json }; - this.cachedNotifications.set(ref.id, ref); } else { Object.assign(ref, json); ref.$isExpired = false; @@ -2300,36 +2364,42 @@ speechSynthesis.getVoices(); return ref; }; - API.expireNotifications = function () { - for (var ref of this.cachedNotifications.values()) { - ref.$isExpired = true; + API.expireFriendRequestNotifications = function () { + var array = $app.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { if ( - ref.type === 'friendRequest' || - ref.type === 'hiddenFriendRequest' + array[i].type === 'friendRequest' || + array[i].type === 'hiddenFriendRequest' ) { - this.cachedNotifications.delete(ref.id); + array.splice(i, 1); } } }; - API.deleteExpiredNotifcations = function () { - for (var ref of this.cachedNotifications.values()) { - if (ref.$isDeleted || ref.$isExpired === false) { - continue; + API.expireNotification = function (notificationId) { + var array = $app.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { + if (array[i].id === notificationId) { + var ref = array[i]; + break; } - ref.$isDeleted = true; - this.$emit('NOTIFICATION:@DELETE', { - ref, - params: { - notificationId: ref.id - } - }); } + if (typeof ref === 'undefined') { + return; + } + ref.$isExpired = true; + database.updateNotificationExpired(ref); + this.$emit('NOTIFICATION:EXPIRE', { + ref, + params: { + notificationId: ref.id + } + }); }; API.refreshNotifications = async function () { this.isNotificationsLoading = true; - this.expireNotifications(); + this.expireFriendRequestNotifications(); var params = { n: 100, offset: 0 @@ -2356,7 +2426,6 @@ speechSynthesis.getVoices(); break; } } - this.deleteExpiredNotifcations(); this.isNotificationsLoading = false; }; @@ -2486,31 +2555,33 @@ speechSynthesis.getVoices(); }); }; - API.sendInviteResponse = function (params, inviteID) { - return this.call(`invite/${inviteID}/response`, { + API.sendInviteResponse = function (params, inviteId) { + return this.call(`invite/${inviteId}/response`, { method: 'POST', - params + params, + inviteId }).then((json) => { var args = { json, params, - inviteID + inviteId }; this.$emit('INVITE:RESPONSE:SEND', args); return args; }); }; - API.sendInviteResponsePhoto = function (params, inviteID) { - return this.call(`invite/${inviteID}/response/photo`, { + API.sendInviteResponsePhoto = function (params, inviteId) { + return this.call(`invite/${inviteId}/response/photo`, { uploadImage: true, postData: JSON.stringify(params), - imageData: $app.uploadImage + imageData: $app.uploadImage, + inviteId }).then((json) => { var args = { json, params, - inviteID + inviteId }; this.$emit('INVITE:RESPONSE:PHOTO:SEND', args); return args; @@ -2560,13 +2631,13 @@ speechSynthesis.getVoices(); }; API.getFriendRequest = function (userId) { - for (var ref of this.cachedNotifications.values()) { + var array = $app.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { if ( - ref.$isDeleted === false && - ref.type === 'friendRequest' && - ref.senderUserId === userId + array[i].type === 'friendRequest' && + array[i].senderUserId === userId ) { - return ref.id; + return array[i].id; } } return ''; @@ -3448,7 +3519,7 @@ speechSynthesis.getVoices(); }); API.$on('USER:CURRENT', function () { - if (this.webSocket === null) { + if ($app.friendLogInitStatus && this.webSocket === null) { this.getAuth(); } }); @@ -3483,6 +3554,11 @@ speechSynthesis.getVoices(); break; case 'hide-notification': + this.$emit('NOTIFICATION:HIDE', { + params: { + notificationId: content + } + }); this.$emit('NOTIFICATION:SEE', { params: { notificationId: content @@ -3902,7 +3978,7 @@ speechSynthesis.getVoices(); API.$on('SHOW_WORLD_DIALOG', (tag) => this.showWorldDialog(tag)); API.$on('SHOW_LAUNCH_DIALOG', (tag) => this.showLaunchDialog(tag)); this.updateLoop(); - this.updateGameLogLoop(); + this.getGameLogTable(); this.$nextTick(function () { this.$el.style.display = ''; if (!this.enablePrimaryPassword) { @@ -3971,13 +4047,27 @@ speechSynthesis.getVoices(); } AppApi.CheckGameRunning().then( ([isGameRunning, isGameNoVR]) => { + this.updateOpenVR(isGameRunning, isGameNoVR); if (isGameRunning !== this.isGameRunning) { this.isGameRunning = isGameRunning; - Discord.SetTimestamps(Date.now(), 0); + if (isGameRunning) { + API.currentUser.$online_for = Date.now(); + API.currentUser.$offline_for = ''; + } else { + API.currentUser.$online_for = ''; + API.currentUser.$offline_for = Date.now(); + Discord.SetActive(false); + this.autoVRChatCacheManagement(); + } + this.lastLocationReset(); + this.clearNowPlaying(); + this.updateVRConfigVars(); + } + if (isGameNoVR !== this.isGameNoVR) { + this.isGameNoVR = isGameNoVR; + this.updateVRConfigVars(); } - this.isGameNoVR = isGameNoVR; this.updateDiscord(); - this.updateOpenVR(); } ); } @@ -3991,46 +4081,30 @@ speechSynthesis.getVoices(); $app.data.debugWebRequests = false; $app.data.debugWebSocket = false; + $app.data.APILastOnline = new Map(); + $app.data.sharedFeed = { gameLog: { wrist: [], - noty: [], lastEntryDate: '' }, feedTable: { wrist: [], - noty: [], lastEntryDate: '' }, notificationTable: { wrist: [], - noty: [], lastEntryDate: '' }, friendLogTable: { wrist: [], - noty: [], lastEntryDate: '' }, pendingUpdate: false }; - $app.data.appInit = false; - $app.data.notyInit = false; - - API.$on('LOGIN', function () { - sharedRepository.setArray('wristFeed', []); - sharedRepository.setArray('notyFeed', []); - setTimeout(function () { - $app.appInit = true; - $app.updateSharedFeed(true); - $app.notyInit = true; - sharedRepository.setBool('VRInit', true); - }, 10000); - }); - $app.methods.updateSharedFeed = function (forceUpdate) { - if (!this.appInit) { + if (!this.friendLogInitStatus) { return; } this.updateSharedFeedGameLog(forceUpdate); @@ -4048,13 +4122,6 @@ speechSynthesis.getVoices(); feeds.notificationTable.wrist, feeds.friendLogTable.wrist ); - var notyFeed = []; - notyFeed = notyFeed.concat( - feeds.gameLog.noty, - feeds.feedTable.noty, - feeds.notificationTable.noty, - feeds.friendLogTable.noty - ); // OnPlayerJoining var L = API.parseLocation(this.lastLocation.location); // WebSocket dosen't update friend only instances var locationBias = Date.now() - 30000; // 30 seconds @@ -4090,16 +4157,13 @@ speechSynthesis.getVoices(); } } var playersInInstance = this.lastLocation.playerList; - if (playersInInstance.includes(ctx.displayName)) { + if (playersInInstance.has(ctx.displayName)) { continue; } var joining = true; var gameLogTable = this.gameLogTable.data; for (var k = gameLogTable.length - 1; k > -1; k--) { var gameLogItem = gameLogTable[k]; - if (gameLogItem.type === 'Notification') { - continue; - } if ( gameLogItem.type === 'Location' || gameLogItem.created_at < bias @@ -4108,7 +4172,7 @@ speechSynthesis.getVoices(); } if ( gameLogItem.type === 'OnPlayerJoined' && - gameLogItem.data === ctx.displayName + gameLogItem.displayName === ctx.displayName ) { joining = false; break; @@ -4134,15 +4198,7 @@ speechSynthesis.getVoices(); ) { wristFeed.unshift(onPlayerJoining); } - if ( - this.sharedFeedFilters.noty.OnPlayerJoining === - 'Friends' || - (this.sharedFeedFilters.noty.OnPlayerJoining === - 'VIP' && - isFavorite) - ) { - notyFeed.unshift(onPlayerJoining); - } + this.queueFeedNoty(onPlayerJoining); } } } @@ -4157,25 +4213,16 @@ speechSynthesis.getVoices(); return 0; }); wristFeed.splice(20); - notyFeed.sort(function (a, b) { - if (a.created_at < b.created_at) { - return 1; - } - if (a.created_at > b.created_at) { - return -1; - } - return 0; - }); - notyFeed.splice(1); - sharedRepository.setArray('wristFeed', wristFeed); - sharedRepository.setArray('notyFeed', notyFeed); + AppApi.ExecuteVrFeedFunction( + 'wristFeedUpdate', + JSON.stringify(wristFeed) + ); if (this.userDialog.visible) { this.applyUserDialogLocation(); } if (this.worldDialog.visible) { this.applyWorldDialogInstances(); } - this.playNoty(notyFeed); feeds.pendingUpdate = false; }; @@ -4197,16 +4244,10 @@ speechSynthesis.getVoices(); } var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours var wristArr = []; - var notyArr = []; var w = 0; - var n = 0; var wristFilter = this.sharedFeedFilters.wrist; - var notyFilter = this.sharedFeedFilters.noty; - var playerCountIndex = 0; - var playerList = []; - var friendList = []; - var currentUserJoinTime = ''; - var currentUserLeaveTime = ''; + var currentUserLeaveTime = 0; + var locationJoinTime = 0; for (var i = data.length - 1; i > -1; i--) { var ctx = data[i]; if (ctx.created_at < bias) { @@ -4215,73 +4256,37 @@ speechSynthesis.getVoices(); if (ctx.type === 'Notification') { continue; } - if (playerCountIndex === 0 && ctx.type === 'Location') { - playerCountIndex = i; - } // on Location change remove OnPlayerLeft - if (ctx.type === 'OnPlayerLeft') { - if (ctx.created_at.slice(0, -4) === currentUserLeaveTime) { - continue; - } - if (ctx.data === API.currentUser.displayName) { - var {created_at} = ctx; - currentUserLeaveTime = created_at.slice(0, -4); - for (var k = w - 1; k > -1; k--) { - var feedItem = wristArr[k]; - if ( - feedItem.created_at.slice(0, -4) === - currentUserLeaveTime && - feedItem.type === 'OnPlayerLeft' - ) { - wristArr.splice(k, 1); - w--; - } + if (ctx.type === 'LocationDestination') { + currentUserLeaveTime = Date.parse(ctx.created_at); + for (var k = w - 1; k > -1; k--) { + var feedItem = wristArr[k]; + if ( + feedItem.type === 'OnPlayerLeft' && + Date.parse(feedItem.created_at) >= + currentUserLeaveTime && + Date.parse(feedItem.created_at) <= + currentUserLeaveTime + 5 * 1000 + ) { + wristArr.splice(k, 1); + w--; } - for (var k = n - 1; k > -1; k--) { - var feedItem = notyArr[k]; - if ( - feedItem.created_at.slice(0, -4) === - currentUserLeaveTime && - feedItem.type === 'OnPlayerLeft' - ) { - notyArr.splice(k, 1); - n--; - } - } - continue; } } // on Location change remove OnPlayerJoined - if (ctx.type === 'OnPlayerJoined') { - if (ctx.created_at.slice(0, -4) === currentUserJoinTime) { - continue; - } - if (ctx.data === API.currentUser.displayName) { - var {created_at} = ctx; - currentUserJoinTime = created_at.slice(0, -4); - for (var k = w - 1; k > -1; k--) { - var feedItem = wristArr[k]; - if ( - feedItem.created_at.slice(0, -4) === - currentUserJoinTime && - feedItem.type === 'OnPlayerJoined' - ) { - wristArr.splice(k, 1); - w--; - } + if (ctx.type === 'Location') { + locationJoinTime = Date.parse(ctx.created_at); + for (var k = w - 1; k > -1; k--) { + var feedItem = wristArr[k]; + if ( + feedItem.type === 'OnPlayerJoined' && + Date.parse(feedItem.created_at) >= locationJoinTime && + Date.parse(feedItem.created_at) <= + locationJoinTime + 20 * 1000 + ) { + wristArr.splice(k, 1); + w--; } - for (var k = n - 1; k > -1; k--) { - var feedItem = notyArr[k]; - if ( - feedItem.created_at.slice(0, -4) === - currentUserJoinTime && - feedItem.type === 'OnPlayerJoined' - ) { - notyArr.splice(k, 1); - n--; - } - } - continue; } } // remove current user @@ -4289,19 +4294,18 @@ speechSynthesis.getVoices(); (ctx.type === 'OnPlayerJoined' || ctx.type === 'OnPlayerLeft' || ctx.type === 'PortalSpawn') && - ctx.data === API.currentUser.displayName + ctx.displayName === API.currentUser.displayName ) { continue; } var isFriend = false; var isFavorite = false; - if ( - ctx.type === 'OnPlayerJoined' || - ctx.type === 'OnPlayerLeft' || - ctx.type === 'PortalSpawn' - ) { + if (ctx.userId) { + isFriend = this.friends.has(ctx.userId); + isFavorite = API.cachedFavoritesByObjectId.has(ctx.userId); + } else if (ctx.displayName) { for (var ref of API.cachedUsers.values()) { - if (ref.displayName === ctx.data) { + if (ref.displayName === ctx.displayName) { isFriend = this.friends.has(ref.id); isFavorite = API.cachedFavoritesByObjectId.has(ref.id); break; @@ -4311,7 +4315,7 @@ speechSynthesis.getVoices(); // BlockedOnPlayerJoined, BlockedOnPlayerLeft, MutedOnPlayerJoined, MutedOnPlayerLeft if (ctx.type === 'OnPlayerJoined' || ctx.type === 'OnPlayerLeft') { for (var ref of this.playerModerationTable.data) { - if (ref.targetDisplayName === ctx.data) { + if (ref.targetDisplayName === ctx.displayName) { if (ref.type === 'block') { var type = `Blocked${ctx.type}`; } else if (ref.type === 'mute') { @@ -4319,39 +4323,23 @@ speechSynthesis.getVoices(); } else { continue; } - var displayName = ref.targetDisplayName; - var userId = ref.targetUserId; - var created_at = ctx.created_at; + var entry = { + created_at: ctx.created_at, + type, + displayName: ref.targetDisplayName, + userId: ref.targetUserId, + isFriend, + isFavorite + }; if ( wristFilter[type] && (wristFilter[type] === 'Everyone' || (wristFilter[type] === 'Friends' && isFriend) || (wristFilter[type] === 'VIP' && isFavorite)) ) { - wristArr.unshift({ - created_at, - type, - displayName, - userId, - isFriend, - isFavorite - }); - } - if ( - notyFilter[type] && - (notyFilter[type] === 'Everyone' || - (notyFilter[type] === 'Friends' && isFriend) || - (notyFilter[type] === 'VIP' && isFavorite)) - ) { - notyArr.unshift({ - created_at, - type, - displayName, - userId, - isFriend, - isFavorite - }); + wristArr.unshift(entry); } + this.queueFeedNoty(entry); } } } @@ -4370,62 +4358,76 @@ speechSynthesis.getVoices(); }); ++w; } - if ( - n < 1 && - notyFilter[ctx.type] && - (notyFilter[ctx.type] === 'On' || - notyFilter[ctx.type] === 'Everyone' || - (notyFilter[ctx.type] === 'Friends' && isFriend) || - (notyFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - notyArr.push({ - ...ctx, - isFriend, - isFavorite - }); - ++n; - } - } - // instance player list - for (var i = playerCountIndex + 1; i < data.length; i++) { - var ctx = data[i]; - if (ctx.type === 'OnPlayerJoined') { - playerList.push(ctx.data); - var isFriend = false; - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === ctx.data) { - isFriend = this.friends.has(ref.id); - break; - } - } - if (ctx.data === API.currentUser.displayName) { - isFriend = true; - } - if (isFriend) { - friendList.push(ctx.data); - } - } - if (ctx.type === 'OnPlayerLeft') { - var index = playerList.indexOf(ctx.data); - if (index > -1) { - playerList.splice(index, 1); - } - var index = friendList.indexOf(ctx.data); - if (index > -1) { - friendList.splice(index, 1); - } - } - } - if (this.isGameRunning) { - this.lastLocation.playerList = playerList; - this.lastLocation.friendList = friendList; - sharedRepository.setObject('last_location', this.lastLocation); } this.sharedFeed.gameLog.wrist = wristArr; - this.sharedFeed.gameLog.noty = notyArr; this.sharedFeed.pendingUpdate = true; }; + $app.methods.queueGameLogNoty = function (noty) { + // remove join/leave notifications when switching worlds + if (noty.type === 'OnPlayerJoined') { + var bias = this.lastLocation.date + 30 * 1000; // 30 secs + if (Date.parse(noty.created_at) <= bias) { + return; + } + } + if (noty.type === 'OnPlayerLeft') { + var bias = this.lastLocationDestinationTime + 5 * 1000; // 5 secs + if (Date.parse(noty.created_at) <= bias) { + return; + } + } + if ( + noty.type === 'Notification' || + noty.type === 'LocationDestination' + // skip unused entries + ) { + return; + } + if (noty.type === 'VideoPlay') { + if (!noty.videoName) { + // skip video without name + return; + } + noty.notyName = noty.videoName; + if (noty.displayName) { + // add requester's name to noty + noty.notyName = `${noty.videoName} (${noty.displayName})`; + } + } + if ( + noty.type !== 'VideoPlay' && + noty.displayName === API.currentUser.displayName + ) { + // remove current user + return; + } + noty.isFriend = false; + noty.isFavorite = false; + if (noty.userId) { + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = API.cachedFavoritesByObjectId.has(noty.userId); + } else if (noty.displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === noty.displayName) { + noty.isFriend = this.friends.has(ref.id); + noty.isFavorite = API.cachedFavoritesByObjectId.has(ref.id); + break; + } + } + } + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'On' || + notyFilter[noty.type] === 'Everyone' || + (notyFilter[noty.type] === 'Friends' && noty.isFriend) || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }; + $app.methods.updateSharedFeedFeedTable = function (forceUpdate) { // GPS, Online, Offline, Status, Avatar var {data} = this.feedTable; @@ -4444,11 +4446,8 @@ speechSynthesis.getVoices(); } var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours var wristArr = []; - var notyArr = []; var w = 0; - var n = 0; var wristFilter = this.sharedFeedFilters.wrist; - var notyFilter = this.sharedFeedFilters.noty; for (var i = data.length - 1; i > -1; i--) { var ctx = data[i]; if (ctx.created_at < bias) { @@ -4457,7 +4456,7 @@ speechSynthesis.getVoices(); if (ctx.type === 'Avatar') { continue; } - // hide private worlds from feeds + // hide private worlds from feed if ( this.hidePrivateFromFeed && ctx.type === 'GPS' && @@ -4480,25 +4479,35 @@ speechSynthesis.getVoices(); }); ++w; } - if ( - n < 1 && - notyFilter[ctx.type] && - (notyFilter[ctx.type] === 'Friends' || - (notyFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - notyArr.push({ - ...ctx, - isFriend, - isFavorite - }); - ++n; - } } this.sharedFeed.feedTable.wrist = wristArr; - this.sharedFeed.feedTable.noty = notyArr; this.sharedFeed.pendingUpdate = true; }; + $app.methods.queueFeedNoty = function (noty) { + if (noty.type === 'Avatar') { + return; + } + // hide private worlds from feed + if ( + this.hidePrivateFromFeed && + noty.type === 'GPS' && + noty.location === 'private' + ) { + return; + } + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = API.cachedFavoritesByObjectId.has(noty.userId); + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'Friends' || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }; + $app.methods.updateSharedFeedNotificationTable = function (forceUpdate) { // invite, requestInvite, requestInviteResponse, inviteResponse, friendRequest var {data} = this.notificationTable; @@ -4518,11 +4527,8 @@ speechSynthesis.getVoices(); } var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours var wristArr = []; - var notyArr = []; var w = 0; - var n = 0; var wristFilter = this.sharedFeedFilters.wrist; - var notyFilter = this.sharedFeedFilters.noty; for (var i = data.length - 1; i > -1; i--) { var ctx = data[i]; if (ctx.created_at < bias) { @@ -4549,26 +4555,25 @@ speechSynthesis.getVoices(); }); ++w; } - if ( - n < 1 && - notyFilter[ctx.type] && - (notyFilter[ctx.type] === 'On' || - notyFilter[ctx.type] === 'Friends' || - (notyFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - notyArr.push({ - ...ctx, - isFriend, - isFavorite - }); - ++n; - } } this.sharedFeed.notificationTable.wrist = wristArr; - this.sharedFeed.notificationTable.noty = notyArr; this.sharedFeed.pendingUpdate = true; }; + $app.methods.queueNotificationNoty = function (noty) { + noty.isFriend = this.friends.has(noty.senderUserId); + noty.isFavorite = API.cachedFavoritesByObjectId.has(noty.senderUserId); + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'On' || + notyFilter[noty.type] === 'Friends' || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }; + $app.methods.updateSharedFeedFriendLogTable = function (forceUpdate) { // TrustLevel, Friend, FriendRequest, Unfriend, DisplayName var {data} = this.friendLogTable; @@ -4588,11 +4593,8 @@ speechSynthesis.getVoices(); } var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours var wristArr = []; - var notyArr = []; var w = 0; - var n = 0; var wristFilter = this.sharedFeedFilters.wrist; - var notyFilter = this.sharedFeedFilters.noty; for (var i = data.length - 1; i > -1; i--) { var ctx = data[i]; if (ctx.created_at < bias) { @@ -4617,29 +4619,58 @@ speechSynthesis.getVoices(); }); ++w; } - if ( - n < 1 && - notyFilter[ctx.type] && - (notyFilter[ctx.type] === 'On' || - notyFilter[ctx.type] === 'Friends' || - (notyFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - notyArr.push({ - ...ctx, - isFriend, - isFavorite - }); - ++n; - } } this.sharedFeed.friendLogTable.wrist = wristArr; - this.sharedFeed.friendLogTable.noty = notyArr; this.sharedFeed.pendingUpdate = true; }; + $app.methods.queueFriendLogNoty = function (noty) { + if (noty.type === 'FriendRequest') { + return; + } + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = API.cachedFavoritesByObjectId.has(noty.userId); + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'On' || + notyFilter[noty.type] === 'Friends' || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }; + $app.data.notyMap = []; - $app.methods.playNoty = function (notyFeed) { + $app.methods.playNoty = function (noty) { + if (API.currentUser.status === 'busy' || !this.friendLogInitStatus) { + return; + } + var displayName = ''; + if (noty.displayName) { + displayName = noty.displayName; + } else if (noty.senderUsername) { + displayName = noty.senderUsername; + } else if (noty.sourceDisplayName) { + displayName = noty.sourceDisplayName; + } + if (displayName) { + // don't play noty twice + if ( + this.notyMap[displayName] && + this.notyMap[displayName] >= noty.created_at + ) { + return; + } + this.notyMap[displayName] = noty.created_at; + } + var bias = new Date(Date.now() - 60000).toJSON(); + if (noty.created_at < bias) { + // don't play noty if it's over 1min old + return; + } + var playNotificationTTS = false; if ( this.notificationTTS === 'Always' || @@ -4666,72 +4697,60 @@ speechSynthesis.getVoices(); if (this.xsNotifications && this.isGameRunning && !this.isGameNoVR) { playXSNotification = true; } - if (this.currentUserStatus === 'busy' || !this.notyInit) { - return; + var playOverlayNotification = false; + if ( + this.overlayNotifications && + !this.isGameNoVR && + this.isGameRunning + ) { + playOverlayNotification = true; } - var notyToPlay = []; - notyFeed.forEach((feed) => { - var displayName = ''; - if (feed.displayName) { - displayName = feed.displayName; - } else if (feed.senderUsername) { - displayName = feed.senderUsername; - } else if (feed.sourceDisplayName) { - displayName = feed.sourceDisplayName; - } else if (feed.data) { - displayName = feed.data; - } else { - console.error('missing displayName'); - } - if ( - (displayName && !this.notyMap[displayName]) || - this.notyMap[displayName] < feed.created_at - ) { - this.notyMap[displayName] = feed.created_at; - notyToPlay.push(feed); - } - }); - var bias = new Date(Date.now() - 60000).toJSON(); var messageList = [ 'inviteMessage', 'requestMessage', 'responseMessage' ]; - for (var i = 0; i < notyToPlay.length; i++) { - let noty = notyToPlay[i]; - if (noty.created_at < bias) { - continue; + let message = ''; + for (var k = 0; k < messageList.length; k++) { + if ( + typeof noty.details !== 'undefined' && + typeof noty.details[messageList[k]] !== 'undefined' + ) { + message = `, ${noty.details[messageList[k]]}`; } - let message = ''; - for (var k = 0; k < messageList.length; k++) { - if ( - typeof noty.details !== 'undefined' && - typeof noty.details[messageList[k]] !== 'undefined' - ) { - message = noty.details[messageList[k]]; - } - } - if (message) { - message = `, ${message}`; - } - if (playNotificationTTS) { - this.playNotyTTS(noty, message); - } - if (playDesktopToast || playXSNotification) { - this.notyGetImage(noty).then((image) => { + } + if (playNotificationTTS) { + this.playNotyTTS(noty, message); + } + if (playDesktopToast || playXSNotification || playOverlayNotification) { + if (this.imageNotifications) { + this.notySaveImage(noty).then((image) => { if (playXSNotification) { this.displayXSNotification(noty, message, image); } if (playDesktopToast) { this.displayDesktopToast(noty, message, image); } + if (playOverlayNotification) { + this.displayOverlayNotification(noty, message, image); + } }); + } else { + if (playXSNotification) { + this.displayXSNotification(noty, message, ''); + } + if (playDesktopToast) { + this.displayDesktopToast(noty, message, ''); + } + if (playOverlayNotification) { + this.displayOverlayNotification(noty, message, ''); + } } } }; $app.methods.notyGetImage = async function (noty) { - var imageURL = ''; + var imageUrl = ''; var userId = ''; if (noty.userId) { userId = noty.userId; @@ -4739,23 +4758,25 @@ speechSynthesis.getVoices(); userId = noty.senderUserId; } else if (noty.sourceUserId) { userId = noty.sourceUserId; - } else if (noty.data) { + } else if (noty.displayName) { for (var ref of API.cachedUsers.values()) { - if (ref.displayName === noty.data) { + if (ref.displayName === noty.displayName) { userId = ref.id; break; } } } - if (noty.details && noty.details.imageUrl) { - imageURL = noty.details.imageUrl; + if (noty.thumbnailImageUrl) { + imageUrl = noty.thumbnailImageUrl; + } else if (noty.details && noty.details.imageUrl) { + imageUrl = noty.details.imageURL; } else if (userId) { - imageURL = await API.getCachedUser({ + imageUrl = await API.getCachedUser({ userId }) .catch((err) => { console.error(err); - return false; + return ''; }) .then((args) => { if ( @@ -4764,45 +4785,55 @@ speechSynthesis.getVoices(); ) { return args.json.userIcon; } + if (args.json.profilePicOverride) { + return args.json.profilePicOverride; + } return args.json.currentAvatarThumbnailImageUrl; }); } - if (!imageURL) { - return false; - } - try { - await fetch(imageURL, { - method: 'GET', - redirect: 'follow', - headers: { - 'User-Agent': appVersion - } - }) - .then((response) => response.arrayBuffer()) - .then((buffer) => { - var binary = ''; - var bytes = new Uint8Array(buffer); - var length = bytes.byteLength; - for (var i = 0; i < length; i++) { - binary += String.fromCharCode(bytes[i]); - } - var imageData = btoa(binary); - AppApi.CacheImage(imageData); - }); - return true; - } catch (err) { - console.error(err); - return false; + return imageUrl; + }; + + $app.methods.notySaveImage = async function (noty) { + var imageUrl = await this.notyGetImage(noty); + var base64Image = ''; + if (imageUrl) { + try { + base64Image = await fetch(imageUrl, { + method: 'GET', + redirect: 'follow' + }) + .then((response) => response.arrayBuffer()) + .then((buffer) => { + var binary = ''; + var bytes = new Uint8Array(buffer); + var length = bytes.byteLength; + for (var i = 0; i < length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + }); + } catch (err) { + console.error(err); + } } + return base64Image; + }; + + $app.methods.displayOverlayNotification = function (noty, message, image) { + AppApi.ExecuteVrOverlayFunction( + 'playNoty', + JSON.stringify({noty, message, image}) + ); }; $app.methods.playNotyTTS = function (noty, message) { switch (noty.type) { case 'OnPlayerJoined': - this.speak(`${noty.data} has joined`); + this.speak(`${noty.displayName} has joined`); break; case 'OnPlayerLeft': - this.speak(`${noty.data} has left`); + this.speak(`${noty.displayName} has left`); break; case 'OnPlayerJoining': this.speak(`${noty.displayName} is joining`); @@ -4873,13 +4904,27 @@ speechSynthesis.getVoices(); ); break; case 'PortalSpawn': - this.speak(`${noty.data} has spawned a portal`); + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; + } + this.speak( + `${noty.displayName} has spawned a portal${locationName}` + ); + break; + case 'AvatarChange': + this.speak( + `${noty.displayName} changed into avatar ${noty.name}` + ); break; case 'Event': this.speak(noty.data); break; case 'VideoPlay': - this.speak(`Now playing: ${noty.data}`); + this.speak(`Now playing: ${noty.notyName}`); break; case 'BlockedOnPlayerJoined': this.speak(`Blocked user ${noty.displayName} has joined`); @@ -4902,7 +4947,7 @@ speechSynthesis.getVoices(); case 'OnPlayerJoined': AppApi.XSNotification( 'VRCX', - `${noty.data} has joined`, + `${noty.displayName} has joined`, timeout, image ); @@ -4910,7 +4955,7 @@ speechSynthesis.getVoices(); case 'OnPlayerLeft': AppApi.XSNotification( 'VRCX', - `${noty.data} has left`, + `${noty.displayName} has left`, timeout, image ); @@ -5036,9 +5081,24 @@ speechSynthesis.getVoices(); ); break; case 'PortalSpawn': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; + } AppApi.XSNotification( 'VRCX', - `${noty.data} has spawned a portal`, + `${noty.displayName} has spawned a portal${locationName}`, + timeout, + image + ); + break; + case 'AvatarChange': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} changed into avatar ${noty.name}`, timeout, image ); @@ -5049,7 +5109,7 @@ speechSynthesis.getVoices(); case 'VideoPlay': AppApi.XSNotification( 'VRCX', - `Now playing: ${noty.data}`, + `Now playing: ${noty.notyName}`, timeout, image ); @@ -5092,10 +5152,14 @@ speechSynthesis.getVoices(); $app.methods.displayDesktopToast = function (noty, message, image) { switch (noty.type) { case 'OnPlayerJoined': - AppApi.DesktopNotification(noty.data, 'has joined', image); + AppApi.DesktopNotification( + noty.displayName, + 'has joined', + image + ); break; case 'OnPlayerLeft': - AppApi.DesktopNotification(noty.data, 'has left', image); + AppApi.DesktopNotification(noty.displayName, 'has left', image); break; case 'OnPlayerJoining': AppApi.DesktopNotification( @@ -5202,9 +5266,23 @@ speechSynthesis.getVoices(); ); break; case 'PortalSpawn': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; + } AppApi.DesktopNotification( - noty.data, - `has spawned a portal`, + noty.displayName, + `has spawned a portal${locationName}`, + image + ); + break; + case 'AvatarChange': + AppApi.DesktopNotification( + noty.displayName, + `changed into avatar ${noty.name}`, image ); break; @@ -5212,7 +5290,7 @@ speechSynthesis.getVoices(); AppApi.DesktopNotification('Event', noty.data, image); break; case 'VideoPlay': - AppApi.DesktopNotification('Now playing', noty.data, image); + AppApi.DesktopNotification('Now playing', noty.notyName, image); break; case 'BlockedOnPlayerJoined': AppApi.DesktopNotification( @@ -5446,7 +5524,6 @@ speechSynthesis.getVoices(); )}!` }).show(); $app.$refs.menu.activeIndex = 'feed'; - $app.resetGameLog(); }); API.$on('LOGIN', function (args) { @@ -5684,10 +5761,7 @@ speechSynthesis.getVoices(); var user = $app.loginForm.savedCredentials[$app.loginForm.lastUserLoggedIn]; if (typeof user !== 'undefined') { - $app.relogin({ - username: user.loginParmas.username, - password: user.loginParmas.password - }).then(() => { + $app.relogin(user).then(() => { new Noty({ type: 'success', text: 'Automatically logged in.' @@ -5960,7 +6034,10 @@ speechSynthesis.getVoices(); }); $app.methods.checkActiveFriends = function (ref) { - if (Array.isArray(ref.activeFriends) === false || !this.appInit) { + if ( + Array.isArray(ref.activeFriends) === false || + !this.friendLogInitStatus + ) { return; } for (var userId of ref.activeFriends) { @@ -6018,6 +6095,9 @@ speechSynthesis.getVoices(); }); API.$on('FRIEND:STATE', function (args) { + if (args.json.state === 'online') { + $app.APILastOnline.set(args.params.userId, Date.now()); + } $app.updateFriend(args.params.userId, args.json.state); }); @@ -6154,7 +6234,7 @@ speechSynthesis.getVoices(); // AddFriend (CurrentUser) 이후, // 서버에서 오는 순서라고 보면 될 듯. if (ctx.state === 'online') { - if (this.appInit) { + if (this.friendLogInitStatus) { API.getUser({ userId: id }); @@ -6217,6 +6297,8 @@ speechSynthesis.getVoices(); ) { API.getUser({ userId: id + }).catch(() => { + this.updateFriendInProgress.delete(id); }); } } else { @@ -6229,6 +6311,22 @@ speechSynthesis.getVoices(); ) { var {location, $location_at} = ref; } + // prevent status flapping + if ( + ctx.state === 'online' && + (stateInput === 'active' || stateInput === 'offline') + ) { + await new Promise((resolve) => { + setTimeout(resolve, 50000); + }); + if (this.APILastOnline.has(id)) { + var date = this.APILastOnline.get(id); + if (date > Date.now() - 60000) { + this.updateFriendInProgress.delete(id); + return; + } + } + } try { var args = await API.getUser({ userId: id @@ -6426,6 +6524,10 @@ speechSynthesis.getVoices(); return $app.sortStatus(a.ref.status, b.ref.status); }; + $app.methods.sortByStatus = function (a, b, field) { + return this.sortStatus(a[field], b[field]); + }; + $app.methods.sortStatus = function (a, b) { switch (b) { case 'join me': @@ -6726,6 +6828,59 @@ speechSynthesis.getVoices(); // App: Feed + $app.methods.feedSearch = function (row, filter) { + var {value} = filter; + if (!value) { + return true; + } + value = value.toUpperCase(); + switch (row.type) { + case 'GPS': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Online': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Offline': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Status': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if ( + String(row.statusDescription).toUpperCase().includes(value) + ) { + return true; + } + return false; + case 'Avatar': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.avatarName).toUpperCase().includes(value)) { + return true; + } + return false; + } + return true; + }; + $app.data.feedTable = { data: [], filters: [ @@ -6737,7 +6892,8 @@ speechSynthesis.getVoices(); }, { prop: 'displayName', - value: '' + value: '', + filterFn: (row, filter) => $app.feedSearch(row, filter) }, { prop: 'userId', @@ -6764,14 +6920,24 @@ speechSynthesis.getVoices(); }; API.$on('LOGIN', async function (args) { + $app.feedTable.data = []; $app.friendLogInitStatus = false; - await database.init(args.json.id); + await database.initUserTables(args.json.id); $app.feedTable.data = await database.getFeedDatabase(); $app.sweepFeed(); + // eslint-disable-next-line require-atomic-updates + $app.notificationTable.data = await database.getNotifications(); if (configRepository.getBool(`friendLogInit_${args.json.id}`)) { - $app.getFriendLog(); + await $app.getFriendLog(); } else { - $app.initFriendLog(args.json.id); + await $app.initFriendLog(args.json.id); + } + this.getAuth(); + + $app.updateSharedFeed(true); + + if ($app.isGameRunning) { + $app.loadPlayerList(); } // remove old data from json file and migrate to SQLite if (VRCXStorage.Get(`${args.json.id}_friendLogUpdatedAt`)) { @@ -6781,6 +6947,63 @@ speechSynthesis.getVoices(); } }); + $app.methods.loadPlayerList = function () { + var {data} = this.gameLogTable; + if (data.length === 0) { + return; + } + var length = 0; + for (var i = data.length - 1; i > -1; i--) { + var ctx = data[i]; + if (ctx.type === 'Location') { + this.lastLocation = { + date: Date.parse(ctx.created_at), + location: ctx.location, + name: ctx.worldName, + playerList: new Map(), + friendList: new Map() + }; + length = i; + break; + } + } + if (length > 0) { + for (var i = length + 1; i < data.length; i++) { + var ctx = data[i]; + if (ctx.type === 'OnPlayerJoined') { + if (!ctx.userId) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === ctx.displayName) { + ctx.userId = ref.id; + break; + } + } + } + var userMap = { + displayName: ctx.displayName, + userId: ctx.userId, + joinTime: Date.parse(ctx.created_at) + }; + this.lastLocation.playerList.set(ctx.displayName, userMap); + if ( + this.friends.has(ctx.userId) || + API.currentUser.displayName === ctx.displayName + ) { + this.lastLocation.friendList.set( + ctx.displayName, + userMap + ); + } + } + if (ctx.type === 'OnPlayerLeft') { + this.lastLocation.playerList.delete(ctx.displayName); + this.lastLocation.friendList.delete(ctx.displayName); + } + } + this.updateVRLastLocation(); + } + }; + API.$on('USER:UPDATE', async function (args) { var {ref, props} = args; if ($app.friends.has(ref.id) === false) { @@ -6905,6 +7128,7 @@ speechSynthesis.getVoices(); this.feedTable.data.push(feed); this.sweepFeed(); this.updateSharedFeed(false); + this.queueFeedNoty(feed); this.notifyMenu('feed'); }; @@ -6978,18 +7202,129 @@ speechSynthesis.getVoices(); date: 0, location: '', name: '', - playerList: [], - friendList: [] + playerList: new Map(), + friendList: new Map() + }; + + $app.methods.lastLocationReset = function () { + var playerList = Array.from(this.lastLocation.playerList.values()); + for (var ref of playerList) { + var time = new Date().getTime() - ref.joinTime; + var entry = { + created_at: new Date().toJSON(), + type: 'OnPlayerLeft', + displayName: ref.displayName, + location: this.lastLocation.location, + userId: ref.userId, + time + }; + database.addGamelogJoinLeaveToDatabase(entry); + this.gameLogTable.data.push(entry); + } + if (playerList.length > 0) { + this.updateSharedFeed(false); + this.notifyMenu('gameLog'); + this.sweepGameLog(); + } + if (this.lastLocation.date !== 0) { + var timeLocation = new Date().getTime() - this.lastLocation.date; + var update = { + time: timeLocation, + created_at: new Date(this.lastLocation.date).toJSON() + }; + database.updateGamelogLocationTimeToDatabase(update); + } + this.lastLocation = { + date: 0, + location: '', + name: '', + playerList: new Map(), + friendList: new Map() + }; + this.updateVRLastLocation(); + }; + + $app.data.lastLocation$ = { + tag: '', + instanceId: '', + accessType: '', + worldName: '', + worldCapacity: 0, + joinUrl: '', + statusName: '', + statusImage: '' }; - $app.data.lastLocation$ = {}; $app.data.discordActive = configRepository.getBool('discordActive'); $app.data.discordInstance = configRepository.getBool('discordInstance'); - var saveDiscordOption = function () { + $app.data.discordJoinButton = configRepository.getBool('discordJoinButton'); + $app.data.discordHideInvite = configRepository.getBool('discordHideInvite'); + $app.methods.saveDiscordOption = function () { configRepository.setBool('discordActive', this.discordActive); configRepository.setBool('discordInstance', this.discordInstance); + configRepository.setBool('discordJoinButton', this.discordJoinButton); + configRepository.setBool('discordHideInvite', this.discordHideInvite); + if (!this.discordActive) { + Discord.SetText('', ''); + Discord.SetActive(false); + } + this.lastLocation$.tag = ''; + this.updateDiscord(); + }; + + $app.methods.gameLogSearch = function (row, filter) { + var {value} = filter; + if (!value) { + return true; + } + value = value.toUpperCase(); + switch (row.type) { + case 'Location': + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'OnPlayerJoined': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'OnPlayerLeft': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'PortalSpawn': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'AvatarChange': + if (String(row.name).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Event': + if (String(row.data).toUpperCase().includes(value)) { + return true; + } + return false; + case 'VideoPlay': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.videoName).toUpperCase().includes(value)) { + return true; + } + if (String(row.videoUrl).toUpperCase().includes(value)) { + return true; + } + return false; + } + return true; }; - $app.watch.discordActive = saveDiscordOption; - $app.watch.discordInstance = saveDiscordOption; $app.data.gameLogTable = { data: [], @@ -7002,18 +7337,22 @@ speechSynthesis.getVoices(); filter.value.some((v) => v === row.type) }, { - prop: 'data', - value: '' + prop: 'displayName', + value: '', + filterFn: (row, filter) => $app.gameLogSearch(row, filter) }, { - prop: 'data', + prop: 'displayName', value: true, - filterFn: (row) => row.data !== API.currentUser.displayName + filterFn: (row) => + row.displayName !== API.currentUser.displayName }, { prop: 'type', value: true, - filterFn: (row) => row.type !== 'Notification' + filterFn: (row) => + row.type !== 'Notification' && + row.type !== 'LocationDestination' } ], tableProps: { @@ -7034,117 +7373,8 @@ speechSynthesis.getVoices(); $app.methods.resetGameLog = async function () { await gameLogService.reset(); - await gameLogService.poll(); this.gameLogTable.data = []; - this.lastLocation = { - date: 0, - location: '', - name: '', - playerList: [], - friendList: [] - }; - }; - - $app.methods.updateGameLogLoop = async function () { - try { - if (API.isLoggedIn === true) { - await this.updateGameLog(); - this.sweepGameLog(); - var length = this.gameLogTable.data.length; - if (length > 0) { - if ( - this.gameLogTable.data[length - 1].created_at !== - this.gameLogTable.lastEntryDate - ) { - this.notifyMenu('gameLog'); - } - this.gameLogTable.lastEntryDate = - this.gameLogTable.data[length - 1].created_at; - } - this.updateSharedFeed(false); - } - } catch (err) { - console.error(err); - } - setTimeout(() => this.updateGameLogLoop(), 500); - }; - - $app.methods.updateGameLog = async function () { - for (var gameLog of await gameLogService.poll()) { - var tableData = null; - - switch (gameLog.type) { - case 'location': - if (this.isGameRunning) { - this.lastLocation = { - date: Date.parse(gameLog.dt), - location: gameLog.location, - name: gameLog.worldName, - playerList: [], - friendList: [] - }; - } - tableData = { - created_at: gameLog.dt, - type: 'Location', - data: [gameLog.location, gameLog.worldName] - }; - break; - - case 'player-joined': - tableData = { - created_at: gameLog.dt, - type: 'OnPlayerJoined', - data: gameLog.userDisplayName - }; - break; - - case 'player-left': - tableData = { - created_at: gameLog.dt, - type: 'OnPlayerLeft', - data: gameLog.userDisplayName - }; - break; - - case 'notification': - tableData = { - created_at: gameLog.dt, - type: 'Notification', - data: gameLog.json - }; - break; - - case 'portal-spawn': - tableData = { - created_at: gameLog.dt, - type: 'PortalSpawn', - data: gameLog.userDisplayName - }; - break; - - case 'event': - tableData = { - created_at: gameLog.dt, - type: 'Event', - data: gameLog.event - }; - break; - - case 'video-play': - tableData = { - created_at: gameLog.dt, - type: 'VideoPlay', - data: gameLog.videoURL, - displayName: gameLog.displayName - }; - break; - } - - if (tableData !== null) { - this.gameLogTable.data.push(tableData); - } - } + this.lastLocationReset(); }; $app.methods.sweepGameLog = function () { @@ -7163,70 +7393,673 @@ speechSynthesis.getVoices(); } }; - $app.methods.updateDiscord = function () { - var ref = API.cachedUsers.get(API.currentUser.id); - if (typeof ref !== 'undefined') { - var myLocation = this.lastLocation.location; - if (ref.location !== myLocation) { - API.applyUser({ - id: ref.id, - location: myLocation - }); + $app.methods.refreshEntireGameLog = async function () { + await gameLogService.setDateTill('1970-01-01'); + await database.initTables(); + await this.resetGameLog(); + var location = ''; + var pushToTable = false; + for (var gameLog of await gameLogService.getAll()) { + if (gameLog.type === 'location') { + location = gameLog.location; + } + this.addGameLogEntry(gameLog, location, pushToTable); + } + this.getGameLogTable(); + }; + + $app.methods.getGameLogTable = async function () { + await database.initTables(); + this.gameLogTable.data = await database.getGamelogDatabase(); + this.sweepGameLog(); + var length = this.gameLogTable.data.length; + if (length > 1) { + this.updateGameLog(this.gameLogTable.data[length - 1].created_at); + } else { + this.refreshEntireGameLog(); + } + }; + + $app.methods.updateGameLog = async function (dateTill) { + await gameLogService.setDateTill(dateTill); + await gameLogService.reset(); + await new Promise((resolve) => { + setTimeout(resolve, 10000); + }); + var location = ''; + var pushToTable = true; + for (var gameLog of await gameLogService.getAll()) { + if (gameLog.type === 'location') { + location = gameLog.location; + } + this.addGameLogEntry(gameLog, location, pushToTable); + } + }; + + $app.methods.addGameLogEvent = function (json) { + var rawLogs = JSON.parse(json); + var gameLog = gameLogService.parseRawGameLog( + rawLogs[1], + rawLogs[2], + rawLogs.slice(3) + ); + var pushToTable = true; + this.addGameLogEntry(gameLog, this.lastLocation.location, pushToTable); + }; + + $app.data.lastLocationDestination = ''; + $app.data.lastLocationDestinationTime = 0; + + $app.methods.addGameLogEntry = function (gameLog, location, pushToTable) { + var userId = ''; + if (gameLog.userDisplayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === gameLog.userDisplayName) { + userId = ref.id; + break; + } } } - if (this.isGameRunning === false || this.lastLocation.location === '') { - Discord.SetActive(false); + switch (gameLog.type) { + case 'location-destination': + if (this.isGameRunning) { + this.cancelVRChatCacheDownload(gameLog.location); + this.clearNowPlaying(); + } + this.lastLocationDestination = gameLog.location; + this.lastLocationDestinationTime = Date.parse(gameLog.dt); + var entry = { + created_at: gameLog.dt, + type: 'LocationDestination', + location: gameLog.location + }; + break; + case 'location': + if (this.isGameRunning) { + this.lastLocationReset(); + this.clearNowPlaying(); + this.lastLocation = { + date: Date.parse(gameLog.dt), + location: gameLog.location, + name: gameLog.worldName, + playerList: new Map(), + friendList: new Map() + }; + this.updateVRLastLocation(); + this.cancelVRChatCacheDownload(gameLog.location); + } + var L = API.parseLocation(gameLog.location); + var entry = { + created_at: gameLog.dt, + type: 'Location', + location: gameLog.location, + worldId: L.worldId, + worldName: gameLog.worldName, + time: 0 + }; + database.addGamelogLocationToDatabase(entry); + break; + case 'player-joined': + var userMap = { + displayName: gameLog.userDisplayName, + userId, + joinTime: Date.parse(gameLog.dt) + }; + this.lastLocation.playerList.set( + gameLog.userDisplayName, + userMap + ); + if ( + this.friends.has(userId) || + API.currentUser.displayName === gameLog.userDisplayName + ) { + this.lastLocation.friendList.set( + gameLog.userDisplayName, + userMap + ); + } + this.updateVRLastLocation(); + var entry = { + created_at: gameLog.dt, + type: 'OnPlayerJoined', + displayName: gameLog.userDisplayName, + location, + userId, + time: 0 + }; + database.addGamelogJoinLeaveToDatabase(entry); + break; + case 'player-left': + var time = 0; + var ref = this.lastLocation.playerList.get( + gameLog.userDisplayName + ); + if (typeof ref !== 'undefined') { + time = new Date().getTime() - ref.joinTime; + this.lastLocation.playerList.delete( + gameLog.userDisplayName + ); + this.lastLocation.friendList.delete( + gameLog.userDisplayName + ); + } + this.updateVRLastLocation(); + var entry = { + created_at: gameLog.dt, + type: 'OnPlayerLeft', + displayName: gameLog.userDisplayName, + location, + userId, + time + }; + database.addGamelogJoinLeaveToDatabase(entry); + break; + case 'portal-spawn': + var entry = { + created_at: gameLog.dt, + type: 'PortalSpawn', + displayName: gameLog.userDisplayName, + location, + userId, + instanceId: '', + worldName: '' + }; + database.addGamelogPortalSpawnToDatabase(entry); + break; + case 'video-play': + this.addGameLogVideo(gameLog, location, userId, pushToTable); + return; + case 'vrcx': + // VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/jr1NX4Jo8GE.mp4",0.1001,239.606,"0905 : [J-POP] 【まなこ】金曜日のおはよう 踊ってみた (vernities)" + var type = gameLog.data.substr(0, gameLog.data.indexOf(' ')); + if (type === 'VideoPlay(PyPyDance)') { + this.addGameLogPyPyDance(gameLog, location, pushToTable); + } + return; + case 'notification': + var entry = { + created_at: gameLog.dt, + type: 'Notification', + data: gameLog.json + }; + break; + case 'event': + var entry = { + created_at: gameLog.dt, + type: 'Event', + data: gameLog.event + }; + database.addGamelogEventToDatabase(entry); + break; + } + if (pushToTable && entry) { + this.queueGameLogNoty(entry); + this.gameLogTable.data.push(entry); + this.updateSharedFeed(false); + this.notifyMenu('gameLog'); + this.sweepGameLog(); + } + }; + + $app.methods.addGameLogVideo = async function ( + gameLog, + location, + userId, + pushToTable + ) { + var videoUrl = gameLog.videoUrl; + var youtubeVideoId = ''; + var videoId = ''; + var videoName = ''; + var videoLength = ''; + var displayName = ''; + var videoPos = 10; // video loading delay + if (typeof gameLog.displayName !== 'undefined') { + displayName = gameLog.displayName; + } + if (typeof gameLog.videoPos !== 'undefined') { + videoPos = gameLog.videoPos; + } + var L = API.parseLocation(location); + if ( + L.worldId !== 'wrld_f20326da-f1ac-45fc-a062-609723b097b1' || + gameLog.videoId === 'YouTube' + ) { + // skip PyPyDance videos + try { + var url = new URL(videoUrl); + var id1 = url.pathname; + var id2 = url.searchParams.get('v'); + if (id1 && id1.length === 12) { + youtubeVideoId = id1.substring(1, 12); + } + if (id2 && id2.length === 11) { + youtubeVideoId = id2; + } + if (this.youTubeApi && youtubeVideoId) { + var data = await this.lookupYouTubeVideo(youtubeVideoId); + if (data || data.pageInfo.totalResults !== 0) { + videoId = 'YouTube'; + videoName = data.items[0].snippet.title; + videoLength = this.convertYoutubeTime( + data.items[0].contentDetails.duration + ); + } + } + } catch { + console.error(`Invalid URL: ${url}`); + } + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + if (pushToTable) { + this.setNowPlaying(entry); + this.queueGameLogNoty(entry); + this.gameLogTable.data.push(entry); + this.updateSharedFeed(false); + this.notifyMenu('gameLog'); + this.sweepGameLog(); + } + database.addGamelogVideoPlayToDatabase(entry); + } + }; + + $app.methods.addGameLogPyPyDance = function ( + gameLog, + location, + pushToTable + ) { + var data = + /VideoPlay\(PyPyDance\) "(.+?)",([\d.]+),([\d.]+),"(.+?)\s*(?:)?"/g.exec( + gameLog.data + ); + var videoUrl = data[1]; + var videoPos = Number(data[2]); + var videoLength = Number(data[3]); + var title = data[4]; + var bracketArray = title.split('('); + var text1 = bracketArray.pop(); + var displayName = text1.slice(0, -1); + var text2 = bracketArray.join('('); + if (text2 === 'URL ') { + var videoId = 'YouTube'; + } else { + var videoId = text2.substr(0, text2.indexOf(':') - 1); + text2 = text2.substr(text2.indexOf(':') + 2); + } + var videoName = text2.slice(0, -1); + var userId = ''; + if (displayName && displayName !== 'Random') { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === displayName) { + userId = ref.id; + break; + } + } + } + if (videoId === 'YouTube') { + var entry = { + dt: gameLog.dt, + videoUrl, + displayName, + videoPos, + videoId + }; + this.addGameLogVideo(entry, location, userId, pushToTable); + } else { + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + if (pushToTable) { + this.setNowPlaying(entry); + this.queueGameLogNoty(entry); + this.gameLogTable.data.push(entry); + this.updateSharedFeed(false); + this.notifyMenu('gameLog'); + this.sweepGameLog(); + } + database.addGamelogVideoPlayToDatabase(entry); + } + }; + + $app.methods.lookupYouTubeVideo = async function (videoId) { + var data = null; + var apiKey = 'AIzaSyA-iUQCpWf5afEL3NanEOSxbzziPMU3bxY'; + if (this.youTubeApiKey) { + apiKey = this.youTubeApiKey; + } + try { + var response = await webApiService.execute({ + url: `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&part=snippet,contentDetails&key=${apiKey}`, + method: 'GET', + headers: { + 'User-Agent': appVersion, + Referer: 'https://vrcx.pypy.moe' + } + }); + var json = JSON.parse(response.data); + if (response.status === 200) { + data = json; + } else { + throw new Error(`Error: ${response.data}`); + } + } catch { + console.error(`YouTube video lookup failed for ${videoId}`); + } + return data; + }; + + $app.data.nowPlaying = { + url: '', + name: '', + length: 0, + startTime: 0, + elapsed: 0, + percentage: 0, + remainingText: '', + playing: false + }; + + $app.methods.clearNowPlaying = function () { + this.nowPlaying = { + url: '', + name: '', + length: 0, + startTime: 0, + elapsed: 0, + percentage: 0, + remainingText: '', + playing: false + }; + this.updateVrNowPlaying(); + }; + + $app.methods.setNowPlaying = function (ctx) { + var displayName = ''; + if (ctx.displayName) { + displayName = ` (${ctx.displayName})`; + } + var name = `${ctx.videoName}${displayName}`; + this.nowPlaying = { + url: ctx.videoUrl, + name, + length: ctx.videoLength, + startTime: Date.parse(ctx.created_at) / 1000 - ctx.videoPos, + elapsed: 0, + percentage: 0, + remainingText: '' + }; + if (!this.nowPlaying.playing) { + this.nowPlaying.playing = true; + this.updateNowPlaying(); + } + }; + + $app.methods.updateNowPlaying = function () { + var np = this.nowPlaying; + if (!np.url) { + this.nowPlaying.playing = false; return; } + var now = Date.now() / 1000; + np.elapsed = Math.round((now - np.startTime) * 10) / 10; + if (np.elapsed >= np.length) { + this.clearNowPlaying(); + return; + } + np.remainingText = this.formatSeconds(np.length - np.elapsed); + np.percentage = Math.round(((np.elapsed * 100) / np.length) * 10) / 10; + this.updateVrNowPlaying(); + setTimeout(() => this.updateNowPlaying(), 1000); + }; + + $app.methods.updateVrNowPlaying = function () { + var json = JSON.stringify(this.nowPlaying); + AppApi.ExecuteVrFeedFunction('nowPlayingUpdate', json); + }; + + $app.methods.formatSeconds = function (duration) { + var pad = function (num, size) { + return `000${num}`.slice(size * -1); + }, + time = parseFloat(duration).toFixed(3), + hours = Math.floor(time / 60 / 60), + minutes = Math.floor(time / 60) % 60, + seconds = Math.floor(time - minutes * 60); + var hoursOut = ''; + if (hours > '0') { + hoursOut = `${pad(hours, 2)}:`; + } + return `${hoursOut + pad(minutes, 2)}:${pad(seconds, 2)}`; + }; + + $app.methods.convertYoutubeTime = function (duration) { + var a = duration.match(/\d+/g); + if ( + duration.indexOf('M') >= 0 && + duration.indexOf('H') === -1 && + duration.indexOf('S') === -1 + ) { + a = [0, a[0], 0]; + } + if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1) { + a = [a[0], 0, a[1]]; + } + if ( + duration.indexOf('H') >= 0 && + duration.indexOf('M') === -1 && + duration.indexOf('S') === -1 + ) { + a = [a[0], 0, 0]; + } + var length = 0; + if (a.length === 3) { + length += parseInt(a[0], 10) * 3600; + length += parseInt(a[1], 10) * 60; + length += parseInt(a[2], 10); + } + if (a.length === 2) { + length += parseInt(a[0], 10) * 60; + length += parseInt(a[1], 10); + } + if (a.length === 1) { + length += parseInt(a[0], 10); + } + return length; + }; + + $app.methods.updateDiscord = function () { + if (!this.discordActive || !this.isGameRunning) { + return; + } + var L = this.lastLocation$; if (this.lastLocation.location !== this.lastLocation$.tag) { - var L = API.parseLocation(this.lastLocation.location); - L.worldName = L.worldId; - this.lastLocation$ = L; + if (this.lastLocation.location) { + Discord.SetActive(true); + } + Discord.SetTimestamps(this.lastLocation.date, 0); + L = API.parseLocation(this.lastLocation.location); + L.worldName = ''; + L.worldCapacity = 0; + L.joinUrl = ''; if (L.worldId) { var ref = API.cachedWorlds.get(L.worldId); if (ref) { L.worldName = ref.name; + L.worldCapacity = ref.capacity * 2; } else { API.getWorld({ worldId: L.worldId }).then((args) => { L.worldName = args.ref.name; + L.worldCapacity = args.ref.capacity * 2; return args; }); } + switch (L.accessType) { + case 'public': + L.joinUrl = getLaunchURL(L.worldId, L.instanceId); + L.accessType = 'Public'; + break; + case 'invite+': + L.accessType = 'Invite+'; + break; + case 'invite': + L.accessType = 'Invite'; + break; + case 'friends': + L.accessType = 'Friends'; + break; + case 'friends+': + L.accessType = 'Friends+'; + break; + } + } + this.lastLocation$ = L; + } + var hidePrivate = false; + if ( + (this.discordHideInvite && L.accessType === 'Invite') || + L.accessType === 'Invite+' + ) { + hidePrivate = true; + } + switch (API.currentUser.status) { + case 'active': + L.statusName = 'Online'; + L.statusImage = 'active'; + break; + case 'join me': + L.statusName = 'Join Me'; + L.statusImage = 'joinme'; + break; + case 'ask me': + L.statusName = 'Ask Me'; + L.statusImage = 'askme'; + hidePrivate = true; + break; + case 'busy': + L.statusName = 'Do Not Disturb'; + L.statusImage = 'busy'; + hidePrivate = true; + break; + } + var appId = '883308884863901717'; + var bigIcon = 'vrchat'; + var instanceId = L.instanceId; + var partySize = this.lastLocation.playerList.size; + var partyMaxSize = L.worldCapacity; + var buttonText = 'Join'; + var buttonUrl = L.joinUrl; + if (!this.discordJoinButton) { + buttonText = ''; + buttonUrl = ''; + } + if (!this.discordInstance) { + partySize = 0; + partyMaxSize = 0; + } + if (hidePrivate) { + instanceId = ''; + partySize = 0; + partyMaxSize = 0; + buttonText = ''; + buttonUrl = ''; + } + if ( + (!hidePrivate && + L.worldId === 'wrld_f20326da-f1ac-45fc-a062-609723b097b1') || + L.worldId === 'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' + ) { + // dance world rpc + if (L.worldId === 'wrld_f20326da-f1ac-45fc-a062-609723b097b1') { + appId = '784094509008551956'; + bigIcon = 'pypy'; + } else if ( + L.worldId === 'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' + ) { + appId = '846232616054030376'; + bigIcon = 'vr_dancing'; + } + if (this.nowPlaying.playing) { + L.worldName = this.nowPlaying.name; + Discord.SetTimestamps( + Date.now(), + (this.nowPlaying.startTime + this.nowPlaying.length) * 1000 + ); } } + Discord.SetAssets( + bigIcon, // big icon + 'Powered by VRCX', // big icon hover text + L.statusImage, // small icon + L.statusName, // small icon hover text + instanceId, // party id + partySize, // party size + partyMaxSize, // party max size + buttonText, // button text + buttonUrl, // button url + appId // app id + ); // NOTE // 글자 수가 짧으면 업데이트가 안된다.. - var LL = this.lastLocation$; - if (LL.worldName.length < 2) { - LL.worldName += '\uFFA0'.repeat(2 - LL.worldName.length); + if (L.worldName.length < 2) { + L.worldName += '\uFFA0'.repeat(2 - L.worldName.length); } - if (this.discordInstance) { - Discord.SetText( - LL.worldName, - `#${LL.instanceName} ${LL.accessType}` - ); + if (hidePrivate) { + Discord.SetText('Private', ''); + Discord.SetTimestamps(0, 0); + } else if (this.discordInstance) { + Discord.SetText(L.worldName, L.accessType); } else { - Discord.SetText(LL.worldName, ''); + Discord.SetText(L.worldName, ''); } - Discord.SetActive(this.discordActive); }; - $app.methods.lookupUser = async function (name) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === name) { - this.showUserDialog(ref.id); + $app.methods.lookupUser = async function (ref) { + if (ref.userId) { + this.showUserDialog(ref.userId); + return; + } + for (var ctx of API.cachedUsers.values()) { + if (ctx.displayName === ref.displayName) { + this.showUserDialog(ctx.id); return; } } - this.searchText = name; + try { + var username = encodeURIComponent(ref.displayName.toLowerCase()); + var args = await API.getUserByUsername({username}); + if (args.ref.displayName === ref.displayName) { + this.showUserDialog(args.ref.id); + return; + } + } catch (err) {} + this.searchText = ref.displayName; await this.searchUser(); - for (var ref of this.searchUserResults) { - if (ref.displayName === name) { + for (var ctx of this.searchUserResults) { + if (ctx.displayName === ref.displayName) { this.searchText = ''; this.clearSearch(); - this.showUserDialog(ref.id); + this.showUserDialog(ctx.id); return; } } @@ -7802,6 +8635,7 @@ speechSynthesis.getVoices(); } this.friendLogTable.data = []; this.friendLogTable.data = await database.getFriendLogHistory(); + await API.refreshFriends(); this.friendLogInitStatus = true; }; @@ -7826,6 +8660,7 @@ speechSynthesis.getVoices(); }; this.friendLogTable.data.push(friendLogHistory); database.addFriendLogHistory(friendLogHistory); + this.queueFriendLogNoty(friendLogHistory); var friendLogCurrent = { userId: id, displayName: ctx.displayName, @@ -7834,6 +8669,20 @@ speechSynthesis.getVoices(); this.friendLog.set(id, friendLogCurrent); database.setFriendLogCurrent(friendLogCurrent); this.notifyMenu('friendLog'); + this.deleteFriendRequest(id); + } + }; + + $app.methods.deleteFriendRequest = function (userId) { + var array = $app.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { + if ( + array[i].type === 'friendRequest' && + array[i].senderUserId === userId + ) { + array.splice(i, 1); + return; + } } }; @@ -7850,6 +8699,7 @@ speechSynthesis.getVoices(); }; this.friendLogTable.data.push(friendLogHistory); database.addFriendLogHistory(friendLogHistory); + this.queueFriendLogNoty(friendLogHistory); this.friendLog.delete(id); database.deleteFriendLogCurrent(id); this.notifyMenu('friendLog'); @@ -7882,18 +8732,17 @@ speechSynthesis.getVoices(); displayName: ref.displayName, previousDisplayName: ctx.displayName }; - this.friendLogTable.data.push(friendLogHistory); - database.addFriendLogHistory(friendLogHistory); - } else if (ctx.displayName === null) { + } else { var friendLogHistory = { created_at: new Date().toJSON(), type: 'Friend', userId: ref.id, displayName: ref.displayName }; - this.friendLogTable.data.push(friendLogHistory); - database.addFriendLogHistory(friendLogHistory); } + this.friendLogTable.data.push(friendLogHistory); + database.addFriendLogHistory(friendLogHistory); + this.queueFriendLogNoty(friendLogHistory); var friendLogCurrent = { userId: ref.id, displayName: ref.displayName, @@ -7904,35 +8753,32 @@ speechSynthesis.getVoices(); ctx.displayName = ref.displayName; this.notifyMenu('friendLog'); } - if (ref.$trustLevel && ctx.trustLevel !== ref.$trustLevel) { - if ( - ctx.trustLevel && - ctx.trustLevel !== 'Legendary User' && - ctx.trustLevel !== 'VRChat Team' && - ctx.trustLevel !== 'Nuisance' - ) { - // TODO: remove - var friendLogHistory = { - created_at: new Date().toJSON(), - type: 'TrustLevel', - userId: ref.id, - displayName: ref.displayName, - trustLevel: ref.$trustLevel, - previousTrustLevel: ctx.trustLevel - }; - this.friendLogTable.data.push(friendLogHistory); - database.addFriendLogHistory(friendLogHistory); - var friendLogCurrent = { - userId: ref.id, - displayName: ref.displayName, - trustLevel: ref.$trustLevel - }; - this.friendLog.set(ref.id, friendLogCurrent); - database.setFriendLogCurrent(friendLogCurrent); - this.notifyMenu('friendLog'); - } - ctx.trustLevel = ref.$trustLevel; + if ( + ref.$trustLevel && + ctx.trustLevel && + ctx.trustLevel !== ref.$trustLevel + ) { + var friendLogHistory = { + created_at: new Date().toJSON(), + type: 'TrustLevel', + userId: ref.id, + displayName: ref.displayName, + trustLevel: ref.$trustLevel, + previousTrustLevel: ctx.trustLevel + }; + this.friendLogTable.data.push(friendLogHistory); + database.addFriendLogHistory(friendLogHistory); + this.queueFriendLogNoty(friendLogHistory); + var friendLogCurrent = { + userId: ref.id, + displayName: ref.displayName, + trustLevel: ref.$trustLevel + }; + this.friendLog.set(ref.id, friendLogCurrent); + database.setFriendLogCurrent(friendLogCurrent); + this.notifyMenu('friendLog'); } + ctx.trustLevel = ref.$trustLevel; }; $app.methods.deleteFriendLog = function (row) { @@ -8081,21 +8927,22 @@ speechSynthesis.getVoices(); var {length} = array; for (var i = 0; i < length; ++i) { if (array[i].id === ref.id) { - if (ref.$isDeleted) { - array.splice(i, 1); - } else { - Vue.set(array, i, ref); - } + Vue.set(array, i, ref); return; } } - if (ref.$isDeleted === false) { - $app.notificationTable.data.push(ref); - if (ref.senderUserId !== this.currentUser.id) { - $app.notifyMenu('notification'); - $app.unseenNotifications.push(ref.id); + if (ref.senderUserId !== this.currentUser.id) { + if ( + ref.type !== 'friendRequest' && + ref.type !== 'hiddenFriendRequest' + ) { + database.addNotificationToDatabase(ref); } + $app.notifyMenu('notification'); + $app.unseenNotifications.push(ref.id); + $app.queueNotificationNoty(ref); } + $app.notificationTable.data.push(ref); $app.updateSharedFeed(true); }); @@ -8107,18 +8954,6 @@ speechSynthesis.getVoices(); } }); - API.$on('NOTIFICATION:@DELETE', function (args) { - var {ref} = args; - var array = $app.notificationTable.data; - var {length} = array; - for (var i = 0; i < length; ++i) { - if (array[i].id === ref.id) { - array.splice(i, 1); - return; - } - } - }); - $app.methods.acceptNotification = function (row) { // FIXME: 메시지 수정 this.$confirm('Continue? Accept Friend Request', 'Confirm', { @@ -8136,8 +8971,7 @@ speechSynthesis.getVoices(); }; $app.methods.hideNotification = function (row) { - // FIXME: 메시지 수정 - this.$confirm('Continue? Delete Notification', 'Confirm', { + this.$confirm(`Continue? Decline ${row.type}`, 'Confirm', { confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', type: 'info', @@ -8160,6 +8994,25 @@ speechSynthesis.getVoices(); }); }; + $app.methods.deleteNotificationLog = function (row) { + this.$confirm(`Continue? Delete ${row.type}`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + removeFromArray(this.notificationTable.data, row); + if ( + row.type !== 'friendRequest' && + row.type !== 'hiddenFriendRequest' + ) { + database.deleteNotification(row.id); + } + } + } + }); + }; + $app.methods.acceptRequestInvite = function (row) { this.$confirm('Continue? Send Invite', 'Confirm', { confirmButtonText: 'Confirm', @@ -8366,6 +9219,9 @@ speechSynthesis.getVoices(); $app.data.xsNotifications = configRepository.getBool( 'VRCX_xsNotifications' ); + $app.data.imageNotifications = configRepository.getBool( + 'VRCX_imageNotifications' + ); $app.data.desktopToast = configRepository.getString('VRCX_desktopToast'); $app.data.minimalFeed = configRepository.getBool('VRCX_minimalFeed'); $app.data.displayVRCPlusIconsAsAvatar = configRepository.getBool( @@ -8404,7 +9260,7 @@ speechSynthesis.getVoices(); 'VRCX_autoUpdateVRCX' ); $app.data.branch = configRepository.getString('VRCX_branch'); - var saveOpenVROption = function () { + $app.methods.saveOpenVROption = function () { configRepository.setBool('openVR', this.openVR); configRepository.setBool('openVRAlways', this.openVRAlways); configRepository.setBool('VRCX_overlaybutton', this.overlaybutton); @@ -8422,6 +9278,10 @@ speechSynthesis.getVoices(); ); configRepository.setBool('VRCX_overlayWrist', this.overlayWrist); configRepository.setBool('VRCX_xsNotifications', this.xsNotifications); + configRepository.setBool( + 'VRCX_imageNotifications', + this.imageNotifications + ); configRepository.setString('VRCX_desktopToast', this.desktopToast); configRepository.setBool('VRCX_minimalFeed', this.minimalFeed); configRepository.setBool( @@ -8453,10 +9313,11 @@ speechSynthesis.getVoices(); 'VRCX_vrBackgroundEnabled', this.vrBackgroundEnabled ); + this.updateSharedFeed(true); this.updateVRConfigVars(); }; $app.data.TTSvoices = speechSynthesis.getVoices(); - var saveNotificationTTS = function () { + $app.methods.saveNotificationTTS = function () { speechSynthesis.cancel(); if ( configRepository.getString('VRCX_notificationTTS') === 'Never' && @@ -8470,25 +9331,6 @@ speechSynthesis.getVoices(); ); this.updateVRConfigVars(); }; - $app.watch.openVR = saveOpenVROption; - $app.watch.openVRAlways = saveOpenVROption; - $app.watch.overlaybutton = saveOpenVROption; - $app.watch.hidePrivateFromFeed = saveOpenVROption; - $app.watch.hideDevicesFromFeed = saveOpenVROption; - $app.watch.overlayNotifications = saveOpenVROption; - $app.watch.overlayWrist = saveOpenVROption; - $app.watch.xsNotifications = saveOpenVROption; - $app.watch.desktopToast = saveOpenVROption; - $app.watch.minimalFeed = saveOpenVROption; - $app.watch.displayVRCPlusIconsAsAvatar = saveOpenVROption; - $app.watch.hideTooltips = saveOpenVROption; - $app.watch.worldAutoCacheInvite = saveOpenVROption; - $app.watch.worldAutoCacheGPS = saveOpenVROption; - $app.watch.worldAutoCacheInviteFilter = saveOpenVROption; - $app.watch.worldAutoCacheGPSFilter = saveOpenVROption; - $app.watch.autoSweepVRChatCache = saveOpenVROption; - $app.watch.vrBackgroundEnabled = saveOpenVROption; - $app.watch.notificationTTS = saveNotificationTTS; $app.data.themeMode = configRepository.getString('VRCX_ThemeMode'); if (!$app.data.themeMode) { $app.data.themeMode = 'system'; @@ -8636,6 +9478,7 @@ speechSynthesis.getVoices(); DisplayName: 'VIP', TrustLevel: 'VIP', PortalSpawn: 'Everyone', + AvatarChange: 'Off', Event: 'On', VideoPlay: 'Off', BlockedOnPlayerJoined: 'Off', @@ -8662,6 +9505,7 @@ speechSynthesis.getVoices(); DisplayName: 'Friends', TrustLevel: 'Friends', PortalSpawn: 'Everyone', + AvatarChange: 'Everyone', Event: 'On', VideoPlay: 'On', BlockedOnPlayerJoined: 'Off', @@ -8780,44 +9624,18 @@ speechSynthesis.getVoices(); this.updateVRConfigVars(); }; - sharedRepository.setBool('is_game_running', false); - var isGameRunningStateChange = function () { - sharedRepository.setBool('is_game_running', this.isGameRunning); - this.lastLocation = { - date: 0, - location: '', - name: '', - playerList: [], - friendList: [] - }; - if (this.isGameRunning) { - API.currentUser.$online_for = Date.now(); - API.currentUser.$offline_for = ''; - } else { - API.currentUser.$online_for = ''; - API.currentUser.$offline_for = Date.now(); - this.autoVRChatCacheManagement(); - } - }; - $app.watch.isGameRunning = isGameRunningStateChange; + $app.data.youTubeApi = configRepository.getBool('VRCX_youtubeAPI'); + $app.data.youTubeApiKey = configRepository.getString('VRCX_youtubeAPIKey'); - sharedRepository.setBool('is_Game_No_VR', false); - var isGameNoVRStateChange = function () { - sharedRepository.setBool('is_Game_No_VR', this.isGameNoVR); + var downloadProgressStateChange = function () { + this.updateVRConfigVars(); }; - $app.watch.isGameNoVR = isGameNoVRStateChange; - - var lastLocationStateChange = function () { - sharedRepository.setObject('last_location', $app.lastLocation); - $app.checkVRChatCacheDownload($app.lastLocation.location); - }; - $app.watch['lastLocation.location'] = lastLocationStateChange; + $app.watch.downloadProgress = downloadProgressStateChange; $app.methods.updateVRConfigVars = function () { - if (configRepository.getBool('isDarkMode')) { - var notificationTheme = 'sunset'; - } else { - var notificationTheme = 'relax'; + var notificationTheme = 'relax'; + if (this.isDarkMode) { + notificationTheme = 'sunset'; } var VRConfigVars = { notificationTTS: this.notificationTTS, @@ -8828,15 +9646,35 @@ speechSynthesis.getVoices(); notificationPosition: this.notificationPosition, notificationTimeout: this.notificationTimeout, notificationTheme, - backgroundEnabled: this.vrBackgroundEnabled + backgroundEnabled: this.vrBackgroundEnabled, + isGameRunning: this.isGameRunning, + isGameNoVR: this.isGameNoVR, + downloadProgress: this.downloadProgress }; - sharedRepository.setObject('VRConfigVars', VRConfigVars); - this.updateSharedFeed(true); + var json = JSON.stringify(VRConfigVars); + AppApi.ExecuteVrFeedFunction('configUpdate', json); + AppApi.ExecuteVrOverlayFunction('configUpdate', json); }; - API.$on('LOGIN', function () { - $app.updateVRConfigVars(); - }); + $app.methods.updateVRLastLocation = function () { + var lastLocation = { + date: this.lastLocation.date, + location: this.lastLocation.location, + name: this.lastLocation.name, + playerList: Array.from(this.lastLocation.playerList.values()), + friendList: Array.from(this.lastLocation.friendList.values()) + }; + var json = JSON.stringify(lastLocation); + AppApi.ExecuteVrFeedFunction('lastLocationUpdate', json); + AppApi.ExecuteVrOverlayFunction('lastLocationUpdate', json); + }; + + $app.methods.vrInit = function () { + this.updateVRConfigVars(); + this.updateVRLastLocation(); + this.updateVrNowPlaying(); + this.updateSharedFeed(true); + }; API.$on('LOGIN', function () { $app.currentUserTreeData = []; @@ -8887,11 +9725,11 @@ speechSynthesis.getVoices(); }); }; - $app.methods.updateOpenVR = function () { + $app.methods.updateOpenVR = function (isGameRunning, isGameNoVR) { if ( this.openVR && - this.isGameNoVR === false && - (this.isGameRunning || this.openVRAlways) + !isGameNoVR && + (isGameRunning || this.openVRAlways) ) { AppApi.StartVR(); } else { @@ -9115,6 +9953,7 @@ speechSynthesis.getVoices(); 'VRCX_notificationTimeout', this.notificationTimeout ); + this.updateVRConfigVars(); } } }); @@ -9381,7 +10220,7 @@ speechSynthesis.getVoices(); D.isFriend = true; }); - API.$on('NOTIFICATION:@DELETE', function (args) { + API.$on('NOTIFICATION:EXPIRE', function (args) { var {ref} = args; var D = $app.userDialog; if ( @@ -9587,33 +10426,28 @@ speechSynthesis.getVoices(); var playersInInstance = this.lastLocation.playerList; if ( this.lastLocation.location === L.tag && - playersInInstance.length > 0 + playersInInstance.size > 0 ) { var ref = API.cachedUsers.get(API.currentUser.id); if (typeof ref === 'undefined') { ref = API.currentUser; } - if (playersInInstance.includes(ref.displayName)) { - users.push(ref); + if (playersInInstance.has(ref.displayName)) { + users.push(ref); // add self } var friendsInInstance = this.lastLocation.friendList; - for (var i = 0; i < friendsInInstance.length; i++) { + for (var friend of friendsInInstance.values()) { + // if friend isn't in instance add them var addUser = true; - var player = friendsInInstance[i]; for (var k = 0; k < users.length; k++) { var user = users[k]; - if (user.displayName === player) { + if (friend.displayName === user.displayName) { addUser = false; break; } } - if (addUser) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === player) { - users.push(ref); - break; - } - } + if (addUser && API.cachedUsers.has(friend.userId)) { + users.push(API.cachedUsers.get(friend.userId)); } } } else if (L.isOffline === false) { @@ -9637,7 +10471,7 @@ speechSynthesis.getVoices(); if (L.worldId && this.lastLocation.location === D.ref.location) { D.instance = { id: D.ref.location, - occupants: this.lastLocation.playerList.length + occupants: this.lastLocation.playerList.size }; } if (L.isOffline || L.isPrivate || L.worldId === '') { @@ -10007,6 +10841,8 @@ speechSynthesis.getVoices(); this.displayPreviousImages('User', 'Display'); } else if (command === 'Manage Gallery') { this.showGalleryDialog(); + } else if (command === 'Copy User') { + this.copyUser(D.id); } else { this.$confirm(`Continue? ${command}`, 'Confirm', { confirmButtonText: 'Confirm', @@ -10176,7 +11012,17 @@ speechSynthesis.getVoices(); D.isFavorite = API.cachedFavoritesByObjectId.has(D.id); this.updateVRChatWorldCache(); if (args.cache) { - API.getWorld(args.params); + API.getWorld(args.params) + .catch((err) => { + throw err; + }) + .then((args1) => { + if (D.id === args1.ref.id) { + D.ref = args1.ref; + this.updateVRChatWorldCache(); + } + return args1; + }); } } return args; @@ -10206,7 +11052,7 @@ speechSynthesis.getVoices(); if (lastLocation$.worldId === D.id) { var instance = { id: lastLocation$.instanceId, - occupants: playersInInstance.length, + occupants: playersInInstance.size, users: [] }; instances[instance.id] = instance; @@ -10214,26 +11060,24 @@ speechSynthesis.getVoices(); if (typeof ref === 'undefined') { ref = API.currentUser; } - if (playersInInstance.includes(ref.displayName)) { - instance.users.push(ref); + if (playersInInstance.has(ref.displayName)) { + instance.users.push(ref); // add self } var friendsInInstance = this.lastLocation.friendList; - for (var i = 0; i < friendsInInstance.length; i++) { + for (var friend of friendsInInstance.values()) { + // if friend isn't in instance add them var addUser = true; - var player = friendsInInstance[i]; for (var k = 0; k < instance.users.length; k++) { var user = instance.users[k]; - if (user.displayName === player) { + if (friend.displayName === user.displayName) { addUser = false; break; } } if (addUser) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === player) { - instance.users.push(ref); - break; - } + var ref = API.cachedUsers.get(friend.userId); + if (typeof ref !== 'undefined') { + instance.users.push(ref); } } } @@ -11103,17 +11947,6 @@ speechSynthesis.getVoices(); }); }; - var getLaunchURL = function (worldId, instanceId) { - if (instanceId) { - return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( - worldId - )}&instanceId=${encodeURIComponent(instanceId)}`; - } - return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( - worldId - )}`; - }; - var updateLocationURL = function () { var D = $app.newInstanceDialog; if (D.instanceId) { @@ -11393,6 +12226,14 @@ speechSynthesis.getVoices(); this.copyToClipboard(`https://vrchat.com/home/world/${worldId}`); }; + $app.methods.copyUser = function (userId) { + this.$message({ + message: 'User URL copied to clipboard', + type: 'success' + }); + this.copyToClipboard(`https://vrchat.com/home/user/${userId}`); + }; + // App: VRCPlus Icons API.$on('LOGIN', function () { @@ -12302,6 +13143,10 @@ speechSynthesis.getVoices(); if (val === null) { return; } + if (!val.id) { + this.lookupUser(val); + return; + } this.showUserDialog(val.id); }; @@ -13208,10 +14053,7 @@ speechSynthesis.getVoices(); if (image.file && image.file.url) { var response = await fetch(image.file.url, { method: 'HEAD', - redirect: 'follow', - headers: { - 'User-Agent': appVersion - } + redirect: 'follow' }).catch((error) => { console.log(error); }); @@ -13439,32 +14281,40 @@ speechSynthesis.getVoices(); $app.data.VRChatConfigList = { cache_size: { - name: 'Max Cache Size [GB] (minimum 20)', + name: 'Max Cache Size [GB] (min 20)', default: '20', type: 'number', min: 20 }, cache_expiry_delay: { - name: 'Cache Expiry [Days] (minimum 30)', + name: 'Cache Expiry [Days] (30 - 150)', default: '30', type: 'number', - min: 30 + min: 30, + max: 150 }, cache_directory: { name: 'Custom Cache Folder Location', default: '%AppData%\\..\\LocalLow\\VRChat\\vrchat' }, dynamic_bone_max_affected_transform_count: { - name: 'Dynamic Bones Limit Max Transforms (0 always disable transforms)', + name: 'Dynamic Bones Limit Max Transforms (0 disable all transforms)', default: '32', type: 'number', min: 0 }, dynamic_bone_max_collider_check_count: { - name: 'Dynamic Bones Limit Max Collider Collisions (0 always disable colliders)', + name: 'Dynamic Bones Limit Max Collider Collisions (0 disable all colliders)', default: '8', type: 'number', min: 0 + }, + fpv_steadycam_fov: { + name: 'First-Person Steadycam FOV', + default: '50', + type: 'number', + min: 30, + max: 110 } }; @@ -13591,6 +14441,57 @@ speechSynthesis.getVoices(); this.VRChatConfigFile.screenshot_res_width = res.width; }; + // YouTube API + + $app.data.youTubeApiKey = ''; + + $app.data.youTubeApiDialog = { + visible: false + }; + + API.$on('LOGOUT', function () { + $app.youTubeApiDialog.visible = false; + }); + + $app.methods.testYouTubeApiKey = async function () { + if (!this.youTubeApiKey) { + this.$message({ + message: 'YouTube API key removed', + type: 'success' + }); + this.youTubeApiDialog.visible = false; + return; + } + var data = await this.lookupYouTubeVideo('dQw4w9WgXcQ'); + if (!data) { + this.youTubeApiKey = ''; + this.$message({ + message: 'Invalid YouTube API key', + type: 'error' + }); + } else { + configRepository.setString( + 'VRCX_youtubeAPIKey', + this.youTubeApiKey + ); + this.$message({ + message: 'YouTube API key valid!', + type: 'success' + }); + this.youTubeApiDialog.visible = false; + } + }; + + $app.methods.changeYouTubeApi = function () { + configRepository.setBool('VRCX_youtubeAPI', this.youTubeApi); + }; + + $app.methods.showYouTubeApiDialog = function () { + this.$nextTick(() => adjustDialogZ(this.$refs.youTubeApiDialog.$el)); + var D = this.youTubeApiDialog; + D.visible = true; + }; + // Asset Bundle Cacher $app.methods.updateVRChatWorldCache = function () { @@ -13676,6 +14577,8 @@ speechSynthesis.getVoices(); }); }; + $app.data.cacheAutoDownloadHistory = new Set(); + $app.methods.downloadVRChatCache = async function () { if (this.downloadQueue.size === 0) { return; @@ -13725,12 +14628,23 @@ speechSynthesis.getVoices(); this.downloadVRChatCache(); return; } - try { - var args = await API.getBundles(fileId); - } catch (err) { - this.downloadCurrent.status = 'API request failed'; - this.downloadCurrent.date = Date.now(); - this.downloadHistoryTable.data.unshift(this.downloadCurrent); + if ( + this.downloadCurrent.type !== 'Auto' && + !this.cacheAutoDownloadHistory.has(assetUrl) + ) { + this.cacheAutoDownloadHistory.add(assetUrl); + try { + var args = await API.getBundles(fileId); + } catch (err) { + this.downloadCurrent.status = 'API request failed'; + this.downloadCurrent.date = Date.now(); + this.downloadHistoryTable.data.unshift(this.downloadCurrent); + this.downloadCurrent = {}; + this.downloadInProgress = false; + this.downloadVRChatCache(); + return; + } + } else { this.downloadCurrent = {}; this.downloadInProgress = false; this.downloadVRChatCache(); @@ -13769,12 +14683,13 @@ speechSynthesis.getVoices(); this.downloadVRChatCacheProgress(); }; - $app.methods.checkVRChatCacheDownload = function (lastLocation) { - var L = API.parseLocation(lastLocation); + $app.methods.cancelVRChatCacheDownload = function (location) { + var L = API.parseLocation(location); if (L.worldId) { if (this.downloadCurrent.id === L.worldId) { - this.cancelVRChatCacheDownload(L.worldId); - } else if (this.downloadQueue.has(L.worldId)) { + AssetBundleCacher.CancelDownload(); + } + if (this.downloadQueue.has(L.worldId)) { this.downloadQueue.delete(L.worldId); this.downloadQueueTable.data = Array.from( this.downloadQueue.values() @@ -13783,18 +14698,6 @@ speechSynthesis.getVoices(); } }; - $app.methods.cancelVRChatCacheDownload = function (worldId) { - if (this.downloadCurrent.id === worldId) { - AssetBundleCacher.CancelDownload(); - } - if (this.downloadQueue.has(worldId)) { - this.downloadQueue.delete(worldId); - this.downloadQueueTable.data = Array.from( - this.downloadQueue.values() - ); - } - }; - $app.methods.cancelAllVRChatCacheDownload = function () { if (typeof this.downloadCurrent.id !== 'undefined') { this.cancelVRChatCacheDownload(this.downloadCurrent.id); @@ -13856,7 +14759,8 @@ speechSynthesis.getVoices(); if ( !L.worldId || this.downloadQueue.has(L.worldId) || - this.downloadCurrent.id === L.worldId + this.downloadCurrent.id === L.worldId || + this.lastLocationDestination === location ) { return; } @@ -13889,11 +14793,6 @@ speechSynthesis.getVoices(); $app.data.downloadQueue = new Map(); $app.data.downloadCurrent = {}; - var downloadProgressUpdateWrist = function () { - sharedRepository.setInt('downloadProgress', this.downloadProgress); - }; - $app.watch.downloadProgress = downloadProgressUpdateWrist; - $app.methods.downloadVRChatCacheProgress = async function () { var downloadProgress = await AssetBundleCacher.CheckDownloadProgress(); switch (downloadProgress) { @@ -14500,7 +15399,7 @@ speechSynthesis.getVoices(); this.$nextTick(() => adjustDialogZ(this.$refs.VRCXUpdateDialog.$el)); var D = this.VRCXUpdateDialog; D.visible = true; - D.updatePending = await AppApi.checkForUpdateZip(); + D.updatePending = await AppApi.CheckForUpdateZip(); this.loadBranchVersions(); }; @@ -14591,7 +15490,7 @@ speechSynthesis.getVoices(); }; $app.methods.checkForVRCXUpdate = async function () { - if (await AppApi.checkForUpdateZip()) { + if (await AppApi.CheckForUpdateZip()) { return; } var url = this.branches[this.branch].urlLatest; diff --git a/html/src/index.pug b/html/src/index.pug index c1c9de07..8db7399b 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -173,6 +173,7 @@ html span(v-else-if="scope.row.status === 'busy'") Do Not Disturb span(v-else) Offline i.x-user-status(:class="statusClass(scope.row.status)") + span ‎ span(v-text="scope.row.statusDescription") template(v-else-if="scope.row.type === 'Avatar'") avatar-info(:imageurl="scope.row.currentAvatarImageUrl" :userid="scope.row.userId" :hintownerid="scope.row.ownerId" :hintavatarname="scope.row.avatarName") @@ -183,10 +184,10 @@ html template(#tool) div(style="margin:0 0 10px;display:flex;align-items:center") el-select(v-model="gameLogTable.filters[0].value" @change="saveTableFilters" multiple clearable collapse-tags style="flex:1" placeholder="Filter") - el-option(v-once v-for="type in ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'PortalSpawn', 'Event', 'VideoPlay']" :key="type" :label="type" :value="type") + el-option(v-once v-for="type in ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'PortalSpawn', 'AvatarChange', 'Event', 'VideoPlay']" :key="type" :label="type" :value="type") el-input(v-model="gameLogTable.filters[1].value" placeholder="Search" style="flex:none;width:150px;margin:0 10px") - el-tooltip(placement="bottom" content="Reset game log" :disabled="hideTooltips") - el-button(type="default" @click="resetGameLog()" icon="el-icon-refresh" circle style="flex:none") + el-tooltip(placement="bottom" content="Reload game log" :disabled="hideTooltips") + el-button(type="default" @click="refreshEntireGameLog" icon="el-icon-refresh" circle style="flex:none") el-table-column(label="Date" prop="created_at" sortable="custom" width="90") template(v-once #default="scope") el-tooltip(placement="right") @@ -194,17 +195,28 @@ html span {{ scope.row.created_at | formatDate('YYYY-MM-DD HH24:MI:SS') }} span {{ scope.row.created_at | formatDate('MM-DD HH24:MI') }} el-table-column(label="Type" prop="type" width="120") + template(v-once #default="scope") + span.x-link(v-if="scope.row.location && scope.row.type !== 'Location'" v-text="scope.row.type" @click="showWorldDialog(scope.row.location)") + span(v-else v-text="scope.row.type") + el-table-column(label="User" prop="displayName" width="160") + template(v-once #default="scope") + span.x-link(v-text="scope.row.displayName" @click="lookupUser(scope.row)") el-table-column(label="Detail" prop="data") template(v-once #default="scope") - location(v-if="scope.row.type === 'Location'" :location="scope.row.data[0]" :hint="scope.row.data[1]") + location(v-if="scope.row.type === 'Location'" :location="scope.row.location" :hint="scope.row.worldName") + location(v-else-if="scope.row.type === 'PortalSpawn'" :location="scope.row.instanceId" :hint="scope.row.worldName") + template(v-else-if="scope.row.type === 'AvatarChange'") + span.x-link(@click="showUserDialog(scope.row.authorId)" v-text="scope.row.name") + template(v-if="scope.row.description && scope.row.name !== scope.row.description") + | - {{ scope.row.description }} template(v-else-if="scope.row.type === 'Event'") span(v-text="scope.row.data") template(v-else-if="scope.row.type === 'VideoPlay'") - span.x-link(v-text="scope.row.data" @click="openExternalLink(scope.row.data)") - template(v-if="scope.row.displayName") - span.x-link(@click="lookupUser(scope.row.displayName)") ({{ scope.row.displayName }}) - template(v-else-if="scope.row.type === 'Notification'") - span.x-link(v-else v-text="scope.row.data" @click="lookupUser(scope.row.data)") + span(v-if="scope.row.videoId") {{ scope.row.videoId }}: + span.x-link(v-if="scope.row.videoName" @click="openExternalLink(scope.row.videoUrl)" v-text="scope.row.videoName") + span.x-link(v-else @click="openExternalLink(scope.row.videoUrl)" v-text="scope.row.videoUrl") + template(v-else-if="scope.row.type === 'Notification' || scope.row.type === 'OnPlayerJoined' || scope.row.type === 'OnPlayerLeft'") + span.x-link(v-else v-text="scope.row.data") //- search .x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'search'") @@ -458,15 +470,26 @@ html span(v-else-if='scope.row.details && scope.row.details.inviteMessage' v-text="scope.row.details.inviteMessage") span(v-else-if='scope.row.details && scope.row.details.requestMessage' v-text="scope.row.details.requestMessage") span(v-else-if='scope.row.details && scope.row.details.responseMessage' v-text="scope.row.details.responseMessage") - el-table-column(label="Action" width="80" align="right") + el-table-column(label="Action" width="100" align="right") template(v-once #default="scope") - template(v-if="scope.row.senderUserId !== API.currentUser.id") - el-button(v-if="scope.row.type === 'friendRequest'" type="text" icon="el-icon-check" size="mini" @click="acceptNotification(scope.row)") - el-button(v-else-if="scope.row.type === 'invite'" type="text" icon="el-icon-chat-line-square" size="mini" @click="showSendInviteResponseDialog(scope.row)") + template(v-if="scope.row.senderUserId !== API.currentUser.id && !scope.row.$isExpired") + template(v-if="scope.row.type === 'friendRequest'") + el-tooltip(placement="top" content="Accept" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-check" size="mini" @click="acceptNotification(scope.row)") + template(v-else-if="scope.row.type === 'invite'") + el-tooltip(placement="top" content="Decline with message" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-chat-line-square" size="mini" @click="showSendInviteResponseDialog(scope.row)") template(v-else-if="scope.row.type === 'requestInvite'") - el-button(v-if="lastLocation.location && isGameRunning && !checkCanInvite(lastLocation.location)" type="text" icon="el-icon-check" size="mini" @click="acceptRequestInvite(scope.row)") - el-button(type="text" icon="el-icon-chat-line-square" size="mini" style="margin-left:5px" @click="showSendInviteRequestResponseDialog(scope.row)") - el-button(type="text" icon="el-icon-close" size="mini" style="margin-left:5px" @click="hideNotification(scope.row)") + template(v-if="lastLocation.location && isGameRunning && !checkCanInvite(lastLocation.location)") + el-tooltip(placement="top" content="Invite" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-check" size="mini" @click="acceptRequestInvite(scope.row)") + el-tooltip(placement="top" content="Decline with message" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-chat-line-square" size="mini" style="margin-left:5px" @click="showSendInviteRequestResponseDialog(scope.row)") + template(v-if="scope.row.type !== 'requestInviteResponse' && scope.row.type !== 'inviteResponse' && scope.row.type !== 'message'") + el-tooltip(placement="top" content="Decline" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-close" size="mini" style="margin-left:5px" @click="hideNotification(scope.row)") + el-tooltip(placement="top" content="Delete log" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="deleteNotificationLog(scope.row)") //- profile .x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'profile'") @@ -696,10 +719,10 @@ html el-radio-button(label="dark") Dark div.options-container-item span.name VRCPlus Profile Icons - el-switch(v-model="displayVRCPlusIconsAsAvatar") + el-switch(v-model="displayVRCPlusIconsAsAvatar" @change="saveOpenVROption") div.options-container-item span.name Disable Tooltips - el-switch(v-model="hideTooltips") + el-switch(v-model="hideTooltips" @change="saveOpenVROption") div.options-container span.header Side Panel br @@ -766,10 +789,16 @@ html span * Only works when VRChat is running. div.options-container-item span.name Enable - el-switch(v-model="discordActive") + el-switch(v-model="discordActive" @change="saveDiscordOption") div.options-container-item - span.name Instance details - el-switch(v-model="discordInstance" :disabled="!discordActive") + span.name Instance type/player count + el-switch(v-model="discordInstance" @change="saveDiscordOption" :disabled="!discordActive") + div.options-container-item + span.name Join button (public only) + el-switch(v-model="discordJoinButton" @change="saveDiscordOption" :disabled="!discordActive") + div.options-container-item + span.name Hide world details in private + el-switch(v-model="discordHideInvite" @change="saveDiscordOption" :disabled="!discordActive") div.options-container span.header SteamVR Overlay div.options-container-item @@ -782,61 +811,64 @@ html br div.options-container-item span.name Enable - el-switch(v-model="openVR") + el-switch(v-model="openVR" @change="saveOpenVROption") div.options-container-item span.name Force Run (Opens SteamVR) - el-switch(v-model="openVRAlways" :disabled="!openVR") + el-switch(v-model="openVRAlways" @change="saveOpenVROption" :disabled="!openVR") div.options-container-item span.name Hide Private Worlds - el-switch(v-model="hidePrivateFromFeed") + el-switch(v-model="hidePrivateFromFeed" @change="saveOpenVROption") br span.sub-header Wrist Feed div.options-container-item span.name Wrist Feed Overlay - el-switch(v-model="overlayWrist" :disabled="!openVR") + el-switch(v-model="overlayWrist" @change="saveOpenVROption" :disabled="!openVR") div.options-container-item span.name(style="min-width:137px") Overlay Button - el-switch(v-model="overlaybutton" inactive-text="Grip" active-text="Menu" :disabled="!openVR || !overlayWrist") + el-switch(v-model="overlaybutton" @change="saveOpenVROption" inactive-text="Grip" active-text="Menu" :disabled="!openVR || !overlayWrist") div.options-container-item span.name Background Color - el-switch(v-model="vrBackgroundEnabled" :disabled="!openVR || !overlayWrist") + el-switch(v-model="vrBackgroundEnabled" @change="saveOpenVROption" :disabled="!openVR || !overlayWrist") div.options-container-item span.name Minimal Feed Icons - el-switch(v-model="minimalFeed" :disabled="!openVR || !overlayWrist") + el-switch(v-model="minimalFeed" @change="saveOpenVROption" :disabled="!openVR || !overlayWrist") div.options-container-item span.name Hide VR Devices - el-switch(v-model="hideDevicesFromFeed" :disabled="!openVR || !overlayWrist") + el-switch(v-model="hideDevicesFromFeed" @change="saveOpenVROption" :disabled="!openVR || !overlayWrist") div.options-container-item - el-button(size="small" icon="el-icon-notebook-2" @click="showWristFeedFiltersDialog()" :disabled="!openVR || !overlayWrist") Wrist Feed Filters + el-button(size="small" icon="el-icon-notebook-2" @click="showWristFeedFiltersDialog" :disabled="!openVR || !overlayWrist") Wrist Feed Filters br span.sub-header Notifications div.options-container-item span.name Overlay Notifications - el-switch(v-model="overlayNotifications" :disabled="!openVR") + el-switch(v-model="overlayNotifications" @change="saveOpenVROption" :disabled="!openVR") div.options-container-item - el-button(size="small" icon="el-icon-rank" @click="showNotificationPositionDialog()" :disabled="!overlayNotifications || !openVR") Notification Position + el-button(size="small" icon="el-icon-rank" @click="showNotificationPositionDialog" :disabled="!overlayNotifications || !openVR") Notification Position div.options-container-item span.name XSOverlay Notifications - el-switch(v-model="xsNotifications") + el-switch(v-model="xsNotifications" @change="saveOpenVROption") div.options-container-item - el-button(size="small" icon="el-icon-time" @click="promptNotificationTimeout()" :disabled="(!overlayNotifications || !openVR) && !xsNotifications") Notification Timeout + span.name User images (slower) + el-switch(v-model="imageNotifications" @change="saveOpenVROption") + div.options-container-item + el-button(size="small" icon="el-icon-time" @click="promptNotificationTimeout" :disabled="(!overlayNotifications || !openVR) && !xsNotifications") Notification Timeout div.options-container-item span.name Desktop Notifications, When to display: br - el-radio-group(v-model="desktopToast" size="mini") + el-radio-group(v-model="desktopToast" @change="saveOpenVROption" size="mini") el-radio-button(label="Never") el-radio-button(label="Inside VR") el-radio-button(label="Game Closed") el-radio-button(label="Game Running") el-radio-button(label="Always") div.options-container-item - el-button(size="small" icon="el-icon-chat-square" @click="showNotyFeedFiltersDialog()") Notification Filters + el-button(size="small" icon="el-icon-chat-square" @click="showNotyFeedFiltersDialog") Notification Filters br span.sub-header Text-To-Speech Options div.options-container-item span.name Notification TTS, When to play: br - el-radio-group(v-model="notificationTTS" size="mini") + el-radio-group(v-model="notificationTTS" @change="saveNotificationTTS" size="mini") el-radio-button(label="Never") el-radio-button(label="Inside VR") el-radio-button(label="Game Closed") @@ -857,31 +889,30 @@ html div.options-container-item span.name Download on invite: br - el-radio-group(v-model="worldAutoCacheInvite" size="mini") + el-radio-group(v-model="worldAutoCacheInvite" @change="saveOpenVROption" size="mini") el-radio-button(label="Never") el-radio-button(label="Game Closed") el-radio-button(label="Game Running") el-radio-button(label="Always") div.options-container-item - el-switch(v-model="worldAutoCacheInviteFilter" inactive-text="VIP" active-text="Everyone" :disabled="worldAutoCacheInvite == 'Never'") + el-switch(v-model="worldAutoCacheInviteFilter" @change="saveOpenVROption" inactive-text="VIP" active-text="Everyone" :disabled="worldAutoCacheInvite === 'Never'") div.options-container-item span.name Download on GPS: br - el-radio-group(v-model="worldAutoCacheGPS" size="mini") + el-radio-group(v-model="worldAutoCacheGPS" @change="saveOpenVROption" size="mini") el-radio-button(label="Never") el-radio-button(label="Game Closed") el-radio-button(label="Game Running") el-radio-button(label="Always") div.options-container-item - el-switch(v-model="worldAutoCacheGPSFilter" inactive-text="VIP" active-text="Everyone" :disabled="worldAutoCacheGPS == 'Never'") + el-switch(v-model="worldAutoCacheGPSFilter" @change="saveOpenVROption" inactive-text="VIP" active-text="Everyone" :disabled="worldAutoCacheGPS === 'Never'") div.options-container-item - el-button-group - el-button(size="small" icon="el-icon-download" @click="showDownloadDialog()") Download History + el-button(size="small" icon="el-icon-download" @click="showDownloadDialog") Download History br span.sub-header Automatically Manage Cache When Closing VRChat div.options-container-item span.name(style="min-width:300px") Auto delete old versions from cache - el-switch(v-model="autoSweepVRChatCache") + el-switch(v-model="autoSweepVRChatCache" @change="saveOpenVROption") div.options-container span.header Application div.options-container-item @@ -900,6 +931,13 @@ html el-button-group el-button(size="small" icon="el-icon-s-operation" @click="showLaunchOptions()") Launch Options el-button(size="small" icon="el-icon-s-operation" @click="showVRChatConfig()") VRChat config.json + div.options-container + span.header YouTube API + div.options-container-item + span.name Enabled + el-switch(v-model="youTubeApi" @change="changeYouTubeApi") + div.options-container-item + el-button(size="small" icon="el-icon-caret-right" @click="showYouTubeApiDialog") YouTube API Key div.options-container(style="margin-top:45px;border-top:1px solid #eee;padding-top:30px") span.header Legal Notice div.options-container-item @@ -1053,6 +1091,7 @@ html el-button(:type="(userDialog.incomingRequest || userDialog.outgoingRequest) ? 'success' : (userDialog.isBlock || userDialog.isMute || userDialog.isHideAvatar) ? 'danger' : 'default'" icon="el-icon-more" circle style="margin-left:5px") el-dropdown-menu(#default="dropdown") el-dropdown-item(icon="el-icon-refresh" command="Refresh") Refresh + el-dropdown-item(icon="el-icon-s-order" command="Copy User") Copy User URL template(v-if="userDialog.ref.id === API.currentUser.id") el-dropdown-item(icon="el-icon-picture-outline" command="Manage Gallery" divided) Manage Gallery/Icons el-dropdown-item(icon="el-icon-s-custom" command="Show Avatar Author") Show Avatar Author @@ -1075,7 +1114,7 @@ html el-dropdown-item(v-else icon="el-icon-plus" command="Send Friend Request") Send Friend Request el-dropdown-item(icon="el-icon-s-custom" command="Show Avatar Author" divided) Show Avatar Author el-dropdown-item(icon="el-icon-s-custom" command="Show Fallback Avatar Details") Show Fallback Avatar Details - el-dropdown-item(v-if="userDialog.currentAvatarImageUrl !== 'https://assets.vrchat.com/system/defaultAvatar.png'" icon="el-icon-picture-outline" command="Previous Images") Show Avatar Previous Images + el-dropdown-item(v-if="userDialog.ref.currentAvatarImageUrl !== 'https://assets.vrchat.com/system/defaultAvatar.png'" icon="el-icon-picture-outline" command="Previous Images") Show Avatar Previous Images el-dropdown-item(v-if="userDialog.isBlock" icon="el-icon-circle-check" command="Unblock" divided style="color:#F56C6C") Unblock el-dropdown-item(v-else icon="el-icon-circle-close" command="Block" divided :disabled="userDialog.ref.$isModerator") Block el-dropdown-item(v-if="userDialog.isMute" icon="el-icon-microphone" command="Unmute" style="color:#F56C6C") Unmute @@ -1665,6 +1704,16 @@ html el-button(size="small" @click="VRChatConfigDialog.visible = false") Cancel el-button(type="primary" size="small" :disabled="VRChatConfigDialog.loading" @click="saveVRChatConfigFile") Save + //- dialog: YouTube Api Dialog + el-dialog.x-dialog(ref="youTubeApiDialog" :visible.sync="youTubeApiDialog.visible" title="YouTube API" width="400px") + div(style='font-size:12px;') + | Enter your YouTube API Key (optional) #[br] + el-input(type="textarea" v-model="youTubeApiKey" placeholder="YouTube API Key" maxlength="39" show-word-limit style="dispaly:block;margin-top:10px") + template(#footer) + div(style="display:flex") + el-button(size="small" @click="openExternalLink('https://rapidapi.com/blog/how-to-get-youtube-api-key/')") Guide + el-button(type="primary" size="small" @click="testYouTubeApiKey" style="margin-left:auto") Save + //- dialog: Cache Download el-dialog.x-dialog(ref="downloadDialog" :visible.sync="downloadDialog.visible" title="Download History" width="770px") div(v-if="downloadInProgress && downloadCurrent.ref") @@ -1769,7 +1818,7 @@ html el-button(type="primary" size="small" style="margin-left:auto" @click="notificationPositionDialog.visible = false") OK //- dialog: Noty feed filters - el-dialog.x-dialog(ref="notyFeedFiltersDialog" :visible.sync="notyFeedFiltersDialog.visible" title="Notification Filters" width="480px") + el-dialog.x-dialog(ref="notyFeedFiltersDialog" :visible.sync="notyFeedFiltersDialog.visible" title="Notification Filters" width="500px") .toggle-list .toggle-item span.toggle-name OnPlayerJoining @@ -1868,21 +1917,30 @@ html el-radio-button(label="Friends") .toggle-item span.toggle-name Portal Spawn + el-tooltip(placement="top" style="margin-left:5px" content="Requires '--enable-sdk-log-levels' steam launch option") + i.el-icon-warning el-radio-group(v-model="sharedFeedFilters.noty.PortalSpawn" size="mini") el-radio-button(label="Off") el-radio-button(label="VIP") el-radio-button(label="Friends") el-radio-button(label="Everyone") - .toggle-item - span.toggle-name Events - el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini") - el-radio-button(label="Off") - el-radio-button(label="On") + //- .toggle-item + //- span.toggle-name Avatar Change + //- el-radio-group(v-model="sharedFeedFilters.noty.AvatarChange" size="mini") + //- el-radio-button(label="Off") + //- el-radio-button(label="VIP") + //- el-radio-button(label="Friends") + //- el-radio-button(label="Everyone") .toggle-item span.toggle-name Video Play el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini") el-radio-button(label="Off") el-radio-button(label="On") + .toggle-item + span.toggle-name Events + el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") .toggle-item span.toggle-name Blocked Player Joins el-radio-group(v-model="sharedFeedFilters.noty.BlockedOnPlayerJoined" size="mini") @@ -1916,7 +1974,7 @@ html el-button(type="primary" size="small" style="margin-left:10px" @click="saveSharedFeedFilters") Save //- dialog: wrist feed filters - el-dialog.x-dialog(ref="wristFeedFiltersDialog" :visible.sync="wristFeedFiltersDialog.visible" title="Wrist Feed Filters" width="480px") + el-dialog.x-dialog(ref="wristFeedFiltersDialog" :visible.sync="wristFeedFiltersDialog.visible" title="Wrist Feed Filters" width="500px") .toggle-list .toggle-item span.toggle-name Self Location @@ -2020,19 +2078,28 @@ html el-radio-button(label="Friends") .toggle-item span.toggle-name Portal Spawn + el-tooltip(placement="top" style="margin-left:5px" content="Requires '--enable-sdk-log-levels' steam launch option") + i.el-icon-warning el-radio-group(v-model="sharedFeedFilters.wrist.PortalSpawn" size="mini") el-radio-button(label="Off") el-radio-button(label="VIP") el-radio-button(label="Friends") el-radio-button(label="Everyone") + //- .toggle-item + //- span.toggle-name Avatar Change + //- el-radio-group(v-model="sharedFeedFilters.wrist.AvatarChange" size="mini") + //- el-radio-button(label="Off") + //- el-radio-button(label="VIP") + //- el-radio-button(label="Friends") + //- el-radio-button(label="Everyone") .toggle-item - span.toggle-name Events - el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini") + span.toggle-name Video Play + el-radio-group(v-model="sharedFeedFilters.wrist.VideoPlay" size="mini") el-radio-button(label="Off") el-radio-button(label="On") .toggle-item - span.toggle-name Video Play - el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini") + span.toggle-name Events + el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini") el-radio-button(label="Off") el-radio-button(label="On") .toggle-item diff --git a/html/src/repository/database.js b/html/src/repository/database.js index 0ec42f4a..b145567e 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -1,7 +1,7 @@ import sqliteService from '../service/sqlite.js'; class Database { - async init(userId) { + async initUserTables(userId) { Database.userId = userId.replaceAll('-', '').replaceAll('_', ''); await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS ${Database.userId}_feed_gps (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, location TEXT, world_name TEXT, previous_location TEXT, time INTEGER)` @@ -21,11 +21,32 @@ class Database { await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS ${Database.userId}_friend_log_history (id INTEGER PRIMARY KEY, created_at TEXT, type TEXT, user_id TEXT, display_name TEXT, previous_display_name TEXT, trust_level TEXT, previous_trust_level TEXT)` ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS ${Database.userId}_notifications (id TEXT PRIMARY KEY, created_at TEXT, type TEXT, sender_user_id TEXT, sender_username TEXT, receiver_user_id TEXT, message TEXT, world_id TEXT, world_name TEXT, image_url TEXT, invite_message TEXT, request_message TEXT, response_message TEXT, expired INTEGER)` + ); await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS memos (user_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)` ); } + async initTables() { + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_location (id INTEGER PRIMARY KEY, created_at TEXT, location TEXT, world_id TEXT, world_name TEXT, time INTEGER, UNIQUE(created_at, location))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_join_leave (id INTEGER PRIMARY KEY, created_at TEXT, type TEXT, display_name TEXT, location TEXT, user_id TEXT, time INTEGER, UNIQUE(created_at, type, display_name))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_portal_spawn (id INTEGER PRIMARY KEY, created_at TEXT, display_name TEXT, location TEXT, user_id TEXT, instance_id TEXT, world_name TEXT, UNIQUE(created_at, display_name))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_video_play (id INTEGER PRIMARY KEY, created_at TEXT, video_url TEXT, video_name TEXT, video_id TEXT, location TEXT, display_name TEXT, user_id TEXT, UNIQUE(created_at, video_url))` + ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS gamelog_event (id INTEGER PRIMARY KEY, created_at TEXT, data TEXT, UNIQUE(created_at, data))` + ); + } + async getFeedDatabase() { var feedDatabase = []; var date = new Date(); @@ -349,6 +370,259 @@ class Database { } ); } + + async getGamelogDatabase() { + var gamelogDatabase = []; + var date = new Date(); + date.setDate(date.getDate() - 7); // 7 day limit + var dateOffset = date.toJSON(); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'Location', + location: dbRow[2], + worldId: dbRow[3], + worldName: dbRow[4], + time: dbRow[5] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_location WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: dbRow[2], + displayName: dbRow[3], + location: dbRow[4], + userId: dbRow[5], + time: dbRow[6] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_join_leave WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'PortalSpawn', + displayName: dbRow[2], + location: dbRow[3], + userId: dbRow[4], + instanceId: dbRow[5], + worldName: dbRow[6] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_portal_spawn WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'VideoPlay', + videoUrl: dbRow[2], + videoName: dbRow[3], + videoId: dbRow[4], + location: dbRow[5], + displayName: dbRow[6], + userId: dbRow[7] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_video_play WHERE created_at >= date('${dateOffset}')`); + await sqliteService.execute((dbRow) => { + var row = { + rowId: dbRow[0], + created_at: dbRow[1], + type: 'Event', + data: dbRow[2] + }; + gamelogDatabase.unshift(row); + }, `SELECT * FROM gamelog_event WHERE created_at >= date('${dateOffset}')`); + var compareByCreatedAt = function (a, b) { + var A = a.created_at; + var B = b.created_at; + if (A < B) { + return -1; + } + if (A > B) { + return 1; + } + return 0; + }; + gamelogDatabase.sort(compareByCreatedAt); + return gamelogDatabase; + } + + addGamelogLocationToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_location (created_at, location, world_id, world_name, time) VALUES (@created_at, @location, @world_id, @world_name, @time)`, + { + '@created_at': entry.created_at, + '@location': entry.location, + '@world_id': entry.worldId, + '@world_name': entry.worldName, + '@time': entry.time + } + ); + } + + updateGamelogLocationTimeToDatabase(entry) { + sqliteService.executeNonQuery( + `UPDATE gamelog_location SET time = @time WHERE created_at = @created_at`, + { + '@created_at': entry.created_at, + '@time': entry.time + } + ); + } + + addGamelogJoinLeaveToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_join_leave (created_at, type, display_name, location, user_id, time) VALUES (@created_at, @type, @display_name, @location, @user_id, @time)`, + { + '@created_at': entry.created_at, + '@type': entry.type, + '@display_name': entry.displayName, + '@location': entry.location, + '@user_id': entry.userId, + '@time': entry.time + } + ); + } + + addGamelogPortalSpawnToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_portal_spawn (created_at, display_name, location, user_id, instance_id, world_name) VALUES (@created_at, @display_name, @location, @user_id, @instance_id, @world_name)`, + { + '@created_at': entry.created_at, + '@display_name': entry.displayName, + '@location': entry.location, + '@user_id': entry.userId, + '@instance_id': entry.instanceId, + '@world_name': entry.worldName + } + ); + } + + addGamelogVideoPlayToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_video_play (created_at, video_url, video_name, video_id, location, display_name, user_id) VALUES (@created_at, @video_url, @video_name, @video_id, @location, @display_name, @user_id)`, + { + '@created_at': entry.created_at, + '@video_url': entry.videoUrl, + '@video_name': entry.videoName, + '@video_id': entry.videoId, + '@location': entry.location, + '@display_name': entry.displayName, + '@user_id': entry.userId + } + ); + } + + addGamelogEventToDatabase(entry) { + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO gamelog_event (created_at, data) VALUES (@created_at, @data)`, + { + '@created_at': entry.created_at, + '@data': entry.data + } + ); + } + + async getNotifications() { + var notifications = []; + await sqliteService.execute((dbRow) => { + var row = { + id: dbRow[0], + created_at: dbRow[1], + type: dbRow[2], + senderUserId: dbRow[3], + senderUsername: dbRow[4], + receiverUserId: dbRow[5], + message: dbRow[6], + details: { + worldId: dbRow[7], + worldName: dbRow[8], + imageUrl: dbRow[9], + inviteMessage: dbRow[10], + requestMessage: dbRow[11], + responseMessage: dbRow[12] + } + }; + row.$isExpired = false; + if (dbRow[13] === 1) { + row.$isExpired = true; + } + notifications.unshift(row); + }, `SELECT * FROM ${Database.userId}_notifications LIMIT 5000`); + return notifications; + } + + addNotificationToDatabase(row) { + var entry = { + id: '', + created_at: '', + type: '', + senderUserId: '', + senderUsername: '', + receiverUserId: '', + message: '', + ...row, + details: { + worldId: '', + worldName: '', + imageUrl: '', + inviteMessage: '', + requestMessage: '', + responseMessage: '', + ...row.details + } + }; + var expired = 0; + if (row.$isExpired) { + expired = 1; + } + sqliteService.executeNonQuery( + `INSERT OR IGNORE INTO ${Database.userId}_notifications (id, created_at, type, sender_user_id, sender_username, receiver_user_id, message, world_id, world_name, image_url, invite_message, request_message, response_message, expired) VALUES (@id, @created_at, @type, @sender_user_id, @sender_username, @receiver_user_id, @message, @world_id, @world_name, @image_url, @invite_message, @request_message, @response_message, @expired)`, + { + '@id': entry.id, + '@created_at': entry.created_at, + '@type': entry.type, + '@sender_user_id': entry.senderUserId, + '@sender_username': entry.senderUsername, + '@receiver_user_id': entry.receiverUserId, + '@message': entry.message, + '@world_id': entry.details.worldId, + '@world_name': entry.details.worldName, + '@image_url': entry.details.imageUrl, + '@invite_message': entry.details.inviteMessage, + '@request_message': entry.details.requestMessage, + '@response_message': entry.details.responseMessage, + '@expired': expired + } + ); + } + + deleteNotification(rowId) { + sqliteService.executeNonQuery( + `DELETE FROM ${Database.userId}_notifications WHERE id = @row_id`, + { + '@row_id': rowId + } + ); + } + + updateNotificationExpired(entry) { + var expired = 0; + if (entry.$isExpired) { + expired = 1; + } + sqliteService.executeNonQuery( + `UPDATE ${Database.userId}_notifications SET expired = @expired WHERE id = @id`, + { + '@id': entry.id, + '@expired': expired + } + ); + } } var self = new Database(); diff --git a/html/src/service/gamelog.js b/html/src/service/gamelog.js index 598e435d..e1e56af4 100644 --- a/html/src/service/gamelog.js +++ b/html/src/service/gamelog.js @@ -1,93 +1,82 @@ // requires binding of LogWatcher -// -var contextMap = new Map(); - -function parseRawGameLog(dt, type, args) { - var gameLog = { - dt, - type - }; - - switch (type) { - case 'location': - gameLog.location = args[0]; - gameLog.worldName = args[1]; - break; - - case 'player-joined': - gameLog.userDisplayName = args[0]; - gameLog.userType = args[1]; - break; - - case 'player-left': - gameLog.userDisplayName = args[0]; - break; - - case 'notification': - gameLog.json = args[0]; - break; - - case 'portal-spawn': - gameLog.userDisplayName = args[0]; - break; - - case 'event': - gameLog.event = args[0]; - break; - - case 'video-play': - gameLog.videoURL = args[0]; - gameLog.displayName = args[1]; - break; - - default: - break; - } - - return gameLog; -} - class GameLogService { - async poll() { - var rawGameLogs = await LogWatcher.Get(); - var gameLogs = []; - var now = Date.now(); + parseRawGameLog(dt, type, args) { + var gameLog = { + dt, + type + }; - for (var [fileName, dt, type, ...args] of rawGameLogs) { - var context = contextMap.get(fileName); - if (typeof context === 'undefined') { - context = { - updatedAt: null, + switch (type) { + case 'location': + gameLog.location = args[0]; + gameLog.worldName = args[1]; + break; - // location - location: null - }; - contextMap.set(fileName, context); - } + case 'location-destination': + gameLog.location = args[0]; + break; - var gameLog = parseRawGameLog(dt, type, args); + case 'player-joined': + gameLog.userDisplayName = args[0]; + gameLog.userType = args[1]; + break; - switch (gameLog.type) { - case 'location': - context.location = gameLog.location; - break; + case 'player-left': + gameLog.userDisplayName = args[0]; + break; - default: - break; - } + case 'notification': + gameLog.json = args[0]; + break; - context.updatedAt = now; + case 'portal-spawn': + gameLog.userDisplayName = args[0]; + break; - gameLogs.push(gameLog); + case 'event': + gameLog.event = args[0]; + break; + + case 'video-play': + gameLog.videoUrl = args[0]; + gameLog.displayName = args[1]; + break; + + case 'vrcx': + gameLog.data = args[0]; + break; + + default: + break; } + return gameLog; + } + + async getAll() { + var gameLogs = []; + var done = false; + while (!done) { + var rawGameLogs = await LogWatcher.Get(); + // eslint-disable-next-line no-unused-vars + for (var [fileName, dt, type, ...args] of rawGameLogs) { + var gameLog = this.parseRawGameLog(dt, type, args); + gameLogs.push(gameLog); + } + if (rawGameLogs.length === 0) { + done = true; + } + } return gameLogs; } + async setDateTill(dateTill) { + await LogWatcher.SetDateTill(dateTill); + } + async reset() { await LogWatcher.Reset(); - contextMap.clear(); } } diff --git a/html/src/vr.js b/html/src/vr.js index 84684d3e..2bcb9763 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -8,11 +8,10 @@ import Noty from 'noty'; import Vue from 'vue'; import ElementUI from 'element-ui'; import locale from 'element-ui/lib/locale/lang/en'; +import MarqueeText from 'vue-marquee-text-component'; +Vue.component('marquee-text', MarqueeText); -import {appVersion} from './constants.js'; -import sharedRepository from './repository/shared.js'; import configRepository from './repository/config.js'; -import webApiService from './service/webapi.js'; speechSynthesis.getVoices(); @@ -109,273 +108,113 @@ speechSynthesis.getVoices(); }; Vue.filter('timeToText', timeToText); - // - // API - // - - var API = {}; - - API.eventHandlers = new Map(); - - API.$emit = function (name, ...args) { - // console.log(name, ...args); - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { - return; - } - try { - for (var handler of handlers) { - handler.apply(this, args); + Vue.component('location', { + template: + '{{ text }}', + props: { + location: String, + hint: { + type: String, + default: '' } - } catch (err) { - console.error(err); - } - }; - - API.$on = function (name, handler) { - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { - handlers = []; - this.eventHandlers.set(name, handlers); - } - handlers.push(handler); - }; - - API.$off = function (name, handler) { - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { - return; - } - var {length} = handlers; - for (var i = 0; i < length; ++i) { - if (handlers[i] === handler) { - if (length > 1) { - handlers.splice(i, 1); - } else { - this.eventHandlers.delete(name); - } - break; - } - } - }; - - API.pendingGetRequests = new Map(); - - API.call = function (endpoint, options) { - var init = { - url: `https://api.vrchat.cloud/api/1/${endpoint}`, - method: 'GET', - ...options - }; - var {params} = init; - var isGetRequest = init.method === 'GET'; - if (isGetRequest === true) { - // transform body to url - if (params === Object(params)) { - var url = new URL(init.url); - var {searchParams} = url; - for (var key in params) { - searchParams.set(key, params[key]); - } - init.url = url.toString(); - } - // merge requests - var req = this.pendingGetRequests.get(init.url); - if (typeof req !== 'undefined') { - return req; - } - } else { - init.headers = { - 'Content-Type': 'application/json;charset=utf-8', - ...init.headers + }, + data() { + return { + text: this.location, + region: this.region }; - init.body = - params === Object(params) ? JSON.stringify(params) : '{}'; - } - init.headers = { - 'User-Agent': appVersion, - ...init.headers - }; - var req = webApiService - .execute(init) - .catch((err) => { - this.$throw(0, err); - }) - .then((response) => { - try { - response.data = JSON.parse(response.data); - return response; - } catch (e) {} - if (response.status === 200) { - this.$throw(0, 'Invalid JSON response'); - } - this.$throw(response.status); - return {}; - }) - .then(({data, status}) => { - if (data === Object(data)) { - if (status === 200) { - if (data.success === Object(data.success)) { - new Noty({ - type: 'success', - text: escapeTag(data.success.message) - }).show(); - } - return data; + }, + methods: { + parse() { + this.text = this.location; + var L = $app.parseLocation(this.location); + if (L.isOffline) { + this.text = 'Offline'; + } else if (L.isPrivate) { + this.text = 'Private'; + } else if (typeof this.hint === 'string' && this.hint !== '') { + if (L.instanceId) { + this.text = `${this.hint} #${L.instanceName} ${L.accessType}`; + } else { + this.text = this.hint; } - if (data.error === Object(data.error)) { - this.$throw( - data.error.status_code || status, - data.error.message, - data.error.data - ); - } else if (typeof data.error === 'string') { - this.$throw(data.status_code || status, data.error); + } else if (L.worldId) { + if (L.instanceId) { + this.text = ` #${L.instanceName} ${L.accessType}`; + } else { + this.text = this.location; + } + } + this.region = ''; + if ( + this.location !== '' && + L.instanceId && + !L.isOffline && + !L.isPrivate + ) { + if (L.region === 'eu') { + this.region = 'europeanunion'; + } else if (L.region === 'jp') { + this.region = 'jp'; + } else { + this.region = 'us'; } } - this.$throw(status, data); - return data; - }); - if (isGetRequest === true) { - req.finally(() => { - this.pendingGetRequests.delete(init.url); - }); - this.pendingGetRequests.set(init.url, req); - } - return req; - }; - - API.statusCodes = { - 100: 'Continue', - 101: 'Switching Protocols', - 102: 'Processing', - 103: 'Early Hints', - 200: 'OK', - 201: 'Created', - 202: 'Accepted', - 203: 'Non-Authoritative Information', - 204: 'No Content', - 205: 'Reset Content', - 206: 'Partial Content', - 207: 'Multi-Status', - 208: 'Already Reported', - 226: 'IM Used', - 300: 'Multiple Choices', - 301: 'Moved Permanently', - 302: 'Found', - 303: 'See Other', - 304: 'Not Modified', - 305: 'Use Proxy', - 306: 'Switch Proxy', - 307: 'Temporary Redirect', - 308: 'Permanent Redirect', - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 407: 'Proxy Authentication Required', - 408: 'Request Timeout', - 409: 'Conflict', - 410: 'Gone', - 411: 'Length Required', - 412: 'Precondition Failed', - 413: 'Payload Too Large', - 414: 'URI Too Long', - 415: 'Unsupported Media Type', - 416: 'Range Not Satisfiable', - 417: 'Expectation Failed', - 418: "I'm a teapot", - 421: 'Misdirected Request', - 422: 'Unprocessable Entity', - 423: 'Locked', - 424: 'Failed Dependency', - 425: 'Too Early', - 426: 'Upgrade Required', - 428: 'Precondition Required', - 429: 'Too Many Requests', - 431: 'Request Header Fields Too Large', - 451: 'Unavailable For Legal Reasons', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', - 505: 'HTTP Version Not Supported', - 506: 'Variant Also Negotiates', - 507: 'Insufficient Storage', - 508: 'Loop Detected', - 510: 'Not Extended', - 511: 'Network Authentication Required', - // CloudFlare Error - 520: 'Web server returns an unknown error', - 521: 'Web server is down', - 522: 'Connection timed out', - 523: 'Origin is unreachable', - 524: 'A timeout occurred', - 525: 'SSL handshake failed', - 526: 'Invalid SSL certificate', - 527: 'Railgun Listener to origin error' - }; - - API.$throw = function (code, error) { - var text = []; - if (code > 0) { - var status = this.statusCodes[code]; - if (typeof status === 'undefined') { - text.push(`${code}`); - } else { - text.push(`${code} ${status}`); } + }, + watch: { + location() { + this.parse(); + } + }, + created() { + this.parse(); } - if (typeof error !== 'undefined') { - text.push(JSON.stringify(error)); - } - text = text.map((s) => escapeTag(s)).join('
'); - if (text.length) { - new Noty({ - type: 'error', - text - }).show(); - } - throw new Error(text); - }; - - // API: Config - - API.cachedConfig = {}; - - API.$on('CONFIG', function (args) { - args.ref = this.applyConfig(args.json); }); - API.applyConfig = function (json) { - var ref = { - clientApiKey: '', - ...json - }; - this.cachedConfig = ref; - return ref; + var $app = { + data: { + // 1 = 대시보드랑 손목에 보이는거 + // 2 = 항상 화면에 보이는 거 + appType: location.href.substr(-1), + currentTime: new Date().toJSON(), + cpuUsage: 0, + config: {}, + nowPlaying: { + url: '', + name: '', + length: 0, + startTime: 0, + elapsed: 0, + percentage: 0, + remainingText: '' + }, + lastLocation: { + date: 0, + location: '', + name: '', + playerList: [], + friendList: [] + }, + lastLocationTimer: '', + wristFeed: [], + devices: [] + }, + computed: {}, + methods: {}, + watch: {}, + el: '#x-app', + mounted() { + setTimeout(function () { + AppApi.ExecuteAppFunction('vrInit', ''); + }, 1000); + if (this.appType === '1') { + this.updateStatsLoop(); + } + } }; - API.getConfig = function () { - return this.call('config', { - method: 'GET' - }).then((json) => { - var args = { - ref: null, - json - }; - this.$emit('CONFIG', args); - return args; - }); - }; - - // API: Location - - API.parseLocation = function (tag) { + $app.methods.parseLocation = function (tag) { var _tag = String(tag || ''); var ctx = { tag: _tag, @@ -448,380 +287,36 @@ speechSynthesis.getVoices(); return ctx; }; - Vue.component('location', { - template: - '{{ text }}', - props: { - location: String, - hint: { - type: String, - default: '' - } - }, - data() { - return { - text: this.location, - region: this.region - }; - }, - methods: { - parse() { - this.text = this.location; - var L = API.parseLocation(this.location); - if (L.isOffline) { - this.text = 'Offline'; - } else if (L.isPrivate) { - this.text = 'Private'; - } else if (typeof this.hint === 'string' && this.hint !== '') { - if (L.instanceId) { - this.text = `${this.hint} #${L.instanceName} ${L.accessType}`; - } else { - this.text = this.hint; - } - } else if (L.worldId) { - if (L.instanceId) { - this.text = ` #${L.instanceName} ${L.accessType}`; - } else { - this.text = this.location; - } - } - this.region = ''; - if ( - this.location !== '' && - L.instanceId && - !L.isOffline && - !L.isPrivate - ) { - if (L.region === 'eu') { - this.region = 'europeanunion'; - } else if (L.region === 'jp') { - this.region = 'jp'; - } else { - this.region = 'us'; - } - } - } - }, - watch: { - location() { - this.parse(); - } - }, - created() { - this.parse(); - } - }); - - // API: World - - API.cachedWorlds = new Map(); - - API.$on('WORLD', function (args) { - args.ref = this.applyWorld(args.json); - }); - - API.applyWorld = function (json) { - var ref = this.cachedWorlds.get(json.id); - if (typeof ref === 'undefined') { - ref = { - id: '', - name: '', - description: '', - authorId: '', - authorName: '', - capacity: 0, - tags: [], - releaseStatus: '', - imageUrl: '', - thumbnailImageUrl: '', - assetUrl: '', - assetUrlObject: {}, - pluginUrl: '', - pluginUrlObject: {}, - unityPackageUrl: '', - unityPackageUrlObject: {}, - unityPackages: [], - version: 0, - favorites: 0, - created_at: '', - updated_at: '', - publicationDate: '', - labsPublicationDate: '', - visits: 0, - popularity: 0, - heat: 0, - publicOccupants: 0, - privateOccupants: 0, - occupants: 0, - instances: [], - // VRCX - $isLabs: false, - // - ...json - }; - this.cachedWorlds.set(ref.id, ref); - } else { - Object.assign(ref, json); - } - ref.$isLabs = ref.tags.includes('system_labs'); - return ref; + $app.methods.configUpdate = function (json) { + this.config = JSON.parse(json); }; - /* - params: { - worldId: string - } - */ - API.getWorld = function (params) { - return this.call(`worlds/${params.worldId}`, { - method: 'GET' - }).then((json) => { - var args = { - ref: null, - json, - params - }; - this.$emit('WORLD', args); - return args; - }); + $app.methods.nowPlayingUpdate = function (json) { + this.nowPlaying = JSON.parse(json); }; - // API: User - - API.cachedUsers = new Map(); - - API.$on('USER', function (args) { - args.ref = this.applyUser(args.json); - }); - - API.applyUser = function (json) { - var ref = this.cachedUsers.get(json.id); - if (typeof ref === 'undefined') { - ref = { - id: '', - username: '', - displayName: '', - userIcon: '', - bio: '', - bioLinks: [], - currentAvatarImageUrl: '', - currentAvatarThumbnailImageUrl: '', - status: '', - statusDescription: '', - state: '', - tags: [], - developerType: '', - last_login: '', - last_platform: '', - allowAvatarCopying: false, - isFriend: false, - location: '', - worldId: '', - instanceId: '', - // VRCX - ...json - }; - this.cachedUsers.set(ref.id, ref); - } else { - var props = {}; - for (var prop in ref) { - if (ref[prop] !== Object(ref[prop])) { - props[prop] = true; - } - } - var $ref = {...ref}; - Object.assign(ref, json); - for (var prop in ref) { - if (ref[prop] !== Object(ref[prop])) { - props[prop] = true; - } - } - for (var prop in props) { - var asis = $ref[prop]; - var tobe = ref[prop]; - if (asis === tobe) { - delete props[prop]; - } else { - props[prop] = [tobe, asis]; - } - } - } - return ref; + $app.methods.lastLocationUpdate = function (json) { + this.lastLocation = JSON.parse(json); }; - /* - params: { - userId: string - } - */ - API.getUser = function (params) { - return this.call(`users/${params.userId}`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('USER', args); - return args; - }); + $app.methods.wristFeedUpdate = function (json) { + this.wristFeed = JSON.parse(json); }; - /* - params: { - userId: string - } - */ - API.getCachedUser = function (params) { - return new Promise((resolve, reject) => { - var ref = this.cachedUsers.get(params.userId); - if (typeof ref === 'undefined') { - this.getUser(params).catch(reject).then(resolve); - } else { - resolve({ - cache: true, - json: ref, - params, - ref - }); - } - }); - }; + $app.methods.updateStatsLoop = async function () { + try { + this.currentTime = new Date().toJSON(); + var cpuUsage = await AppApi.CpuUsage(); + this.cpuUsage = cpuUsage.toFixed(0); - var $app = { - data: { - API, - // 1 = 대시보드랑 손목에 보이는거 - // 2 = 항상 화면에 보이는 거 - appType: location.href.substr(-1), - currentTime: new Date().toJSON(), - currentUserStatus: null, - cpuUsage: 0, - config: {}, - isGameRunning: false, - isGameNoVR: false, - downloadProgress: 0, - lastLocation: { - date: 0, - location: '', - name: '', - playerList: [], - friendList: [] - }, - lastLocationTimer: '', - wristFeedLastEntry: '', - notyFeedLastEntry: '', - wristFeed: [], - notyMap: [], - devices: [] - }, - computed: {}, - methods: {}, - watch: {}, - el: '#x-app', - mounted() { - // https://media.discordapp.net/attachments/581757976625283083/611170278218924033/unknown.png - // 현재 날짜 시간 - // 컨트롤러 배터리 상황 - // -- - // OO is in Let's Just H!!!!! [GPS] - // OO has logged in [Online] -> TODO: location - // OO has logged out [Offline] -> TODO: location - // OO has joined [OnPlayerJoined] - // OO has left [OnPlayerLeft] - // [Moderation] - // OO has blocked you - // OO has muted you - // OO has hidden you - // -- - API.getConfig() - .catch((err) => { - // FIXME: 어케 복구하냐 이건 - throw err; - }) - .then((args) => { - if (this.appType === '1') { - this.updateCpuUsageLoop(); - } - this.initLoop(); - return args; - }); - } - }; - - $app.methods.updateVRConfigVars = function () { - this.currentUserStatus = sharedRepository.getString( - 'current_user_status' - ); - this.isGameRunning = sharedRepository.getBool('is_game_running'); - this.isGameNoVR = sharedRepository.getBool('is_Game_No_VR'); - this.downloadProgress = sharedRepository.getInt('downloadProgress'); - var lastLocation = sharedRepository.getObject('last_location'); - if (lastLocation) { - this.lastLocation = lastLocation; + this.lastLocationTimer = ''; if (this.lastLocation.date !== 0) { this.lastLocationTimer = timeToText( Date.now() - this.lastLocation.date ); - } else { - this.lastLocationTimer = ''; } - } - var newConfig = sharedRepository.getObject('VRConfigVars'); - if (newConfig) { - if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) { - this.config = newConfig; - this.notyFeedLastEntry = ''; - this.wristFeedLastEntry = ''; - if (this.appType === '2') { - this.initNotyMap(); - } - } - } else { - throw 'config not set'; - } - }; - $app.methods.initNotyMap = function () { - var notyFeed = sharedRepository.getArray('notyFeed'); - if (notyFeed === null) { - return; - } - notyFeed.forEach((feed) => { - var displayName = ''; - if (feed.displayName) { - displayName = feed.displayName; - } else if (feed.senderUsername) { - displayName = feed.senderUsername; - } else if (feed.sourceDisplayName) { - displayName = feed.sourceDisplayName; - } else if (feed.data) { - displayName = feed.data; - } else { - console.error('missing displayName'); - } - if ( - (displayName && !this.notyMap[displayName]) || - this.notyMap[displayName] < feed.created_at - ) { - this.notyMap[displayName] = feed.created_at; - } - }); - }; - - $app.methods.initLoop = function () { - if (!sharedRepository.getBool('VRInit')) { - setTimeout(this.initLoop, 500); - } else { - this.updateLoop(); - } - }; - - $app.methods.updateLoop = async function () { - try { - this.currentTime = new Date().toJSON(); - await this.updateVRConfigVars(); - if (!this.config.hideDevicesFromFeed && this.appType === '1') { + if (!this.config.hideDevicesFromFeed) { AppApi.GetVRDevices().then((devices) => { devices.forEach((device) => { device[2] = parseInt(device[2], 10); @@ -831,183 +326,120 @@ speechSynthesis.getVoices(); } else { this.devices = ''; } - await this.updateSharedFeeds(); } catch (err) { console.error(err); } - setTimeout(() => this.updateLoop(), 500); + setTimeout(() => this.updateStatsLoop(), 500); }; - $app.methods.updateCpuUsageLoop = async function () { - try { - var cpuUsage = await AppApi.CpuUsage(); - this.cpuUsage = cpuUsage.toFixed(0); - } catch (err) { - console.error(err); + $app.methods.playNoty = function (json) { + var {noty, message, image} = JSON.parse(json); + var text = ''; + var img = ''; + if (image) { + img = ``; } - setTimeout(() => this.updateCpuUsageLoop(), 1000); - }; - - $app.methods.updateSharedFeeds = function () { - if (this.appType === '1') { - this.wristFeed = sharedRepository.getArray('wristFeed'); - } - if (this.appType === '2') { - var notyFeed = sharedRepository.getArray('notyFeed'); - this.updateSharedFeedNoty(notyFeed); - } - }; - - $app.methods.updateSharedFeedNoty = function (notyFeed) { - var notyToPlay = []; - notyFeed.forEach((feed) => { - var displayName = ''; - if (feed.displayName) { - displayName = feed.displayName; - } else if (feed.senderUsername) { - displayName = feed.senderUsername; - } else if (feed.sourceDisplayName) { - displayName = feed.sourceDisplayName; - } else if (feed.data) { - displayName = feed.data; - } else { - console.error('missing displayName'); - } - if ( - (displayName && !this.notyMap[displayName]) || - this.notyMap[displayName] < feed.created_at - ) { - this.notyMap[displayName] = feed.created_at; - notyToPlay.push(feed); - } - }); - // disable notifications when busy - if (this.currentUserStatus === 'busy') { - return; - } - var bias = new Date(Date.now() - 60000).toJSON(); - var noty = {}; - var messageList = [ - 'inviteMessage', - 'requestMessage', - 'responseMessage' - ]; - for (var i = 0; i < notyToPlay.length; i++) { - noty = notyToPlay[i]; - if (noty.created_at < bias) { - continue; - } - var message = ''; - for (var k = 0; k < messageList.length; k++) { - if ( - typeof noty.details !== 'undefined' && - typeof noty.details[messageList[k]] !== 'undefined' - ) { - message = noty.details[messageList[k]]; + switch (noty.type) { + case 'OnPlayerJoined': + text = `${noty.displayName} has joined`; + break; + case 'OnPlayerLeft': + text = `${noty.displayName} has left`; + break; + case 'OnPlayerJoining': + text = `${noty.displayName} is joining`; + break; + case 'GPS': + text = `${ + noty.displayName + } is in ${this.displayLocation( + noty.location, + noty.worldName + )}`; + break; + case 'Online': + text = `${noty.displayName} has logged in`; + break; + case 'Offline': + text = `${noty.displayName} has logged out`; + break; + case 'Status': + text = `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`; + break; + case 'invite': + text = `${ + noty.senderUsername + } has invited you to ${this.displayLocation( + noty.details.worldId, + noty.details.worldName + )}${message}`; + break; + case 'requestInvite': + text = `${noty.senderUsername} has requested an invite ${message}`; + break; + case 'inviteResponse': + text = `${noty.senderUsername} has responded to your invite ${message}`; + break; + case 'requestInviteResponse': + text = `${noty.senderUsername} has responded to your invite request ${message}`; + break; + case 'friendRequest': + text = `${noty.senderUsername} has sent you a friend request`; + break; + case 'Friend': + text = `${noty.displayName} is now your friend`; + break; + case 'Unfriend': + text = `${noty.displayName} is no longer your friend`; + break; + case 'TrustLevel': + text = `${noty.displayName} trust level is now ${noty.trustLevel}`; + break; + case 'DisplayName': + text = `${noty.previousDisplayName} changed their name to ${noty.displayName}`; + break; + case 'PortalSpawn': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.instanceId, + noty.worldName + )}`; } - } - if (message) { - message = `, ${message}`; - } - if ( - this.config.overlayNotifications && - !this.isGameNoVR && - this.isGameRunning - ) { - var text = ''; - switch (noty.type) { - case 'OnPlayerJoined': - text = `${noty.data} has joined`; - break; - case 'OnPlayerLeft': - text = `${noty.data} has left`; - break; - case 'OnPlayerJoining': - text = `${noty.displayName} is joining`; - break; - case 'GPS': - text = `${ - noty.displayName - } is in ${this.displayLocation( - noty.location, - noty.worldName - )}`; - break; - case 'Online': - text = `${noty.displayName} has logged in`; - break; - case 'Offline': - text = `${noty.displayName} has logged out`; - break; - case 'Status': - text = `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`; - break; - case 'invite': - text = `${ - noty.senderUsername - } has invited you to ${this.displayLocation( - noty.details.worldId, - noty.details.worldName - )}${message}`; - break; - case 'requestInvite': - text = `${noty.senderUsername} has requested an invite ${message}`; - break; - case 'inviteResponse': - text = `${noty.senderUsername} has responded to your invite ${message}`; - break; - case 'requestInviteResponse': - text = `${noty.senderUsername} has responded to your invite request ${message}`; - break; - case 'friendRequest': - text = `${noty.senderUsername} has sent you a friend request`; - break; - case 'Friend': - text = `${noty.displayName} is now your friend`; - break; - case 'Unfriend': - text = `${noty.displayName} is no longer your friend`; - break; - case 'TrustLevel': - text = `${noty.displayName} trust level is now ${noty.trustLevel}`; - break; - case 'DisplayName': - text = `${noty.previousDisplayName} changed their name to ${noty.displayName}`; - break; - case 'PortalSpawn': - text = `${noty.data} has spawned a portal`; - break; - case 'Event': - text = noty.data; - break; - case 'VideoPlay': - text = `Now playing: ${noty.data}`; - break; - case 'BlockedOnPlayerJoined': - text = `Blocked user ${noty.displayName} has joined`; - break; - case 'BlockedOnPlayerLeft': - text = `Blocked user ${noty.displayName} has left`; - break; - case 'MutedOnPlayerJoined': - text = `Muted user ${noty.displayName} has joined`; - break; - case 'MutedOnPlayerLeft': - text = `Muted user ${noty.displayName} has left`; - break; - default: - break; - } - if (text) { - new Noty({ - type: 'alert', - theme: this.config.notificationTheme, - timeout: this.config.notificationTimeout, - layout: this.config.notificationPosition, - text - }).show(); - } - } + text = `${noty.displayName} has spawned a portal${locationName}`; + break; + case 'AvatarChange': + text = `${noty.displayName} changed into avatar ${noty.name}`; + break; + case 'Event': + text = noty.data; + break; + case 'VideoPlay': + text = `Now playing: ${noty.notyName}`; + break; + case 'BlockedOnPlayerJoined': + text = `Blocked user ${noty.displayName} has joined`; + break; + case 'BlockedOnPlayerLeft': + text = `Blocked user ${noty.displayName} has left`; + break; + case 'MutedOnPlayerJoined': + text = `Muted user ${noty.displayName} has joined`; + break; + case 'MutedOnPlayerLeft': + text = `Muted user ${noty.displayName} has left`; + break; + default: + break; + } + if (text) { + new Noty({ + type: 'alert', + theme: this.config.notificationTheme, + timeout: this.config.notificationTimeout, + layout: this.config.notificationPosition, + text: `${img}
${text}
` + }).show(); } }; @@ -1033,7 +465,7 @@ speechSynthesis.getVoices(); $app.methods.displayLocation = function (location, worldName) { var text = ''; - var L = API.parseLocation(location); + var L = this.parseLocation(location); if (L.isOffline) { text = 'Offline'; } else if (L.isPrivate) { diff --git a/html/src/vr.pug b/html/src/vr.pug index 516220b2..ee69bd60 100644 --- a/html/src/vr.pug +++ b/html/src/vr.pug @@ -36,17 +36,23 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.displayName")] #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} + | #[span.name(v-text="feed.displayName")] + template(v-if="feed.statusDescription === feed.previousStatusDescription") + i.x-user-status(:class="statusClass(feed.previousStatus)") + i.el-icon-right + i.x-user-status(:class="statusClass(feed.status)") + template(v-else) + | #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} div(v-else-if="feed.type === 'OnPlayerJoined'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | ▶️ #[span.name(v-text="feed.data")] + | ▶️ #[span.name(v-text="feed.displayName")] div(v-else-if="feed.type === 'OnPlayerLeft'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | ◀️ #[span.name(v-text="feed.data")] + | ◀️ #[span.name(v-text="feed.displayName")] div(v-else-if="feed.type === 'OnPlayerJoining'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -57,7 +63,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - location(:location="feed.data[0]" :hint="feed.data[1]") + location(:location="feed.location" :hint="feed.worldName") + div(v-else-if="feed.type === 'VideoPlay'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | 🎵 #[span.name(v-text="feed.displayName")] + template(v-if="feed.videoName") + | #[span(v-text="feed.videoName")] + template(v-else) + | #[span(v-text="feed.videoUrl")] div(v-else-if="feed.type === 'invite'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -107,7 +122,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | ✨ #[span.name(v-text="feed.data")] + | ✨ #[span.name(v-text="feed.displayName")] + template(v-if="feed.worldName") + | #[location(:location="feed.instanceId" :hint="feed.worldName")] + div(v-else-if="feed.type === 'AvatarChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | 🧍 #[span.name(v-text="feed.displayName")] {{ feed.name }} + template(v-if="feed.description && feed.description !== feed.name") + | - {{ feed.description }} div(v-else-if="feed.type === 'Event'" class="x-friend-item") .detail span.extra @@ -159,17 +183,23 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.displayName")] is #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} + | #[span.name(v-text="feed.displayName")] + template(v-if="feed.statusDescription === feed.previousStatusDescription") + i.x-user-status(:class="statusClass(feed.previousStatus)") + i.el-icon-right + i.x-user-status(:class="statusClass(feed.status)") + template(v-else) + | #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}} div(v-else-if="feed.type === 'OnPlayerJoined'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.data")] has joined + | #[span.name(v-text="feed.displayName")] has joined div(v-else-if="feed.type === 'OnPlayerLeft'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.data")] has left + | #[span.name(v-text="feed.displayName")] has left div(v-else-if="feed.type === 'OnPlayerJoining'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -179,7 +209,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - location(:location="feed.data[0]" :hint="feed.data[1]") + location(:location="feed.location" :hint="feed.worldName") + div(v-else-if="feed.type === 'VideoPlay'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | #[span.name(v-text="feed.displayName")] changed video to + template(v-if="feed.videoName") + | #[span(v-text="feed.videoName")] + template(v-else) + | #[span(v-text="feed.videoUrl")] div(v-else-if="feed.type === 'invite'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") .detail span.extra @@ -229,7 +268,16 @@ html .detail span.extra span.time {{ feed.created_at | formatDate('HH:MI') }} - | #[span.name(v-text="feed.data")] has spawned a portal + | #[span.name(v-text="feed.displayName")] has spawned a portal + template(v-if="feed.worldName") + | to #[location(:location="feed.instanceId" :hint="feed.worldName")] + div(v-else-if="feed.type === 'AvatarChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }") + .detail + span.extra + span.time {{ feed.created_at | formatDate('HH:MI') }} + | #[span.name(v-text="feed.displayName")] changed into avatar {{ feed.name }} + template(v-if="feed.description && feed.description !== feed.name") + | - {{ feed.description }} div(v-else-if="feed.type === 'Event'" class="x-friend-item") .detail span.extra @@ -301,21 +349,25 @@ html br span {{ device[2] }}% .x-containerbottom + template(v-if="nowPlaying.playing") + span(style="float:right;padding-left:10px") {{ nowPlaying.remainingText }} + marquee-text {{ nowPlaying.name }} ‎ + div.np-progress-bar(:style="{ width: nowPlaying.percentage + '%' }") template(v-if="config && config.minimalFeed") - template(v-if="downloadProgress === 100") + template(v-if="config.downloadProgress === 100") span(style="display:inline-block;margin-right:5px") #[i.el-icon-loading] - template(v-else-if="downloadProgress > 0") - span(style="display:inline-block;margin-right:5px") {{ downloadProgress }}% - template(v-if="lastLocation.date != 0") + template(v-else-if="config.downloadProgress > 0") + span(style="display:inline-block;margin-right:5px") {{ config.downloadProgress }}% + template(v-if="lastLocation.date !== 0") span(style="float:right") {{ lastLocationTimer }} span(style="display:inline-block") {{ lastLocation.playerList.length }} span(style="display:inline-block;font-weight:bold") {{ lastLocation.friendList.length !== 0 ? `‎‎‎‎‎‎‎‎‏‏‎ ‎(${lastLocation.friendList.length})` : ''}} template(v-else) - template(v-if="downloadProgress === 100") + template(v-if="config.downloadProgress === 100") span(style="display:inline-block;margin-right:5px") Downloading: #[i.el-icon-loading] - template(v-else-if="downloadProgress > 0") - span(style="display:inline-block;margin-right:5px") Downloading: {{ downloadProgress }}% - template(v-if="lastLocation.date != 0") + template(v-else-if="config.downloadProgress > 0") + span(style="display:inline-block;margin-right:5px") Downloading: {{ config.downloadProgress }}% + template(v-if="lastLocation.date !== 0") span(style="float:right") Timer: {{ lastLocationTimer }} span(style="display:inline-block") Players: {{ lastLocation.playerList.length }} span(style="display:inline-block;font-weight:bold") {{ lastLocation.friendList.length !== 0 ? `‎‎‎‎‎‎‎‎‏‏‎ ‎(${lastLocation.friendList.length})` : ''}} diff --git a/html/src/vr.scss b/html/src/vr.scss index ad8fd3b2..8ebd7eeb 100644 --- a/html/src/vr.scss +++ b/html/src/vr.scss @@ -22,9 +22,6 @@ .noty_body { display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .noty_layout { @@ -34,6 +31,7 @@ .noty_theme__relax.noty_bar, .noty_theme__sunset.noty_bar { + height: 42px; position: relative; margin: 4px 0; overflow: hidden; @@ -42,9 +40,7 @@ .noty_theme__relax.noty_bar .noty_body, .noty_theme__sunset.noty_bar .noty_body { - padding: 5px 10px 10px; font-size: 15px; - text-align: center; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); } @@ -143,6 +139,19 @@ opacity: 0.6; } +.noty-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 8px 8px 0 11px; +} + +.noty-img { + height: 42px; + float: left; + border-radius: 4px; +} + ::-webkit-scrollbar { width: 8px; height: 8px; @@ -202,16 +211,22 @@ button { } .x-containerbottom span { - padding: 0px; display: block; overflow: hidden; } +.np-progress-bar { + width: 0%; + height: 2px; + background-color: white; +} + .x-friend-item { box-sizing: border-box; display: flex; align-items: center; font-size: 18px; + height: 27.1px; } .x-friend-item .time {