diff --git a/SteamToolsV2+.sln b/SteamToolsV2+.sln index 7f72768c5..0c90065d8 100644 --- a/SteamToolsV2+.sln +++ b/SteamToolsV2+.sln @@ -79,6 +79,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Steam4NET", "references\Ste EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Titanium.Web.Proxy", "references\Titanium-Web-Proxy\src\Titanium.Web.Proxy\Titanium.Web.Proxy.csproj", "{35D60129-6652-4B6E-A04A-9766C3CF1869}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Application.SteamTools.Tools.OpenSourceLibraryList", "System.Application.SteamTools.Tools.OpenSourceLibraryList\System.Application.SteamTools.Tools.OpenSourceLibraryList.csproj", "{CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -283,6 +285,14 @@ Global {35D60129-6652-4B6E-A04A-9766C3CF1869}.Release|Any CPU.Build.0 = Release|Any CPU {35D60129-6652-4B6E-A04A-9766C3CF1869}.Release|x86.ActiveCfg = Release|Any CPU {35D60129-6652-4B6E-A04A-9766C3CF1869}.Release|x86.Build.0 = Release|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Debug|x86.Build.0 = Debug|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Release|Any CPU.Build.0 = Release|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Release|x86.ActiveCfg = Release|Any CPU + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -320,6 +330,7 @@ Global {6DD9644F-F005-4C19-8ADF-70C303EC5BB8} = {8EB91F53-1062-4E9B-8E4A-096F35EA688D} {3C05CED6-7712-40F0-AF17-8DD3F4C0A224} = {8EB91F53-1062-4E9B-8E4A-096F35EA688D} {35D60129-6652-4B6E-A04A-9766C3CF1869} = {8EB91F53-1062-4E9B-8E4A-096F35EA688D} + {CF7C2B80-D393-45EA-A67D-DF46FF17B0F8} = {CC6FF77A-6E6B-4D42-B12B-0177745E9D22} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3833E7C5-69BC-4CB3-92CE-A8854AA35D8A} diff --git a/System.Application.SteamTools.Client.Desktop.Windows/MouseHook.cs b/System.Application.SteamTools.Client.Desktop.Windows/MouseHook.cs new file mode 100644 index 000000000..8eca72ef6 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/MouseHook.cs @@ -0,0 +1,167 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Windows; + +namespace System +{ + internal static class MouseHook + { + delegate int HookProc(int nCode, int wParam, IntPtr lParam); + + static int _mouseHookHandle; + static HookProc? _mouseDelegate; + + static event MouseUpEventHandler? MouseUp; + + public static event MouseUpEventHandler OnMouseUp + { + add + { + Subscribe(); + MouseUp += value; + } + remove + { + MouseUp -= value; + Unsubscribe(); + } + } + + static void Unsubscribe() + { + if (_mouseHookHandle != 0) + { + int result = UnhookWindowsHookEx(_mouseHookHandle); + _mouseHookHandle = 0; + _mouseDelegate = null; + if (result == 0) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new Win32Exception(errorCode); + } + } + } + + static void Subscribe() + { + if (_mouseHookHandle == 0) + { + var currentProcessMainModuleName = Process.GetCurrentProcess().MainModule?.ModuleName; + if (string.IsNullOrWhiteSpace(currentProcessMainModuleName)) throw new ArgumentNullException(nameof(currentProcessMainModuleName)); + _mouseDelegate = MouseHookProc; + _mouseHookHandle = SetWindowsHookEx(WH_MOUSE_LL, + _mouseDelegate, + GetModuleHandle(currentProcessMainModuleName), + 0); + if (_mouseHookHandle == 0) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new Win32Exception(errorCode); + } + } + } + + static int MouseHookProc(int nCode, int wParam, IntPtr lParam) + { + if (nCode >= 0) + { + var mouseHookStructObj = Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT)); + if (mouseHookStructObj == null) throw new ArgumentNullException(nameof(mouseHookStructObj)); + var mouseHookStruct = (MSLLHOOKSTRUCT)mouseHookStructObj; + if (wParam == WM_LBUTTONUP) + { + if (MouseUp != null) + { + MouseUp.Invoke(null, new Point(mouseHookStruct.pt.X, mouseHookStruct.pt.Y)); + } + } + } + return CallNextHookEx(_mouseHookHandle, nCode, wParam, lParam); + } + + const int WH_MOUSE_LL = 14; + const int WM_LBUTTONUP = 0x0202; + + [StructLayout(LayoutKind.Sequential)] + struct MSLLHOOKSTRUCT + { + public PointInt32 pt; + public uint mouseData; + public uint flags; + public uint time; + public IntPtr dwExtraInfo; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr GetModuleHandle(string name); + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall, SetLastError = true)] + static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId); + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall, SetLastError = true)] + static extern int UnhookWindowsHookEx(int idHook); + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall)] + static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam); + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto)] + public static extern IntPtr LoadCursor(IntPtr hInstance, uint lpCursorName); + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto)] + public static extern bool SetSystemCursor(IntPtr hcur, uint id); + + public const uint OCR_NORMAL = 32512; + public const uint OCR_IBEAM = 32513; + public const uint OCR_CROSS = 32515; + public const uint OCR_HAND = 32649; + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto)] + public static extern bool SystemParametersInfo(uint uiAction, uint uiParam, + IntPtr pvParam, uint fWinIni); + + public enum SystemParametersDesktopInfo : uint + { + SPIF_SENDWININICHANGE = 2, + SPI_SETDESKWALLPAPER = 20, + SPI_SETDESKPATTERN = 21, + SPI_SETWORKAREA = 47, + SPI_GETWORKAREA = 48, + SPI_GETFONTSMOOTHING = 74, + SPI_SETFONTSMOOTHING = 75, + SPI_SETCURSORS = 87, + SPI_GETDESKWALLPAPER = 115, + SPI_GETFLATMENU = 4130, + SPI_SETFLATMENU = 4131, + SPI_GETDROPSHADOW = 4132, + SPI_SETDROPSHADOW = 4133, + SPI_GETCLEARTYPE = 4168, + SPI_SETCLEARTYPE = 4169, + SPI_GETFONTSMOOTHINGTYPE = 8202, + SPI_SETFONTSMOOTHINGTYPE = 8203, + SPI_GETFONTSMOOTHINGCONTRAST = 8204, + SPI_SETFONTSMOOTHINGCONTRAST = 8205, + SPI_GETFONTSMOOTHINGORIENTATION = 8210, + SPI_SETFONTSMOOTHINGORIENTATION = 8211 + } + [Flags] + public enum SystemParamtersInfoFlags : uint + { + None = 0, + SPIF_UPDATEINIFILE = 1, + SPIF_SENDCHANGE = 2, + SPIF_SENDWININICHANGE = 2 + } + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto, EntryPoint = "GetCursorPos")] + public static extern bool GetCursorPos(out PointInt32 pt); + + [DllImport(User32Window.LibraryName, CharSet = CharSet.Auto, EntryPoint = "WindowFromPoint")] + public static extern IntPtr WindowFromPoint(PointInt32 pt); + } + + internal delegate void MouseUpEventHandler(object? sender, Point p); +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/NativeMethods.cs b/System.Application.SteamTools.Client.Desktop.Windows/NativeMethods.cs new file mode 100644 index 000000000..56426c95e --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/NativeMethods.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace System +{ + internal static class NativeMethods + { + [DllImport("Avrt.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr AvSetMmThreadCharacteristics(string taskName, ref uint taskIndex); + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/PointInt32.cs b/System.Application.SteamTools.Client.Desktop.Windows/PointInt32.cs new file mode 100644 index 000000000..153cb029f --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/PointInt32.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace System +{ + [Serializable] + [StructLayout(LayoutKind.Sequential)] + internal struct PointInt32 + { + public int X; + public int Y; + + public PointInt32(int x, int y) + { + X = x; + Y = y; + } + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/RECT.cs b/System.Application.SteamTools.Client.Desktop.Windows/RECT.cs new file mode 100644 index 000000000..e942867f2 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/RECT.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace System +{ + [StructLayout(LayoutKind.Sequential)] + internal class RECT + { + public int left; + public int top; + public int width; + public int height; + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/User32Window.cs b/System.Application.SteamTools.Client.Desktop.Windows/User32Window.cs new file mode 100644 index 000000000..2ac0199b6 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/User32Window.cs @@ -0,0 +1,217 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace System +{ + internal static class User32Window + { + public const string LibraryName = "user32.dll"; + + [DllImport(LibraryName, CharSet = CharSet.Auto, EntryPoint = "FlashWindow")] + static extern void FlashWindow(IntPtr hwnd, bool bInvert); + + [DllImport(LibraryName, CharSet = CharSet.Auto, EntryPoint = "FlashWindowEx")] + static extern void FlashWindowEx(ref FLASHWINFO pwfi); + + [DllImport(LibraryName, CharSet = CharSet.Auto)] + public static extern bool ShowWindow(IntPtr hWnd, short State); + + [DllImport(LibraryName, CharSet = CharSet.Auto)] + internal static extern IntPtr GetOpenClipboardWindow(); + + [DllImport(LibraryName, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern int GetWindowTextLength(IntPtr hWnd); + + public enum Cmd_SHOWWINDOWS : short + { + /// + /// Minimizes a window, even if the thread that owns the window is not responding. This flag should only be used when + /// minimizing windows from a different thread. + /// + SW_FORCEMINIMIZE = 11, + + /// + /// Hides the window and activates another window. + /// + SW_HIDE = 0, + + /// + /// Maximizes the specified window. + /// + SW_MAXIMIZE = 3, + + /// + /// Minimizes the specified window and activates the next top-level window in the Z order. + /// + SW_MINIMIZE = 6, + + /// + /// Activates and displays the window. If the window is minimized or maximized, the system restores it to its original + /// size and position. An application should specify this flag when restoring a minimized window. + /// + SW_RESTORE = 9, + + /// + /// Activates the window and displays it in its current size and position. + /// + SW_SHOW = 5, + + /// + /// Sets the show state based on the SW_ value specified in the STARTUPINFO structure passed to the CreateProcess + /// function by the program that started the application. + /// + SW_SHOWDEFAULT = 10, + + /// + /// Activates the window and displays it as a maximized window. + /// + SW_SHOWMAXIMIZED = 3, + + /// + /// Activates the window and displays it as a minimized window. + /// + SW_SHOWMINIMIZED = 2, + + /// + /// Displays the window as a minimized window. This value is similar to SW_SHOWMINIMIZED, except the window is not + /// activated. + /// + SW_SHOWMINNOACTIVE = 7, + + /// + /// Displays the window in its current size and position. This value is similar to SW_SHOW, except that the window is + /// not activated. + /// + SW_SHOWNA = 8, + + /// + /// Displays a window in its most recent size and position. This value is similar to SW_SHOWNORMAL, except that the + /// window is not activated. + /// + SW_SHOWNOACTIVATE = 4, + + /// + /// Activates and displays a window. If the window is minimized or maximized, the system restores it to its original + /// size and position. An application should specify this flag when displaying the window for the first time. + /// + SW_SHOWNORMAL = 1 + }; + + internal struct FLASHWINFO + { +#pragma warning disable 649 + + /// + /// 该结构的字节大小 + /// + public uint cbSize; + + /// + /// 要闪烁的窗口的句柄,该窗口可以是打开的或最小化的 + /// + public IntPtr hwnd; + + /// + /// 闪烁的状态 + /// + public uint dwFlags; + + /// + /// 闪烁窗口的次数 + /// + public uint uCount; + + /// + /// 窗口闪烁的频度,毫秒为单位;若该值为0,则为默认图标的闪烁频度 + /// + public uint dwTimeout; + +#pragma warning restore 649 + } + + public const uint FLASHW_TRAY = 2; + public const uint FLASHW_TIMERNOFG = 12; + + //获取窗口标题 + [DllImport(LibraryName, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int GetWindowText( + IntPtr hWnd,//窗口句柄 + StringBuilder lpString,//标题 + int nMaxCount //最大值 + ); + + //获取类的名字 + [DllImport(LibraryName, CharSet = CharSet.Unicode)] + public static extern int GetClassName( + IntPtr hWnd,//句柄 + StringBuilder lpString, //类名 + int nMaxCount //最大值 + ); + + //获取句柄所属进程pid + [DllImport(LibraryName, CharSet = CharSet.Auto, EntryPoint = "GetWindowThreadProcessId")] + public static extern int GetWindowThreadProcessId(IntPtr hwnd, out int pid); + + [DllImport(LibraryName, CharSet = CharSet.Unicode)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport(LibraryName, CharSet = CharSet.Unicode)] + public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); + + [DllImport(LibraryName, CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam); + + public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + [DllImport(LibraryName, ExactSpelling = true)] + public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport(LibraryName, ExactSpelling = true)] + public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); + + [DllImport(LibraryName, ExactSpelling = true)] + public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int nWidth, int nHeight, bool bRepaint); + + // This static method is required because Win32 does not support + // GetWindowLongPtr directly + public static IntPtr GetWindowLongPtr(IntPtr hwnd, int nIndex) + { + return IntPtr.Size > 4 + ? GetWindowLongPtr_x64(hwnd, nIndex) + : new IntPtr(GetWindowLong(hwnd, nIndex)); + } + + [DllImport(LibraryName, CharSet = CharSet.Auto)] + static extern int GetWindowLong(IntPtr hwnd, int nIndex); + + [DllImport(LibraryName, CharSet = CharSet.Auto, EntryPoint = "GetWindowLongPtr")] + static extern IntPtr GetWindowLongPtr_x64(IntPtr hwnd, int nIndex); + + // This static method is required because Win32 does not support + // GetWindowLongPtr directly + public static IntPtr SetWindowLongPtr(IntPtr hwnd, int nIndex, IntPtr dwNewLong) + { + return IntPtr.Size > 4 + ? SetWindowLongPtr_x64(hwnd, nIndex, dwNewLong) + : new IntPtr(SetWindowLong(hwnd, nIndex, dwNewLong.ToInt32())); + } + + [DllImport(LibraryName, CharSet = CharSet.Auto)] + static extern int SetWindowLong(IntPtr hwnd, int nIndex, int dwNewLong); + + [DllImport(LibraryName, CharSet = CharSet.Auto, EntryPoint = "SetWindowLongPtr")] + static extern IntPtr SetWindowLongPtr_x64(IntPtr hwnd, int nIndex, IntPtr dwNewLong); + + public static void FlashWindow(IntPtr hwnd) + { + //ShowWindow(hwnd, (short)Cmd_SHOWWINDOWS.SW_NORMAL); + //直接调用FlashWindow即可 + FlashWindow(hwnd, true); + } + + public static void FlashWindow(ref FLASHWINFO f) + { + //直接调用FlashWindow即可 + FlashWindowEx(ref f); + } + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/WinHttp.cs b/System.Application.SteamTools.Client.Desktop.Windows/WinHttp.cs new file mode 100644 index 000000000..fda5ea225 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/WinHttp.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; + +namespace System +{ + internal static class WinHttp + { + [DllImport("winhttp.dll", SetLastError = true)] + internal static extern bool WinHttpGetIEProxyConfigForCurrentUser(ref WinHttpCurrentUserIEProxyConfig pProxyConfig); + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WinHttpCurrentUserIEProxyConfig + { + [MarshalAs(UnmanagedType.Bool)] + public bool AutoDetect; + + [MarshalAs(UnmanagedType.LPWStr)] + public string AutoConfigUrl; + + [MarshalAs(UnmanagedType.LPWStr)] + public string Proxy; + + [MarshalAs(UnmanagedType.LPWStr)] + public string ProxyBypass; + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/WinInet.cs b/System.Application.SteamTools.Client.Desktop.Windows/WinInet.cs new file mode 100644 index 000000000..bb26b6e4b --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/WinInet.cs @@ -0,0 +1,128 @@ +using System.Net; +using System.Runtime.InteropServices; +using System.Text; + +namespace System +{ + internal class WinInet + { + // For PInvoke: Initiates the enumeration of the cache groups in the Internet cache + [DllImport(@"wininet", + SetLastError = true, + CharSet = CharSet.Auto, + EntryPoint = "FindFirstUrlCacheGroup", + CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr FindFirstUrlCacheGroup( + int dwFlags, + int dwFilter, + IntPtr lpSearchCondition, + int dwSearchCondition, + ref long lpGroupId, + IntPtr lpReserved); + + // For PInvoke: Retrieves the next cache group in a cache group enumeration + [DllImport(@"wininet", + SetLastError = true, + CharSet = CharSet.Auto, + EntryPoint = "FindNextUrlCacheGroup", + CallingConvention = CallingConvention.StdCall)] + public static extern bool FindNextUrlCacheGroup( + IntPtr hFind, + ref long lpGroupId, + IntPtr lpReserved); + + // For PInvoke: Releases the specified GROUPID and any associated state in the cache index file + [DllImport(@"wininet", + SetLastError = true, + CharSet = CharSet.Auto, + EntryPoint = "DeleteUrlCacheGroup", + CallingConvention = CallingConvention.StdCall)] + public static extern bool DeleteUrlCacheGroup( + long groupId, + int dwFlags, + IntPtr lpReserved); + + // For PInvoke: Begins the enumeration of the Internet cache + [DllImport(@"wininet", + SetLastError = true, + CharSet = CharSet.Auto, + EntryPoint = "FindFirstUrlCacheEntryA", + CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr FindFirstUrlCacheEntry( + [MarshalAs(UnmanagedType.LPWStr)] string lpszUrlSearchPattern, + IntPtr lpFirstCacheEntryInfo, + ref int lpdwFirstCacheEntryInfoBufferSize); + + // For PInvoke: Retrieves the next entry in the Internet cache + [DllImport(@"wininet", + SetLastError = true, + CharSet = CharSet.Auto, + EntryPoint = "FindNextUrlCacheEntryA", + CallingConvention = CallingConvention.StdCall)] + public static extern bool FindNextUrlCacheEntry( + IntPtr hFind, + IntPtr lpNextCacheEntryInfo, + ref int lpdwNextCacheEntryInfoBufferSize); + + // For PInvoke: Removes the file that is associated with the source name from the cache, if the file exists + [DllImport(@"wininet", + SetLastError = true, + CharSet = CharSet.Auto, + EntryPoint = "DeleteUrlCacheEntryA", + CallingConvention = CallingConvention.StdCall)] + public static extern bool DeleteUrlCacheEntry( + IntPtr lpszUrlName); + + [DllImport("wininet.dll", + SetLastError = true, +CharSet = CharSet.Unicode, + EntryPoint = "InternetGetCookieEx", + CallingConvention = CallingConvention.StdCall)] + public static extern bool InternetGetCookieEx( + string url, + string? cookieName, + StringBuilder cookieData, + ref int size, + int dwFlags, + IntPtr lpReserved); + + //Read HTTP Only + const int InternetCookieHttponly = 0x2000; + + public static CookieContainer? GetUriCookieContainer(Uri uri) + { + // First, create a null cookie container + CookieContainer? cookies = null; + + // Determine the size of the cookie + var datasize = 8192 * 16; + var cookieData = new StringBuilder(datasize); + + // Call InternetGetCookieEx from wininet.dll + if (!InternetGetCookieEx(uri.ToString(), null, cookieData, ref datasize, InternetCookieHttponly, IntPtr.Zero)) + { + if (datasize < 0) + return null; + // Allocate stringbuilder large enough to hold the cookie + cookieData = new StringBuilder(datasize); + if (!InternetGetCookieEx( + uri.ToString(), + null, cookieData, + ref datasize, + InternetCookieHttponly, + IntPtr.Zero)) + return null; + } + + // If the cookie contains data, add it to the cookie container + if (cookieData.Length > 0) + { + cookies = new CookieContainer(); + cookies.SetCookies(uri, cookieData.ToString().Replace(';', ',')); + } + + // Return the cookie container + return cookies; + } + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/Windows/Point.Generated.cs b/System.Application.SteamTools.Client.Desktop.Windows/Windows/Point.Generated.cs new file mode 100644 index 000000000..cd3ab2e04 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/Windows/Point.Generated.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// +// This file was generated, please do not edit it directly. +// +// Please see MilCodeGen.html for more information. +// + +// These types are aliased to match the unamanaged names used in interop + +namespace System.Windows +{ + [Serializable] + partial struct Point : IFormattable + { + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + /// + /// Compares two Point instances for exact equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Point instances are exactly equal, false otherwise + /// + /// The first Point to compare + /// The second Point to compare + public static bool operator ==(Point point1, Point point2) + { + return point1.X == point2.X && + point1.Y == point2.Y; + } + + /// + /// Compares two Point instances for exact inequality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which are logically equal may fail. + /// Furthermore, using this equality operator, Double.NaN is not equal to itself. + /// + /// + /// bool - true if the two Point instances are exactly unequal, false otherwise + /// + /// The first Point to compare + /// The second Point to compare + public static bool operator !=(Point point1, Point point2) + { + return !(point1 == point2); + } + /// + /// Compares two Point instances for object equality. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the two Point instances are exactly equal, false otherwise + /// + /// The first Point to compare + /// The second Point to compare + public static bool Equals(Point point1, Point point2) + { + return point1.X.Equals(point2.X) && + point1.Y.Equals(point2.Y); + } + + /// + /// Equals - compares this Point with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if the object is an instance of Point and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is Point)) + { + return false; + } + + Point value = (Point)o; + return Point.Equals(this, value); + } + + /// + /// Equals - compares this Point with the passed in object. In this equality + /// Double.NaN is equal to itself, unlike in numeric equality. + /// Note that double values can acquire error when operated upon, such that + /// an exact comparison between two values which + /// are logically equal may fail. + /// + /// + /// bool - true if "value" is equal to "this". + /// + /// The Point to compare to "this" + public bool Equals(Point value) + { + return Point.Equals(this, value); + } + /// + /// Returns the HashCode for this Point + /// + /// + /// int - the HashCode for this Point + /// + public override int GetHashCode() + { + // Perform field-by-field XOR of HashCodes + return X.GetHashCode() ^ + Y.GetHashCode(); + } + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + #region Public Properties + + /// + /// X - double. Default value is 0. + /// + public double X + { + get + { + return _x; + } + + set + { + _x = value; + } + } + + /// + /// Y - double. Default value is 0. + /// + public double Y + { + get + { + return _y; + } + + set + { + _y = value; + } + } + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + string IFormattable.ToString(string format, IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(format, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + // Helper to get the numeric list separator for a given culture. + const char separator = ','; + return String.Format(provider, + "{1:" + format + "}{0}{2:" + format + "}", + separator, + _x, + _y); + } + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Dependency Properties + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Internal Fields + // + //------------------------------------------------------ + + #region Internal Fields + + internal double _x; + internal double _y; + + #endregion Internal Fields + + #region Constructors + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #endregion Constructors + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop.Windows/Windows/Point.cs b/System.Application.SteamTools.Client.Desktop.Windows/Windows/Point.cs new file mode 100644 index 000000000..c0ae8b2cd --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop.Windows/Windows/Point.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// + +namespace System.Windows +{ + /// + /// Point - Defaults to 0,0 + /// + public partial struct Point + { + #region Constructors + + /// + /// Constructor which accepts the X and Y values + /// + /// The value for the X coordinate of the new Point + /// The value for the Y coordinate of the new Point + public Point(double x, double y) + { + _x = x; + _y = y; + } + + #endregion Constructors + + #region Public Methods + + /// + /// Offset - update the location by adding offsetX to X and offsetY to Y + /// + /// The offset in the x dimension + /// The offset in the y dimension + public void Offset(double offsetX, double offsetY) + { + _x += offsetX; + _y += offsetY; + } + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop/Proxy/DnsResolution.cs b/System.Application.SteamTools.Client.Desktop/DnsResolution.cs similarity index 98% rename from System.Application.SteamTools.Client.Desktop/Proxy/DnsResolution.cs rename to System.Application.SteamTools.Client.Desktop/DnsResolution.cs index 46f84aba3..3389b586f 100644 --- a/System.Application.SteamTools.Client.Desktop/Proxy/DnsResolution.cs +++ b/System.Application.SteamTools.Client.Desktop/DnsResolution.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; -namespace System.Application.Proxy +namespace System.Application { /// /// DNS 解析 diff --git a/System.Application.SteamTools.Client.Desktop/Scripts/GM.js b/System.Application.SteamTools.Client.Desktop/Scripts/GM.js new file mode 100644 index 000000000..609163f67 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop/Scripts/GM.js @@ -0,0 +1,49 @@ +// ==UserScript== +// @name GM +// @namespace +// @version 0.1 +// @description 基础脚本框架(不建议取消勾选,会导致某些脚本无法运行) +// @author 软妹币玩家 +// @match * +// @enable true +// @grant GM_xmlhttpRequest +// ==/UserScript== + +var unsafeWindow = this; + +function GM_xmlhttpRequest(option) { + if (String(option) !== '[object Object]') return undefined + option.method = option.method ? option.method.toUpperCase() : 'GET' + option.data = option.data || {} + var formData = [] + for (var key in option.data) { + formData.push(''.concat(key, '=', option.data[key])) + } + option.data = formData.join('&') + + if (option.method === 'GET') { + option.url += location.search.length === 0 ? ''.concat('?', option.data) : ''.concat('&', option.data) + } + + var xhr = new XMLHttpRequest(); + xhr.timeout = option.timeout; + xhr.responseType = option.responseType || 'json' + xhr.onload = (e) => { + if (option.onload && typeof option.onload === 'function') + { + option.onload(e.target) + } + }; + xhr.onerror = option.onerror; + xhr.ontimeout = option.ontimeout; + xhr.open(option.method, option.url, true) + if (option.method === 'POST') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') + } + if (option.headers) { + Object.keys(option.headers).forEach(function (key) { + xhr.setRequestHeader(key, option.headers[key]); + }); + } + xhr.send(option.method === 'POST' ? option.data : null) +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop/Scripts/Steam Economy Enhancer.js b/System.Application.SteamTools.Client.Desktop/Scripts/Steam Economy Enhancer.js new file mode 100644 index 000000000..d0c07b2f8 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop/Scripts/Steam Economy Enhancer.js @@ -0,0 +1,3571 @@ +// ==UserScript== +// @name Steam Economy Enhancer +// @icon https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg +// @namespace https://github.com/Nuklon +// @author Nuklon +// @license MIT +// @version 6.8.3 +// @description Enhances the Steam Inventory and Steam Market. +// @include *://steamcommunity.com/id/*/inventory* +// @include *://steamcommunity.com/profiles/*/inventory* +// @include *://steamcommunity.com/market* +// @include *://steamcommunity.com/tradeoffer* +// @require https://cdn.bootcdn.net/ajax/libs/jquery/3.3.1/jquery.min.js +// @require https://cdn.bootcdn.net/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js +// @require https://github.com/rmbadmin/jquery-observe/releases/download/3.0.0/jquery-observe.js +// @require https://cdnjs.cloudflare.com/ajax/libs/paginationjs/2.1.2/pagination.min.js +// @require https://cdnjs.cloudflare.com/ajax/libs/async/2.6.0/async.js +// @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.7.1/localforage.min.js +// @require https://moment.github.io/luxon/global/luxon.min.js +// @require https://cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.js +// @require https://github.com/rmariuzzo/checkboxes.js/releases/download/v1.2.2/jquery.checkboxes-1.2.2.min.js +// @grant unsafeWindow +// @homepageURL https://github.com/Nuklon/Steam-Economy-Enhancer +// @supportURL https://github.com/Nuklon/Steam-Economy-Enhancer/issues +// @downloadURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js +// @updateURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js +// ==/UserScript== +// jQuery is already added by Steam, force no conflict mode. +(function($, async) { + $.noConflict(true); + + var DateTime = luxon.DateTime; + + const STEAM_INVENTORY_ID = 753; + + const PAGE_MARKET = 0; + const PAGE_MARKET_LISTING = 1; + const PAGE_TRADEOFFER = 2; + const PAGE_INVENTORY = 3; + + const COLOR_ERROR = '#8A4243'; + const COLOR_SUCCESS = '#407736'; + const COLOR_PENDING = '#908F44'; + const COLOR_PRICE_FAIR = '#496424'; + const COLOR_PRICE_CHEAP = '#837433'; + const COLOR_PRICE_EXPENSIVE = '#813030'; + const COLOR_PRICE_NOT_CHECKED = '#26566c'; + + const ERROR_SUCCESS = null; + const ERROR_FAILED = 1; + const ERROR_DATA = 2; + + var marketLists = []; + var totalNumberOfProcessedQueueItems = 0; + var totalNumberOfQueuedItems = 0; + var totalPriceWithFeesOnMarket = 0; + var totalPriceWithoutFeesOnMarket = 0; + var totalScrap = 0; + + var spinnerBlock = + '
 
 
 
 
 
'; + var numberOfFailedRequests = 0; + + var enableConsoleLog = false; + + var isLoggedIn = typeof unsafeWindow.g_rgWalletInfo !== 'undefined' && unsafeWindow.g_rgWalletInfo != null || (typeof unsafeWindow.g_bLoggedIn !== 'undefined' && unsafeWindow.g_bLoggedIn); + + var currentPage = window.location.href.includes('.com/market') ? + (window.location.href.includes('market/listings') ? + PAGE_MARKET_LISTING : + PAGE_MARKET) : + (window.location.href.includes('.com/tradeoffer') ? + PAGE_TRADEOFFER : + PAGE_INVENTORY); + + var market = new SteamMarket(unsafeWindow.g_rgAppContextData, + typeof unsafeWindow.g_strInventoryLoadURL !== 'undefined' && unsafeWindow.g_strInventoryLoadURL != null ? + unsafeWindow.g_strInventoryLoadURL : + location.protocol + '//steamcommunity.com/my/inventory/json/', + isLoggedIn ? unsafeWindow.g_rgWalletInfo : undefined); + + var currencyId = + isLoggedIn && + market != null && + market.walletInfo != null && + market.walletInfo.wallet_currency != null ? + market.walletInfo.wallet_currency : + 3; + + var currencySymbol = unsafeWindow.GetCurrencySymbol(unsafeWindow.GetCurrencyCode(currencyId)); + + function SteamMarket(appContext, inventoryUrl, walletInfo) { + this.appContext = appContext; + this.inventoryUrl = inventoryUrl; + this.walletInfo = walletInfo; + this.inventoryUrlBase = inventoryUrl.replace('/inventory/json', ''); + if (!this.inventoryUrlBase.endsWith('/')) + this.inventoryUrlBase += '/'; + } + + //#region Settings + const SETTING_MIN_NORMAL_PRICE = 'SETTING_MIN_NORMAL_PRICE'; + const SETTING_MAX_NORMAL_PRICE = 'SETTING_MAX_NORMAL_PRICE'; + const SETTING_MIN_FOIL_PRICE = 'SETTING_MIN_FOIL_PRICE'; + const SETTING_MAX_FOIL_PRICE = 'SETTING_MAX_FOIL_PRICE'; + const SETTING_MIN_MISC_PRICE = 'SETTING_MIN_MISC_PRICE'; + const SETTING_MAX_MISC_PRICE = 'SETTING_MAX_MISC_PRICE'; + const SETTING_PRICE_OFFSET = 'SETTING_PRICE_OFFSET'; + const SETTING_PRICE_MIN_CHECK_PRICE = 'SETTING_PRICE_MIN_CHECK_PRICE'; + const SETTING_PRICE_ALGORITHM = 'SETTING_PRICE_ALGORITHM'; + const SETTING_PRICE_IGNORE_LOWEST_Q = 'SETTING_PRICE_IGNORE_LOWEST_Q'; + const SETTING_PRICE_HISTORY_HOURS = 'SETTING_PRICE_HISTORY_HOURS'; + const SETTING_INVENTORY_PRICE_LABELS = 'SETTING_INVENTORY_PRICE_LABELS'; + const SETTING_TRADEOFFER_PRICE_LABELS = 'SETTING_TRADEOFFER_PRICE_LABELS'; + const SETTING_LAST_CACHE = 'SETTING_LAST_CACHE'; + const SETTING_RELIST_AUTOMATICALLY = 'SETTING_RELIST_AUTOMATICALLY'; + const SETTING_MARKET_PAGE_COUNT = 'SETTING_MARKET_PAGE_COUNT'; + const SETTING_INVENTORY_PRICES = 'SETTING_INVENTORY_PRICES'; + + var settingDefaults = { + SETTING_MIN_NORMAL_PRICE: 0.05, + SETTING_MAX_NORMAL_PRICE: 2.50, + SETTING_MIN_FOIL_PRICE: 0.15, + SETTING_MAX_FOIL_PRICE: 10, + SETTING_MIN_MISC_PRICE: 0.05, + SETTING_MAX_MISC_PRICE: 10, + SETTING_PRICE_OFFSET: 0.00, + SETTING_PRICE_MIN_CHECK_PRICE: 0.00, + SETTING_PRICE_ALGORITHM: 1, + SETTING_PRICE_IGNORE_LOWEST_Q: 1, + SETTING_PRICE_HISTORY_HOURS: 12, + SETTING_INVENTORY_PRICE_LABELS: 1, + SETTING_TRADEOFFER_PRICE_LABELS: 1, + SETTING_LAST_CACHE: 0, + SETTING_RELIST_AUTOMATICALLY: 0, + SETTING_MARKET_PAGE_COUNT: 100 + }; + + function getSettingWithDefault(name) { + return getLocalStorageItem(name) || (name in settingDefaults ? settingDefaults[name] : null); + } + + function setSetting(name, value) { + setLocalStorageItem(name, value); + } + //#endregion + + //#region Storage + + var storagePersistent = localforage.createInstance({ + name: 'see_persistent' + }); + + var storageSession; + + var currentUrl = new URL(window.location.href); + var noCache = currentUrl.searchParams.get('no-cache') != null; + + // This does not work the same as the 'normal' session storage because opening a new browser session/tab will clear the cache. + // For this reason, a rolling cache is used. + if (getSessionStorageItem('SESSION') == null || noCache) { + var lastCache = getSettingWithDefault(SETTING_LAST_CACHE); + if (lastCache > 5) + lastCache = 0; + + setSetting(SETTING_LAST_CACHE, lastCache + 1); + + storageSession = localforage.createInstance({ + name: 'see_session_' + lastCache + }); + + storageSession.clear(); // Clear any previous data. + setSessionStorageItem('SESSION', lastCache); + } else { + storageSession = localforage.createInstance({ + name: 'see_session_' + getSessionStorageItem('SESSION') + }); + } + + function getLocalStorageItem(name) { + try { + return localStorage.getItem(name); + } catch (e) { + return null; + } + } + + function setLocalStorageItem(name, value) { + try { + localStorage.setItem(name, value); + return true; + } catch (e) { + logConsole('Failed to set local storage item ' + name + ', ' + e + '.') + return false; + } + } + + function getSessionStorageItem(name) { + try { + return sessionStorage.getItem(name); + } catch (e) { + return null; + } + } + + function setSessionStorageItem(name, value) { + try { + sessionStorage.setItem(name, value); + return true; + } catch (e) { + logConsole('Failed to set session storage item ' + name + ', ' + e + '.') + return false; + } + } + //#endregion + + //#region Price helpers + function getPriceInformationFromItem(item) { + var isTradingCard = getIsTradingCard(item); + var isFoilTradingCard = getIsFoilTradingCard(item); + return getPriceInformation(isTradingCard, isFoilTradingCard); + } + + function getPriceInformation(isTradingCard, isFoilTradingCard) { + var maxPrice = 0; + var minPrice = 0; + + if (!isTradingCard) { + maxPrice = getSettingWithDefault(SETTING_MAX_MISC_PRICE); + minPrice = getSettingWithDefault(SETTING_MIN_MISC_PRICE); + } else { + maxPrice = isFoilTradingCard ? + getSettingWithDefault(SETTING_MAX_FOIL_PRICE) : + getSettingWithDefault(SETTING_MAX_NORMAL_PRICE); + minPrice = isFoilTradingCard ? + getSettingWithDefault(SETTING_MIN_FOIL_PRICE) : + getSettingWithDefault(SETTING_MIN_NORMAL_PRICE); + } + + maxPrice = maxPrice * 100.0; + minPrice = minPrice * 100.0; + + var maxPriceBeforeFees = market.getPriceBeforeFees(maxPrice); + var minPriceBeforeFees = market.getPriceBeforeFees(minPrice); + + return { + maxPrice, + minPrice, + maxPriceBeforeFees, + minPriceBeforeFees + }; + } + + // Calculates the average history price, before the fee. + function calculateAverageHistoryPriceBeforeFees(history) { + var highest = 0; + var total = 0; + + if (history != null) { + // Highest average price in the last xx hours. + var timeAgo = Date.now() - (getSettingWithDefault(SETTING_PRICE_HISTORY_HOURS) * 60 * 60 * 1000); + + history.forEach(function(historyItem) { + var d = new Date(historyItem[0]); + if (d.getTime() > timeAgo) { + highest += historyItem[1] * historyItem[2]; + total += historyItem[2]; + } + }); + } + + if (total == 0) + return 0; + + highest = Math.floor(highest / total); + return market.getPriceBeforeFees(highest); + } + + // Calculates the listing price, before the fee. + function calculateListingPriceBeforeFees(histogram) { + if (typeof histogram === 'undefined' || + histogram == null || + histogram.lowest_sell_order == null || + histogram.sell_order_graph == null) + return 0; + + var listingPrice = market.getPriceBeforeFees(histogram.lowest_sell_order); + + var shouldIgnoreLowestListingOnLowQuantity = getSettingWithDefault(SETTING_PRICE_IGNORE_LOWEST_Q) == 1; + + if (shouldIgnoreLowestListingOnLowQuantity && histogram.sell_order_graph.length >= 2) { + var listingPrice2ndLowest = market.getPriceBeforeFees(histogram.sell_order_graph[1][0] * 100); + + if (listingPrice2ndLowest > listingPrice) { + var numberOfListingsLowest = histogram.sell_order_graph[0][1]; + var numberOfListings2ndLowest = histogram.sell_order_graph[1][1]; + + var percentageLower = (100 * (numberOfListingsLowest / numberOfListings2ndLowest)); + + // The percentage should change based on the quantity (for example, 1200 listings vs 5, or 1 vs 25). + if (numberOfListings2ndLowest >= 1000 && percentageLower <= 5) { + listingPrice = listingPrice2ndLowest; + } else if (numberOfListings2ndLowest < 1000 && percentageLower <= 10) { + listingPrice = listingPrice2ndLowest; + } else if (numberOfListings2ndLowest < 100 && percentageLower <= 15) { + listingPrice = listingPrice2ndLowest; + } else if (numberOfListings2ndLowest < 50 && percentageLower <= 20) { + listingPrice = listingPrice2ndLowest; + } else if (numberOfListings2ndLowest < 25 && percentageLower <= 25) { + listingPrice = listingPrice2ndLowest; + } else if (numberOfListings2ndLowest < 10 && percentageLower <= 30) { + listingPrice = listingPrice2ndLowest; + } + } + } + + return listingPrice; + } + + function calculateBuyOrderPriceBeforeFees(histogram) { + if (typeof histogram === 'undefined') + return 0; + + return market.getPriceBeforeFees(histogram.highest_buy_order); + } + + // Calculate the sell price based on the history and listings. + // applyOffset specifies whether the price offset should be applied when the listings are used to determine the price. + function calculateSellPriceBeforeFees(history, histogram, applyOffset, minPriceBeforeFees, maxPriceBeforeFees) { + var historyPrice = calculateAverageHistoryPriceBeforeFees(history); + var listingPrice = calculateListingPriceBeforeFees(histogram); + var buyPrice = calculateBuyOrderPriceBeforeFees(histogram); + + var shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1; + var shouldUseBuyOrder = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 3; + + // If the highest average price is lower than the first listing, return the offset + that listing. + // Otherwise, use the highest average price instead. + var calculatedPrice = 0; + if (shouldUseBuyOrder && buyPrice !== -2) { + calculatedPrice = buyPrice; + } else if (historyPrice < listingPrice || !shouldUseAverage) { + calculatedPrice = listingPrice; + } else { + calculatedPrice = historyPrice; + } + + var changedToMax = false; + // List for the maximum price if there are no listings yet. + if (calculatedPrice == 0) { + calculatedPrice = maxPriceBeforeFees; + changedToMax = true; + } + + + // Apply the offset to the calculated price, but only if the price wasn't changed to the max (as otherwise it's impossible to list for this price). + if (!changedToMax && applyOffset) { + calculatedPrice = calculatedPrice + (getSettingWithDefault(SETTING_PRICE_OFFSET) * 100); + } + + + // Keep our minimum and maximum in mind. + calculatedPrice = clamp(calculatedPrice, minPriceBeforeFees, maxPriceBeforeFees); + + + // In case there's a buy order higher than the calculated price. + if (typeof histogram !== 'undefined' && histogram != null && histogram.highest_buy_order != null) { + var buyOrderPrice = market.getPriceBeforeFees(histogram.highest_buy_order); + if (buyOrderPrice > calculatedPrice) + calculatedPrice = buyOrderPrice; + } + + return calculatedPrice; + } + //#endregion + + //#region Integer helpers + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + function getNumberOfDigits(x) { + return (Math.log10((x ^ (x >> 31)) - (x >> 31)) | 0) + 1; + } + + function padLeftZero(str, max) { + str = str.toString(); + return str.length < max ? padLeftZero("0" + str, max) : str; + } + + function replaceNonNumbers(str) { + return str.replace(/\D/g, ''); + } + //#endregion + + //#region Steam Market + + // Sell an item with a price in cents. + // Price is before fees. + SteamMarket.prototype.sellItem = function(item, price, callback /*err, data*/ ) { + var sessionId = readCookie('sessionid'); + var itemId = item.assetid || item.id; + $.ajax({ + type: "POST", + url: 'https://steamcommunity.com/market/sellitem/', + data: { + sessionid: sessionId, + appid: item.appid, + contextid: item.contextid, + assetid: itemId, + amount: 1, + price: price + }, + success: function(data) { + if (data.success === false && isRetryMessage(data.message)) { + callback(ERROR_FAILED, data); + } else { + callback(ERROR_SUCCESS, data); + } + }, + error: function(data) { + return callback(ERROR_FAILED, data); + }, + crossDomain: true, + xhrFields: { + withCredentials: true + }, + dataType: 'json' + }); + }; + + // Removes an item. + // Item is the unique item id. + SteamMarket.prototype.removeListing = function(item, callback /*err, data*/ ) { + var sessionId = readCookie('sessionid'); + $.ajax({ + type: "POST", + url: window.location.protocol + '//steamcommunity.com/market/removelisting/' + item, + data: { + sessionid: sessionId + }, + success: function(data) { + callback(ERROR_SUCCESS, data); + }, + error: function() { + return callback(ERROR_FAILED); + }, + crossDomain: true, + xhrFields: { + withCredentials: true + }, + dataType: 'json' + }); + }; + + // Get the price history for an item. + // + // PriceHistory is an array of prices in the form [data, price, number sold]. + // Example: [["Fri, 19 Jul 2013 01:00:00 +0000",7.30050206184,362]] + // Prices are ordered by oldest to most recent. + // Price is inclusive of fees. + SteamMarket.prototype.getPriceHistory = function(item, cache, callback) { + try { + var market_name = getMarketHashName(item); + if (market_name == null) { + callback(ERROR_FAILED); + return; + } + + var appid = item.appid; + + if (cache) { + var storage_hash = 'pricehistory_' + appid + '+' + market_name; + + storageSession.getItem(storage_hash) + .then(function(value) { + if (value != null) + callback(ERROR_SUCCESS, value, true); + else + market.getCurrentPriceHistory(appid, market_name, callback); + }) + .catch(function(error) { + market.getCurrentPriceHistory(appid, market_name, callback); + }); + } else + market.getCurrentPriceHistory(appid, market_name, callback); + } catch (e) { + return callback(ERROR_FAILED); + } + }; + + SteamMarket.prototype.getGooValue = function(item, callback) { + try { + var sessionId = readCookie('sessionid'); + $.ajax({ + type: "GET", + url: this.inventoryUrlBase + 'ajaxgetgoovalue/', + data: { + sessionid: sessionId, + appid: item.market_fee_app, + assetid: item.assetid, + contextid: item.contextid + }, + success: function(data) { + callback(ERROR_SUCCESS, data); + }, + error: function(data) { + return callback(ERROR_FAILED, data); + }, + crossDomain: true, + xhrFields: { + withCredentials: true + }, + dataType: 'json' + }); + } catch (e) { + return callback(ERROR_FAILED); + } + //http://steamcommunity.com/auction/ajaxgetgoovalueforitemtype/?appid=582980&item_type=18&border_color=0 + // OR + //http://steamcommunity.com/my/ajaxgetgoovalue/?sessionid=xyz&appid=535690&assetid=4830605461&contextid=6 + //sessionid=xyz + //appid = 535690 + //assetid = 4830605461 + //contextid = 6 + } + + + // Grinds the item into gems. + SteamMarket.prototype.grindIntoGoo = function(item, callback) { + try { + var sessionId = readCookie('sessionid'); + $.ajax({ + type: "POST", + url: this.inventoryUrlBase + 'ajaxgrindintogoo/', + data: { + sessionid: sessionId, + appid: item.market_fee_app, + assetid: item.assetid, + contextid: item.contextid, + goo_value_expected: item.goo_value_expected + }, + success: function(data) { + callback(ERROR_SUCCESS, data); + }, + error: function(data) { + return callback(ERROR_FAILED, data); + }, + crossDomain: true, + xhrFields: { + withCredentials: true + }, + dataType: 'json' + }); + } catch (e) { + return callback(ERROR_FAILED); + } + + //sessionid = xyz + //appid = 535690 + //assetid = 4830605461 + //contextid = 6 + //goo_value_expected = 10 + //http://steamcommunity.com/my/ajaxgrindintogoo/ + } + + + // Unpacks the booster pack. + SteamMarket.prototype.unpackBoosterPack = function(item, callback) { + try { + var sessionId = readCookie('sessionid'); + $.ajax({ + type: "POST", + url: this.inventoryUrlBase + 'ajaxunpackbooster/', + data: { + sessionid: sessionId, + appid: item.market_fee_app, + communityitemid: item.assetid + }, + success: function(data) { + callback(ERROR_SUCCESS, data); + }, + error: function(data) { + return callback(ERROR_FAILED, data); + }, + crossDomain: true, + xhrFields: { + withCredentials: true + }, + dataType: 'json' + }); + } catch (e) { + return callback(ERROR_FAILED); + } + + //sessionid = xyz + //appid = 535690 + //communityitemid = 4830605461 + //http://steamcommunity.com/my/ajaxunpackbooster/ + } + + // Get the current price history for an item. + SteamMarket.prototype.getCurrentPriceHistory = function(appid, market_name, callback) { + var url = window.location.protocol + + '//steamcommunity.com/market/pricehistory/?appid=' + + appid + + '&market_hash_name=' + + market_name; + + $.get(url, + function(data) { + if (!data || !data.success || !data.prices) { + callback(ERROR_DATA); + return; + } + + // Multiply prices so they're in pennies. + for (var i = 0; i < data.prices.length; i++) { + data.prices[i][1] *= 100; + data.prices[i][2] = parseInt(data.prices[i][2]); + } + + // Store the price history in the session storage. + var storage_hash = 'pricehistory_' + appid + '+' + market_name; + storageSession.setItem(storage_hash, data.prices); + + callback(ERROR_SUCCESS, data.prices, false); + }, + 'json') + .fail(function(data) { + if (!data || !data.responseJSON) { + return callback(ERROR_FAILED); + } + if (!data.responseJSON.success) { + callback(ERROR_DATA); + return; + } + return callback(ERROR_FAILED); + }); + } + + // Get the item name id from a market item. + // + // This id never changes so we can store this in the persistent storage. + SteamMarket.prototype.getMarketItemNameId = function(item, callback) { + try { + var market_name = getMarketHashName(item); + if (market_name == null) { + callback(ERROR_FAILED); + return; + } + + var appid = item.appid; + var storage_hash = 'itemnameid_' + appid + '+' + market_name; + + storagePersistent.getItem(storage_hash) + .then(function(value) { + if (value != null) + callback(ERROR_SUCCESS, value); + else + return market.getCurrentMarketItemNameId(appid, market_name, callback); + }) + .catch(function(error) { + return market.getCurrentMarketItemNameId(appid, market_name, callback); + }); + } catch (e) { + return callback(ERROR_FAILED); + } + } + + // Get the item name id from a market item. + SteamMarket.prototype.getCurrentMarketItemNameId = function(appid, market_name, callback) { + var url = window.location.protocol + '//steamcommunity.com/market/listings/' + appid + '/' + market_name; + $.get(url, + function(page) { + var matches = /Market_LoadOrderSpread\( (.+) \);/.exec(page); + if (matches == null) { + callback(ERROR_DATA); + return; + } + + var item_nameid = matches[1]; + + // Store the item name id in the persistent storage. + var storage_hash = 'itemnameid_' + appid + '+' + market_name; + storagePersistent.setItem(storage_hash, item_nameid); + + callback(ERROR_SUCCESS, item_nameid); + }) + .fail(function(e) { + return callback(ERROR_FAILED, e.status); + }); + }; + + // Get the sales listings for this item in the market, with more information. + // + //{ + //"success" : 1, + //"sell_order_table" : "
Price<\/th>Quantity<\/th><\/tr>
0,04\u20ac<\/td>311<\/td><\/tr>
0,05\u20ac<\/td>895<\/td><\/tr>
0,06\u20ac<\/td>495<\/td><\/tr>
0,07\u20ac<\/td>174<\/td><\/tr>
0,08\u20ac<\/td>49<\/td><\/tr>
0,09\u20ac or more<\/td>41<\/td><\/tr><\/table>", + //"sell_order_summary" : "1965<\/span> for sale starting at 0,04\u20ac<\/span>", + //"buy_order_table" : "
Price<\/th>Quantity<\/th><\/tr>
0,03\u20ac<\/td>93<\/td><\/tr><\/table>", + //"buy_order_summary" : "93<\/span> requests to buy at 0,03\u20ac<\/span> or lower", + //"highest_buy_order" : "3", + //"lowest_sell_order" : "4", + //"buy_order_graph" : [[0.03, 93, "93 buy orders at 0,03\u20ac or higher"]], + //"sell_order_graph" : [[0.04, 311, "311 sell orders at 0,04\u20ac or lower"], [0.05, 1206, "1,206 sell orders at 0,05\u20ac or lower"], [0.06, 1701, "1,701 sell orders at 0,06\u20ac or lower"], [0.07, 1875, "1,875 sell orders at 0,07\u20ac or lower"], [0.08, 1924, "1,924 sell orders at 0,08\u20ac or lower"], [0.09, 1934, "1,934 sell orders at 0,09\u20ac or lower"], [0.1, 1936, "1,936 sell orders at 0,10\u20ac or lower"], [0.11, 1937, "1,937 sell orders at 0,11\u20ac or lower"], [0.12, 1944, "1,944 sell orders at 0,12\u20ac or lower"], [0.14, 1945, "1,945 sell orders at 0,14\u20ac or lower"]], + //"graph_max_y" : 3000, + //"graph_min_x" : 0.03, + //"graph_max_x" : 0.14, + //"price_prefix" : "", + //"price_suffix" : "\u20ac" + //} + SteamMarket.prototype.getItemOrdersHistogram = function(item, cache, callback) { + try { + var market_name = getMarketHashName(item); + if (market_name == null) { + callback(ERROR_FAILED); + return; + } + + var appid = item.appid; + + if (cache) { + var storage_hash = 'itemordershistogram_' + appid + '+' + market_name; + storageSession.getItem(storage_hash) + .then(function(value) { + if (value != null) + callback(ERROR_SUCCESS, value, true); + else { + market.getCurrentItemOrdersHistogram(item, market_name, callback); + } + }) + .catch(function(error) { + market.getCurrentItemOrdersHistogram(item, market_name, callback); + }); + } else { + market.getCurrentItemOrdersHistogram(item, market_name, callback); + } + + } catch (e) { + return callback(ERROR_FAILED); + } + }; + + // Get the sales listings for this item in the market, with more information. + SteamMarket.prototype.getCurrentItemOrdersHistogram = function(item, market_name, callback) { + market.getMarketItemNameId(item, + function(error, item_nameid) { + if (error) { + if (item_nameid != 429) // 429 = Too many requests made. + callback(ERROR_DATA); + else + callback(ERROR_FAILED); + return; + } + var url = window.location.protocol + + '//steamcommunity.com/market/itemordershistogram?language=english¤cy=' + + currencyId + + '&item_nameid=' + + item_nameid + + '&two_factor=0'; + + $.get(url, + function(histogram) { + // Store the histogram in the session storage. + var storage_hash = 'itemordershistogram_' + item.appid + '+' + market_name; + storageSession.setItem(storage_hash, histogram); + + callback(ERROR_SUCCESS, histogram, false); + }) + .fail(function() { + return callback(ERROR_FAILED, null); + }); + }); + }; + + // Calculate the price before fees (seller price) from the buyer price + SteamMarket.prototype.getPriceBeforeFees = function(price, item) { + var publisherFee = -1; + + if (item != null) { + if (item.market_fee != null) + publisherFee = item.market_fee; + else if (item.description != null && item.description.market_fee != null) + publisherFee = item.description.market_fee; + } + + if (publisherFee == -1) { + if (this.walletInfo != null) + publisherFee = this.walletInfo['wallet_publisher_fee_percent_default']; + else + publisherFee = 0.10; + } + + price = Math.round(price); + var feeInfo = CalculateFeeAmount(price, publisherFee, this.walletInfo); + return price - feeInfo.fees; + }; + + // Calculate the buyer price from the seller price + SteamMarket.prototype.getPriceIncludingFees = function(price, item) { + var publisherFee = -1; + if (item != null) { + if (item.market_fee != null) + publisherFee = item.market_fee; + else if (item.description != null && item.description.market_fee != null) + publisherFee = item.description.market_fee; + } + if (publisherFee == -1) { + if (this.walletInfo != null) + publisherFee = this.walletInfo['wallet_publisher_fee_percent_default']; + else + publisherFee = 0.10; + } + + price = Math.round(price); + var feeInfo = CalculateAmountToSendForDesiredReceivedAmount(price, publisherFee, this.walletInfo); + return feeInfo.amount; + }; + //#endregion + + function replaceAll(str, find, replace) { + return str.replace(new RegExp(find, 'g'), replace); + } + + // Cannot use encodeURI / encodeURIComponent, Steam only escapes certain characters. + function escapeURI(name) { + var previousName = ''; + while (previousName != name) { + previousName = name; + name = name.replace('?', '%3F') + .replace('#', '%23') + .replace(' ', '%09'); + } + return name; + } + + //#region Steam Market / Inventory helpers + function getMarketHashName(item) { + if (item == null) + return null; + + if (item.description != null && item.description.market_hash_name != null) + return escapeURI(item.description.market_hash_name); + + if (item.description != null && item.description.name != null) + return escapeURI(item.description.name); + + if (item.market_hash_name != null) + return escapeURI(item.market_hash_name); + + if (item.name != null) + return escapeURI(item.name); + + return null; + } + + function getIsCrate(item) { + if (item == null) + return false; + // This is available on the inventory page. + var tags = item.tags != null ? + item.tags : + (item.description != null && item.description.tags != null ? + item.description.tags : + null); + if (tags != null) { + var isTaggedAsCrate = false; + tags.forEach(function (arrayItem) { + if (arrayItem.category == 'Type') + if (arrayItem.internal_name == 'Supply Crate') + isTaggedAsCrate = true; + }); + if (isTaggedAsCrate) + return true; + } + } + + function getIsTradingCard(item) { + if (item == null) + return false; + + // This is available on the inventory page. + var tags = item.tags != null ? + item.tags : + (item.description != null && item.description.tags != null ? + item.description.tags : + null); + if (tags != null) { + var isTaggedAsTradingCard = false; + tags.forEach(function(arrayItem) { + if (arrayItem.category == 'item_class') + if (arrayItem.internal_name == 'item_class_2') // trading card. + isTaggedAsTradingCard = true; + }); + if (isTaggedAsTradingCard) + return true; + } + + // This is available on the market page. + if (item.owner_actions != null) { + for (var i = 0; i < item.owner_actions.length; i++) { + if (item.owner_actions[i].link == null) + continue; + + // Cards include a link to the gamecard page. + // For example: "http://steamcommunity.com/my/gamecards/503820/". + if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards')) + return true; + } + } + + // A fallback for the market page (only works with language on English). + if (item.type != null && item.type.toLowerCase().includes('trading card')) + return true; + + return false; + } + + function getIsFoilTradingCard(item) { + if (!getIsTradingCard(item)) + return false; + + // This is available on the inventory page. + var tags = item.tags != null ? + item.tags : + (item.description != null && item.description.tags != null ? + item.description.tags : + null); + if (tags != null) { + var isTaggedAsFoilTradingCard = false; + tags.forEach(function(arrayItem) { + if (arrayItem.category == 'cardborder') + if (arrayItem.internal_name == 'cardborder_1') // foil border. + isTaggedAsFoilTradingCard = true; + }); + if (isTaggedAsFoilTradingCard) + return true; + } + + // This is available on the market page. + if (item.owner_actions != null) { + for (var i = 0; i < item.owner_actions.length; i++) { + if (item.owner_actions[i].link == null) + continue; + + // Cards include a link to the gamecard page. + // The border parameter specifies the foil cards. + // For example: "http://steamcommunity.com/my/gamecards/503820/?border=1". + if (item.owner_actions[i].link.toString().toLowerCase().includes('gamecards') && + item.owner_actions[i].link.toString().toLowerCase().includes('border')) + return true; + } + } + + // A fallback for the market page (only works with language on English). + if (item.type != null && item.type.toLowerCase().includes('foil trading card')) + return true; + + return false; + } + + function CalculateFeeAmount(amount, publisherFee, walletInfo) { + if (walletInfo == null || !walletInfo['wallet_fee']) { + return { + fees: 0 + }; + } + + publisherFee = (publisherFee == null) ? 0 : publisherFee; + // Since CalculateFeeAmount has a Math.floor, we could be off a cent or two. Let's check: + var iterations = 0; // shouldn't be needed, but included to be sure nothing unforseen causes us to get stuck + var nEstimatedAmountOfWalletFundsReceivedByOtherParty = + parseInt((amount - parseInt(walletInfo['wallet_fee_base'])) / + (parseFloat(walletInfo['wallet_fee_percent']) + parseFloat(publisherFee) + 1)); + var bEverUndershot = false; + var fees = CalculateAmountToSendForDesiredReceivedAmount(nEstimatedAmountOfWalletFundsReceivedByOtherParty, + publisherFee, + walletInfo); + while (fees.amount != amount && iterations < 10) { + if (fees.amount > amount) { + if (bEverUndershot) { + fees = CalculateAmountToSendForDesiredReceivedAmount( + nEstimatedAmountOfWalletFundsReceivedByOtherParty - 1, + publisherFee, + walletInfo); + fees.steam_fee += (amount - fees.amount); + fees.fees += (amount - fees.amount); + fees.amount = amount; + break; + } else { + nEstimatedAmountOfWalletFundsReceivedByOtherParty--; + } + } else { + bEverUndershot = true; + nEstimatedAmountOfWalletFundsReceivedByOtherParty++; + } + fees = CalculateAmountToSendForDesiredReceivedAmount(nEstimatedAmountOfWalletFundsReceivedByOtherParty, + publisherFee, + walletInfo); + iterations++; + } + // fees.amount should equal the passed in amount + return fees; + } + + // Clamps cur between min and max (inclusive). + function clamp(cur, min, max) { + if (cur < min) + cur = min; + + if (cur > max) + cur = max; + + return cur; + } + + // Strangely named function, it actually works out the fees and buyer price for a seller price + function CalculateAmountToSendForDesiredReceivedAmount(receivedAmount, publisherFee, walletInfo) { + if (walletInfo == null || !walletInfo['wallet_fee']) { + return { + amount: receivedAmount + }; + } + + publisherFee = (publisherFee == null) ? 0 : publisherFee; + var nSteamFee = parseInt(Math.floor(Math.max(receivedAmount * parseFloat(walletInfo['wallet_fee_percent']), + walletInfo['wallet_fee_minimum']) + + parseInt(walletInfo['wallet_fee_base']))); + var nPublisherFee = parseInt(Math.floor(publisherFee > 0 ? Math.max(receivedAmount * publisherFee, 1) : 0)); + var nAmountToSend = receivedAmount + nSteamFee + nPublisherFee; + return { + steam_fee: nSteamFee, + publisher_fee: nPublisherFee, + fees: nSteamFee + nPublisherFee, + amount: parseInt(nAmountToSend) + }; + } + + function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') + c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + return null; + } + + function isRetryMessage(message) { + var messageList = [ + "You cannot sell any items until your previous action completes.", + "There was a problem listing your item. Refresh the page and try again.", + "We were unable to contact the game's item server. The game's item server may be down or Steam may be experiencing temporary connectivity issues. Your listing has not been created. Refresh the page and try again." + ]; + + return messageList.indexOf(message) !== -1; + } + //#endregion + + //#region Logging + var userScrolled = false; + var logger = document.createElement('div'); + logger.setAttribute('id', 'logger'); + + function updateScroll() { + if (!userScrolled) { + var element = document.getElementById("logger"); + element.scrollTop = element.scrollHeight; + } + } + + function logDOM(text) { + logger.innerHTML += text + '
'; + + updateScroll(); + } + + function clearLogDOM() { + logger.innerHTML = ''; + + updateScroll(); + } + + function logConsole(text) { + if (enableConsoleLog) { + console.log(text); + } + } + //#endregion + + //#region Inventory + if (currentPage == PAGE_INVENTORY) { + + function onQueueDrain() { + if (itemQueue.length() == 0 && sellQueue.length() == 0 && scrapQueue.length() == 0 && boosterQueue.length() == 0) { + $('#inventory_items_spinner').remove(); + } + } + + function updateTotals() { + if ($('#loggerTotal').length == 0) { + $(logger).parent().append('
'); + } + + var totals = document.getElementById('loggerTotal'); + totals.innerHTML = ''; + + if (totalPriceWithFeesOnMarket > 0) { + totals.innerHTML += '
Total listed for ' + + (totalPriceWithFeesOnMarket / 100.0).toFixed(2) + + currencySymbol + + ', you will receive ' + + (totalPriceWithoutFeesOnMarket / 100).toFixed(2) + + currencySymbol + + '.
'; + } + if (totalScrap > 0) { + totals.innerHTML += '
Total scrap ' + totalScrap + '.
'; + } + } + + var sellQueue = async.queue(function(task, next) { + market.sellItem(task.item, + task.sellPrice, + function(err, data) { + totalNumberOfProcessedQueueItems++; + + var digits = getNumberOfDigits(totalNumberOfQueuedItems); + var itemId = task.item.assetid || task.item.id; + var itemName = task.item.name || task.item.description.name; + var padLeft = padLeftZero('' + totalNumberOfProcessedQueueItems, digits) + ' / ' + totalNumberOfQueuedItems; + + if (!err) { + logDOM(padLeft + + ' - ' + + itemName + + ' listed for ' + + (market.getPriceIncludingFees(task.sellPrice) / 100.0).toFixed(2) + + currencySymbol + + ', you will receive ' + + (task.sellPrice / 100.0).toFixed(2) + currencySymbol + + '.'); + + $('#' + task.item.appid + '_' + task.item.contextid + '_' + itemId) + .css('background', COLOR_SUCCESS); + + totalPriceWithoutFeesOnMarket += task.sellPrice; + totalPriceWithFeesOnMarket += market.getPriceIncludingFees(task.sellPrice); + updateTotals(); + } else if (data != null && isRetryMessage(data.message)) { + logDOM(padLeft + + ' - ' + + itemName + + ' retrying listing because ' + + data.message[0].toLowerCase() + + data.message.slice(1)); + + totalNumberOfProcessedQueueItems--; + sellQueue.unshift(task); + sellQueue.pause(); + + setTimeout(function() { + sellQueue.resume(); + }, getRandomInt(30000, 45000)); + } else { + if (data != null && data.responseJSON != null && data.responseJSON.message != null) { + logDOM(padLeft + + ' - ' + + itemName + + ' not added to market because ' + + data.responseJSON.message[0].toLowerCase() + + data.responseJSON.message.slice(1)); + } else + logDOM(padLeft + ' - ' + itemName + ' not added to market.'); + + $('#' + task.item.appid + '_' + task.item.contextid + '_' + itemId) + .css('background', COLOR_ERROR); + } + + next(); + }); + }, + 1); + + sellQueue.drain = function() { + onQueueDrain(); + } + + function sellAllItems(appId) { + loadAllInventories().then(function() { + var items = getInventoryItems(); + var filteredItems = []; + + items.forEach(function(item) { + if (!item.marketable) { + return; + } + + filteredItems.push(item); + }); + + sellItems(filteredItems); + }, + function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + function sellAllCards() { + loadAllInventories().then(function() { + var items = getInventoryItems(); + var filteredItems = []; + + items.forEach(function(item) { + if (!getIsTradingCard(item) || !item.marketable) { + return; + } + + filteredItems.push(item); + }); + + sellItems(filteredItems); + }, + function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + function sellAllCrates() { + loadAllInventories().then(function () { + var items = getInventoryItems(); + var filteredItems = []; + items.forEach(function (item) { + if (!getIsCrate(item) || !item.marketable) { + return; + } + filteredItems.push(item); + }); + + sellItems(filteredItems); + }, + function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + var scrapQueue = async.queue(function(item, next) { + scrapQueueWorker(item, function(success) { + if (success) { + setTimeout(function() { + next(); + }, 250); + } else { + var delay = numberOfFailedRequests > 1 ? + getRandomInt(30000, 45000) : + getRandomInt(1000, 1500); + + if (numberOfFailedRequests > 3) + numberOfFailedRequests = 0; + + setTimeout(function() { + next(); + }, delay); + } + }); + }, 1); + + scrapQueue.drain = function() { + onQueueDrain(); + } + + function scrapQueueWorker(item, callback) { + var failed = 0; + var itemName = item.name || item.description.name; + var itemId = item.assetid || item.id; + + market.getGooValue(item, + function(err, goo) { + totalNumberOfProcessedQueueItems++; + + var digits = getNumberOfDigits(totalNumberOfQueuedItems); + var padLeft = padLeftZero('' + totalNumberOfProcessedQueueItems, digits) + ' / ' + totalNumberOfQueuedItems; + + if (err != ERROR_SUCCESS) { + logConsole('Failed to get gems value for ' + itemName); + logDOM(padLeft + ' - ' + itemName + ' not turned into gems due to missing gems value.'); + + $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_ERROR); + return callback(false); + } + + item.goo_value_expected = parseInt(goo.goo_value); + + market.grindIntoGoo(item, + function(err, result) { + if (err != ERROR_SUCCESS) { + logConsole('Failed to turn item into gems for ' + itemName); + logDOM(padLeft + ' - ' + itemName + ' not turned into gems due to unknown error.'); + + $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_ERROR); + return callback(false); + } + + logConsole('============================') + logConsole(itemName); + logConsole('Turned into ' + goo.goo_value + ' gems'); + logDOM(padLeft + ' - ' + itemName + ' turned into ' + item.goo_value_expected + ' gems.'); + $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_SUCCESS); + + totalScrap += item.goo_value_expected; + updateTotals(); + + callback(true); + }); + }); + } + + var boosterQueue = async.queue(function(item, next) { + boosterQueueWorker(item, function(success) { + if (success) { + setTimeout(function() { + next(); + }, 250); + } else { + var delay = numberOfFailedRequests > 1 ? + getRandomInt(30000, 45000) : + getRandomInt(1000, 1500); + + if (numberOfFailedRequests > 3) + numberOfFailedRequests = 0; + + setTimeout(function() { + next(); + }, delay); + } + }); + }, 1); + + boosterQueue.drain = function() { + onQueueDrain(); + } + + function boosterQueueWorker(item, callback) { + var failed = 0; + var itemName = item.name || item.description.name; + var itemId = item.assetid || item.id; + + market.unpackBoosterPack(item, + function(err, goo) { + totalNumberOfProcessedQueueItems++; + + var digits = getNumberOfDigits(totalNumberOfQueuedItems); + var padLeft = padLeftZero('' + totalNumberOfProcessedQueueItems, digits) + ' / ' + totalNumberOfQueuedItems; + + if (err != ERROR_SUCCESS) { + logConsole('Failed to unpack booster pack ' + itemName); + logDOM(padLeft + ' - ' + itemName + ' not unpacked.'); + + $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_ERROR); + return callback(false); + } + + logDOM(padLeft + ' - ' + itemName + ' unpacked.'); + $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_SUCCESS); + + callback(true); + }); + } + + + // Turns the selected items into gems. + function turnSelectedItemsIntoGems() { + var ids = getSelectedItems(); + + loadAllInventories().then(function() { + var items = getInventoryItems(); + + var numberOfQueuedItems = 0; + items.forEach(function(item) { + // Ignored queued items. + if (item.queued != null) { + return; + } + + if (item.owner_actions == null) { + return; + } + + var canTurnIntoGems = false; + for (var owner_action in item.owner_actions) { + if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { + canTurnIntoGems = true; + } + } + + if (!canTurnIntoGems) + return; + + var itemId = item.assetid || item.id; + if (ids.indexOf(itemId) !== -1) { + item.queued = true; + scrapQueue.push(item); + numberOfQueuedItems++; + } + }); + + if (numberOfQueuedItems > 0) { + totalNumberOfQueuedItems += numberOfQueuedItems; + + $('#inventory_items_spinner').remove(); + $('#inventory_sell_buttons').append('
' + + spinnerBlock + + '
Processing ' + numberOfQueuedItems + ' items
' + + '
'); + } + }, function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + // Unpacks the selected booster packs. + function unpackSelectedBoosterPacks() { + var ids = getSelectedItems(); + + loadAllInventories().then(function() { + var items = getInventoryItems(); + + var numberOfQueuedItems = 0; + items.forEach(function(item) { + // Ignored queued items. + if (item.queued != null) { + return; + } + + if (item.owner_actions == null) { + return; + } + + var canOpenBooster = false; + for (var owner_action in item.owner_actions) { + if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { + canOpenBooster = true; + } + } + + if (!canOpenBooster) + return; + + var itemId = item.assetid || item.id; + if (ids.indexOf(itemId) !== -1) { + item.queued = true; + boosterQueue.push(item); + numberOfQueuedItems++; + } + }); + + if (numberOfQueuedItems > 0) { + totalNumberOfQueuedItems += numberOfQueuedItems; + + $('#inventory_items_spinner').remove(); + $('#inventory_sell_buttons').append('
' + + spinnerBlock + + '
Processing ' + numberOfQueuedItems + ' items
' + + '
'); + } + }, function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + function sellSelectedItems() { + getInventorySelectedMarketableItems(function(items) { + sellItems(items); + }); + } + + function canSellSelectedItemsManually(items) { + // We have to construct an URL like this + // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 + var appid = items[0].appid; + var contextid = items[0].contextid; + + var hasInvalidItem = false; + + items.forEach(function(item) { + if (item.contextid != contextid || item.commodity == false) + hasInvalidItem = true; + }); + + return !hasInvalidItem; + } + + function sellSelectedItemsManually() { + getInventorySelectedMarketableItems(function(items) { + // We have to construct an URL like this + // https://steamcommunity.com/market/multisell?appid=730&contextid=2&items[]=Falchion%20Case&qty[]=100 + + var appid = items[0].appid; + var contextid = items[0].contextid; + + var itemsWithQty = {}; + + items.forEach(function(item) { + itemsWithQty[item.market_hash_name] = itemsWithQty[item.market_hash_name] + 1 || 1; + }); + + var itemsString = ''; + for (var itemName in itemsWithQty) { + itemsString += '&items[]=' + encodeURI(itemName) + '&qty[]=' + itemsWithQty[itemName]; + } + + var baseUrl = 'https://steamcommunity.com/market/multisell'; + var redirectUrl = baseUrl + '?appid=' + appid + '&contextid=' + contextid + itemsString; + + var dialog = unsafeWindow.ShowDialog('Steam Economy Enhancer', ''); + dialog.OnDismiss(function() { + items.forEach(function(item) { + var itemId = item.assetid || item.id; + $('#' + item.appid + '_' + item.contextid + '_' + itemId).css('background', COLOR_PENDING); + }); + }); + }); + } + + function sellItems(items) { + if (items.length == 0) { + logDOM('These items cannot be added to the market...'); + + return; + } + + var numberOfQueuedItems = 0; + + items.forEach(function(item, index, array) { + // Ignored queued items. + if (item.queued != null) { + return; + } + + item.queued = true; + var itemId = item.assetid || item.id; + item.ignoreErrors = false; + itemQueue.push(item); + numberOfQueuedItems++; + }); + + if (numberOfQueuedItems > 0) { + totalNumberOfQueuedItems += numberOfQueuedItems; + + $('#inventory_items_spinner').remove(); + $('#inventory_sell_buttons').append('
' + + spinnerBlock + + '
Processing ' + numberOfQueuedItems + ' items
' + + '
'); + } + } + + var itemQueue = async.queue(function(item, next) { + itemQueueWorker(item, + item.ignoreErrors, + function(success, cached) { + if (success) { + setTimeout(function() { + next(); + }, + cached ? 0 : getRandomInt(1000, 1500)); + } else { + if (!item.ignoreErrors) { + item.ignoreErrors = true; + itemQueue.push(item); + } + + var delay = numberOfFailedRequests > 1 ? + getRandomInt(30000, 45000) : + getRandomInt(1000, 1500); + + if (numberOfFailedRequests > 3) + numberOfFailedRequests = 0; + + setTimeout(function() { + next(); + }, + cached ? 0 : delay); + } + }); + }, 1); + + function itemQueueWorker(item, ignoreErrors, callback) { + var priceInfo = getPriceInformationFromItem(item); + + var failed = 0; + var itemName = item.name || item.description.name; + + market.getPriceHistory(item, + true, + function(err, history, cachedHistory) { + if (err) { + logConsole('Failed to get price history for ' + itemName); + + if (err == ERROR_FAILED) + failed += 1; + } + + market.getItemOrdersHistogram(item, + true, + function(err, histogram, cachedListings) { + if (err) { + logConsole('Failed to get orders histogram for ' + itemName); + + if (err == ERROR_FAILED) + failed += 1; + } + + if (failed > 0 && !ignoreErrors) { + return callback(false, cachedHistory && cachedListings); + } + + logConsole('============================') + logConsole(itemName); + + var sellPrice = calculateSellPriceBeforeFees(history, + histogram, + true, + priceInfo.minPriceBeforeFees, + priceInfo.maxPriceBeforeFees); + + + logConsole('Sell price: ' + + sellPrice / 100.0 + + ' (' + + market.getPriceIncludingFees(sellPrice) / 100.0 + + ')'); + + sellQueue.push({ + item: item, + sellPrice: sellPrice + }); + + return callback(true, cachedHistory && cachedListings); + }); + }); + } + + // Initialize the inventory UI. + function initializeInventoryUI() { + var isOwnInventory = unsafeWindow.g_ActiveUser.strSteamId == unsafeWindow.g_steamID; + var previousSelection = -1; // To store the index of the previous selection. + updateInventoryUI(isOwnInventory); + + $('.games_list_tabs').on('click', + '*', + function() { + updateInventoryUI(isOwnInventory); + }); + + // Ignore selection on other user's inventories. + if (!isOwnInventory) + return; + + // Steam adds 'display:none' to items while searching. These should not be selected while using shift/ctrl. + var filter = ".itemHolder:not([style*=none])"; + $('#inventories').selectable({ + filter: filter, + selecting: function(e, ui) { + // Get selected item index. + var selectedIndex = $(ui.selecting.tagName, e.target).index(ui.selecting); + + // If shift key was pressed and there is previous - select them all. + if (e.shiftKey && previousSelection > -1) { + $(ui.selecting.tagName, e.target) + .slice(Math.min(previousSelection, selectedIndex), + 1 + Math.max(previousSelection, selectedIndex)).each(function() { + if ($(this).is(filter)) { + $(this).addClass('ui-selected'); + } + }); + previousSelection = -1; // Reset previous. + } else { + previousSelection = selectedIndex; // Save previous. + } + }, + selected: function(e, ui) { + updateInventorySelection(ui.selected); + } + }); + } + + // Gets the selected items in the inventory. + function getSelectedItems() { + var ids = []; + $('.inventory_ctn').each(function() { + $(this).find('.inventory_page').each(function() { + var inventory_page = this; + + $(inventory_page).find('.itemHolder').each(function() { + if (!$(this).hasClass('ui-selected')) + return; + + $(this).find('.item').each(function() { + var matches = this.id.match(/_(\-?\d+)$/); + if (matches) { + ids.push(matches[1]); + } + }); + }); + }); + }); + + return ids; + } + + // Gets the selected and marketable items in the inventory. + function getInventorySelectedMarketableItems(callback) { + var ids = getSelectedItems(); + + loadAllInventories().then(function() { + var items = getInventoryItems(); + var filteredItems = []; + + items.forEach(function(item) { + if (!item.marketable) { + return; + } + + var itemId = item.assetid || item.id; + if (ids.indexOf(itemId) !== -1) { + filteredItems.push(item); + } + }); + + callback(filteredItems); + }, function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + // Gets the selected and gemmable items in the inventory. + function getInventorySelectedGemsItems(callback) { + var ids = getSelectedItems(); + + loadAllInventories().then(function() { + var items = getInventoryItems(); + var filteredItems = []; + + items.forEach(function(item) { + var canTurnIntoGems = false; + for (var owner_action in item.owner_actions) { + if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('GetGooValue')) { + canTurnIntoGems = true; + } + } + + if (!canTurnIntoGems) + return; + + var itemId = item.assetid || item.id; + if (ids.indexOf(itemId) !== -1) { + filteredItems.push(item); + } + }); + + callback(filteredItems); + }, function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + // Gets the selected and booster pack items in the inventory. + function getInventorySelectedBoosterPackItems(callback) { + var ids = getSelectedItems(); + + loadAllInventories().then(function() { + var items = getInventoryItems(); + var filteredItems = []; + + items.forEach(function(item) { + var canOpenBooster = false; + for (var owner_action in item.owner_actions) { + if (item.owner_actions[owner_action].link != null && item.owner_actions[owner_action].link.includes('OpenBooster')) { + canOpenBooster = true; + } + } + + if (!canOpenBooster) + return; + + var itemId = item.assetid || item.id; + if (ids.indexOf(itemId) !== -1) { + filteredItems.push(item); + } + }); + + callback(filteredItems); + }, function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + // Updates the (selected) sell ... items button. + function updateSellSelectedButton() { + getInventorySelectedMarketableItems(function(items) { + var selectedItems = items.length; + if (items.length == 0) { + $('.sell_selected').hide(); + $('.sell_manual').hide(); + } else { + $('.sell_selected').show(); + if (canSellSelectedItemsManually(items)) { + $('.sell_manual').show(); + $('.sell_manual > span').text('Sell ' + selectedItems + (selectedItems == 1 ? ' Item Manual' : ' Items Manual')); + } else { + $('.sell_manual').hide(); + } + $('.sell_selected > span').text('Sell ' + selectedItems + (selectedItems == 1 ? ' Item' : ' Items')); + } + }); + } + + // Updates the (selected) turn into ... gems button. + function updateTurnIntoGemsButton() { + getInventorySelectedGemsItems(function(items) { + var selectedItems = items.length; + if (items.length == 0) { + $('.turn_into_gems').hide(); + } else { + $('.turn_into_gems').show(); + $('.turn_into_gems > span') + .text('Turn ' + selectedItems + (selectedItems == 1 ? ' Item Into Gems' : ' Items Into Gems')); + } + }); + } + + // Updates the (selected) open ... booster packs button. + function updateOpenBoosterPacksButton() { + getInventorySelectedBoosterPackItems(function(items) { + var selectedItems = items.length; + if (items.length == 0) { + $('.unpack_booster_packs').hide(); + } else { + $('.unpack_booster_packs').show(); + $('.unpack_booster_packs > span') + .text('Unpack ' + selectedItems + (selectedItems == 1 ? ' Booster Pack' : ' Booster Packs')); + } + }); + } + + function updateInventorySelection(item) { + updateSellSelectedButton(); + updateTurnIntoGemsButton(); + updateOpenBoosterPacksButton(); + + // Wait until g_ActiveInventory.selectedItem is identical to the selected UI item. + // This also makes sure that the new - and correct - item_info (iteminfo0 or iteminfo1) is visible. + var selectedItemIdUI = $('div', item).attr('id'); + var selectedItemIdInventory = getActiveInventory().selectedItem.appid + + '_' + + getActiveInventory().selectedItem.contextid + + '_' + + getActiveInventory().selectedItem.assetid; + if (selectedItemIdUI !== selectedItemIdInventory) { + setTimeout(function() { + updateInventorySelection(item); + }, 250); + + return; + } + + var item_info = $('.inventory_iteminfo:visible').first(); + if (item_info.html().indexOf('checkout/sendgift/') > -1) // Gifts have no market information. + return; + + // Use a 'hard' item id instead of relying on the selected item_info (sometimes Steam temporarily changes the correct item (?)). + var item_info_id = item_info.attr('id'); + + // Move scrap to bottom, this is of little interest. + var scrap = $('#' + item_info_id + '_scrap_content'); + scrap.next().insertBefore(scrap); + + // Starting at prices are already retrieved in the table. + //$('#' + item_info_id + '_item_market_actions > div:nth-child(1) > div:nth-child(2)') + // .remove(); // Starting at: x,xx. + + var market_hash_name = getMarketHashName(getActiveInventory().selectedItem); + if (market_hash_name == null) + return; + + var appid = getActiveInventory().selectedItem.appid; + var item = { + appid: parseInt(appid), + description: { + market_hash_name: market_hash_name + } + }; + + market.getItemOrdersHistogram(item, + false, + function(err, histogram) { + if (err) { + logConsole('Failed to get orders histogram for ' + (getActiveInventory().selectedItem.name || getActiveInventory().selectedItem.description.name)); + return; + } + + var groupMain = $('
' + + '
Sell
' + + histogram.sell_order_table + + '
' + + '
Buy
' + + histogram.buy_order_table + + '
' + + '
'); + + $('#' + item_info_id + '_item_market_actions > div').after(groupMain); + + var ownerActions = $('#' + item_info_id + '_item_owner_actions'); + // ownerActions is hidden on other games' inventories, we need to show it to have a "Market" button visible + ownerActions.show(); + + ownerActions.append('View in Community Market'); + $('#' + item_info_id + '_item_market_actions > div:nth-child(1) > div:nth-child(1)').hide(); + + var isBoosterPack = getActiveInventory().selectedItem.name.toLowerCase().endsWith('booster pack'); + if (isBoosterPack) { + var tradingCardsUrl = "/market/search?q=&category_753_Game%5B%5D=tag_app_" + getActiveInventory().selectedItem.market_fee_app + "&category_753_item_class%5B%5D=tag_item_class_2&appid=753"; + ownerActions.append('
View trading cards in Community Market'); + } + + + // Generate quick sell buttons. + var itemId = getActiveInventory().selectedItem.assetid || getActiveInventory().selectedItem.id; + + // Ignored queued items. + if (getActiveInventory().selectedItem.queued != null) { + return; + } + + var prices = []; + + if (histogram != null && histogram.highest_buy_order != null) { + prices.push(parseInt(histogram.highest_buy_order)); + } + + if (histogram != null && histogram.lowest_sell_order != null) { + // Transaction volume must be separable into three or more parts (no matter if equal): valve+publisher+seller. + if (parseInt(histogram.lowest_sell_order) > 3) { + prices.push(parseInt(histogram.lowest_sell_order) - 1); + } + prices.push(parseInt(histogram.lowest_sell_order)); + } + + prices = prices.filter((v, i) => prices.indexOf(v) === i).sort((a, b) => a - b); + + var buttons = ' '; + prices.forEach(function(e) { + buttons += + '' + + '' + + '' + + (e / 100.0) + + currencySymbol + + '' + + '' + + '' + + '' + }); + + $('#' + item_info_id + '_item_market_actions', item_info).append(buttons); + + $('#' + item_info_id + '_item_market_actions', item_info).append( + ''); + + $('.quick_sell').on('click', + function() { + var price = $(this).attr('id').replace('quick_sell', ''); + price = market.getPriceBeforeFees(price); + + totalNumberOfQueuedItems++; + + sellQueue.push({ + item: getActiveInventory().selectedItem, + sellPrice: price + }); + }); + + $('.quick_sell_custom').on('click', + function() { + var price = $('#quick_sell_input', $('#' + item_info_id + '_item_market_actions', item_info)).val() * 100; + price = market.getPriceBeforeFees(price); + + totalNumberOfQueuedItems++; + + sellQueue.push({ + item: getActiveInventory().selectedItem, + sellPrice: price + }); + }); + }); + } + + // Update the inventory UI. + function updateInventoryUI(isOwnInventory) { + // Remove previous containers (e.g., when a user changes inventory). + $('#inventory_sell_buttons').remove(); + $('#price_options').remove(); + $('#inventory_reload_button').remove(); + + $('#see_settings').remove(); + $('#global_action_menu') + .prepend('⬖ Steam Economy Enhancer'); + $('#see_settings').on('click', '*', () => openSettings()); + + var appId = getActiveInventory().m_appid; + var showMiscOptions = appId == 753; + var TF2 = appId == 440; + + var sellButtons = $('
' + + 'Sell All Items' + + '' + + '' + + (showMiscOptions ? + 'Sell All Cards' + + '' : + '') + + (TF2 ? 'Sell All Crates' : '') + + '
'); + + var reloadButton = + $('Reload Inventory'); + + $('#inventory_logos')[0].style.height = 'auto'; + + $('#inventory_applogo').hide(); // Hide the Steam/game logo, we don't need to see it twice. + $('#inventory_applogo').after(logger); + + + $("#logger").on('scroll', + function() { + var hasUserScrolledToBottom = + $("#logger").prop('scrollHeight') - $("#logger").prop('clientHeight') <= + $("#logger").prop('scrollTop') + 1; + userScrolled = !hasUserScrolledToBottom; + }); + + // Only add buttons on the user's inventory. + if (isOwnInventory) { + $('#inventory_applogo').after(sellButtons); + + // Add bindings to sell buttons. + $('.sell_all').on('click', + '*', + function() { + sellAllItems(appId); + }); + $('.sell_selected').on('click', '*', sellSelectedItems); + $('.sell_manual').on('click', '*', sellSelectedItemsManually); + $('.sell_all_cards').on('click', '*', sellAllCards); + $('.sell_all_crates').on('click', '*', sellAllCrates); + $('.turn_into_gems').on('click', '*', turnSelectedItemsIntoGems); + $('.unpack_booster_packs').on('click', '*', unpackSelectedBoosterPacks); + + } + + $('.inventory_rightnav').prepend(reloadButton); + $('.reload_inventory').on('click', + '*', + function() { + window.location.reload(); + }); + + loadAllInventories().then(function() { + var updateInventoryPrices = function() { + if (getSettingWithDefault(SETTING_INVENTORY_PRICE_LABELS) == 1) { + setInventoryPrices(getInventoryItems()); + } + }; + + // Load after the inventory is loaded. + updateInventoryPrices(); + + $('#inventory_pagecontrols').observe('childlist', + '*', + function(record) { + updateInventoryPrices(); + }); + }, + function() { + logDOM('Could not retrieve the inventory...'); + }); + } + + // Loads the specified inventories. + function loadInventories(inventories) { + return new Promise(function(resolve) { + inventories.reduce(function(promise, inventory) { + return promise.then(function() { + return inventory.LoadCompleteInventory().done(function() {}); + }); + }, + Promise.resolve()); + + resolve(); + }); + } + + // Loads all inventories. + function loadAllInventories() { + var items = []; + + for (var child in getActiveInventory().m_rgChildInventories) { + items.push(getActiveInventory().m_rgChildInventories[child]); + } + items.push(getActiveInventory()); + + return loadInventories(items); + } + + // Gets the inventory items from the active inventory. + function getInventoryItems() { + var arr = []; + + for (var child in getActiveInventory().m_rgChildInventories) { + for (var key in getActiveInventory().m_rgChildInventories[child].m_rgAssets) { + var value = getActiveInventory().m_rgChildInventories[child].m_rgAssets[key]; + if (typeof value === 'object') { + // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. + Object.assign(value, value.description); + // Includes the id of the inventory item. + value['id'] = key; + arr.push(value); + } + } + } + + // Some inventories (e.g. BattleBlock Theater) do not have child inventories, they have just one. + for (var key in getActiveInventory().m_rgAssets) { + var value = getActiveInventory().m_rgAssets[key]; + if (typeof value === 'object') { + // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. + Object.assign(value, value.description); + // Includes the id of the inventory item. + value['id'] = key; + arr.push(value); + } + } + + return arr; + } + } + //#endregion + + //#region Inventory + Tradeoffer + if (currentPage == PAGE_INVENTORY || currentPage == PAGE_TRADEOFFER) { + + // Gets the active inventory. + function getActiveInventory() { + return unsafeWindow.g_ActiveInventory; + } + + // Sets the prices for the items. + function setInventoryPrices(items) { + inventoryPriceQueue.kill(); + + items.forEach(function(item) { + if (!item.marketable) { + return; + } + + if (!$(item.element).is(":visible")) { + return; + } + + inventoryPriceQueue.push(item); + }); + } + + var inventoryPriceQueue = async.queue(function(item, next) { + inventoryPriceQueueWorker(item, + false, + function(success, cached) { + if (success) { + setTimeout(function() { + next(); + }, + cached ? 0 : getRandomInt(1000, 1500)); + } else { + if (!item.ignoreErrors) { + item.ignoreErrors = true; + inventoryPriceQueue.push(item); + } + + numberOfFailedRequests++; + + var delay = numberOfFailedRequests > 1 ? + getRandomInt(30000, 45000) : + getRandomInt(1000, 1500); + + if (numberOfFailedRequests > 3) + numberOfFailedRequests = 0; + + setTimeout(function() { + next(); + }, cached ? 0 : delay); + } + }); + }, + 1); + + function inventoryPriceQueueWorker(item, ignoreErrors, callback) { + var priceInfo = getPriceInformationFromItem(item); + + var failed = 0; + var itemName = item.name || item.description.name; + + // Only get the market orders here, the history is not important to visualize the current prices. + market.getItemOrdersHistogram(item, + true, + function(err, histogram, cachedListings) { + if (err) { + logConsole('Failed to get orders histogram for ' + itemName); + + if (err == ERROR_FAILED) + failed += 1; + } + + if (failed > 0 && !ignoreErrors) { + return callback(false, cachedListings); + } + + var sellPrice = calculateSellPriceBeforeFees(null, histogram, false, 0, 65535); + + var itemPrice = sellPrice == 65535 ? + '∞' : + (market.getPriceIncludingFees(sellPrice) / 100.0).toFixed(2) + currencySymbol; + + var elementName = (currentPage == PAGE_TRADEOFFER ? '#item' : '#') + + item.appid + + '_' + + item.contextid + + '_' + + item.id; + var element = $(elementName); + + $('.inventory_item_price', element).remove(); + element.append('' + itemPrice + ''); + + return callback(true, cachedListings); + }); + } + } + //#endregion + + //#region Market + if (currentPage == PAGE_MARKET || currentPage == PAGE_MARKET_LISTING) { + var marketListingsRelistedAssets = []; + + var marketListingsQueue = async.queue(function(listing, next) { + marketListingsQueueWorker(listing, + false, + function(success, cached) { + if (success) { + setTimeout(function() { + next(); + }, + cached ? 0 : getRandomInt(1000, 1500)); + } else { + setTimeout(function() { + marketListingsQueueWorker(listing, + true, + function(success, cached) { + next(); // Go to the next queue item, regardless of success. + }); + }, + cached ? 0 : getRandomInt(30000, 45000)); + } + }); + }, 1); + + marketListingsQueue.drain = function() { + injectJs(function() { + g_bMarketWindowHidden = false; + }) + }; + + // Gets the price, in cents, from a market listing. + function getPriceFromMarketListing(listing) { + var priceLabel = listing.trim().replace('--', '00'); + + // Fixes RUB, which has a dot at the end. + if (priceLabel[priceLabel.length - 1] === '.' || priceLabel[priceLabel.length - 1] === ",") + priceLabel = priceLabel.slice(0, -1); + + // For round numbers (e.g., 100 EUR). + if (priceLabel.indexOf('.') === -1 && priceLabel.indexOf(',') === -1) { + priceLabel = priceLabel + ',00'; + } + + return parseInt(replaceNonNumbers(priceLabel)); + } + + function marketListingsQueueWorker(listing, ignoreErrors, callback) { + var asset = unsafeWindow.g_rgAssets[listing.appid][listing.contextid][listing.assetid]; + + // An asset: + //{ + // "currency" : 0, + // "appid" : 753, + // "contextid" : "6", + // "id" : "4363079664", + // "classid" : "2228526061", + // "instanceid" : "0", + // "amount" : "1", + // "status" : 2, + // "original_amount" : "1", + // "background_color" : "", + // "icon_url" : "xx", + // "icon_url_large" : "xxx", + // "descriptions" : [{ + // "value" : "Their dense, shaggy fur conceals the presence of swams of moogamites, purple scaly skin, and more nipples than one would expect." + // } + // ], + // "tradable" : 1, + // "owner_actions" : [{ + // "link" : "http://steamcommunity.com/my/gamecards/443880/", + // "name" : "View badge progress" + // }, { + // "link" : "javascript:GetGooValue( '%contextid%', '%assetid%', 443880, 7, 0 )", + // "name" : "Turn into Gems..." + // } + // ], + // "name" : "Wook", + // "type" : "Loot Rascals Trading Card", + // "market_name" : "Wook", + // "market_hash_name" : "443880-Wook", + // "market_fee_app" : 443880, + // "commodity" : 1, + // "market_tradable_restriction" : 7, + // "market_marketable_restriction" : 7, + // "marketable" : 1, + // "app_icon" : "xxxx", + // "owner" : 0 + //} + + var market_hash_name = getMarketHashName(asset); + var appid = listing.appid; + + var listingUI = $(getListingFromLists(listing.listingid).elm); + + var game_name = asset.type; + var price = getPriceFromMarketListing($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listingUI).text()); + + if (price <= getSettingWithDefault(SETTING_PRICE_MIN_CHECK_PRICE) * 100) { + $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_NOT_CHECKED); + $('.market_listing_my_price', listingUI).last().prop('title', 'The price is not checked.'); + listingUI.addClass('not_checked'); + + return callback(true, true); + } + + var priceInfo = getPriceInformationFromItem(asset); + var item = { + appid: parseInt(appid), + description: { + market_hash_name: market_hash_name + } + }; + + var failed = 0; + + market.getPriceHistory(item, + true, + function(errorPriceHistory, history, cachedHistory) { + if (errorPriceHistory) { + logConsole('Failed to get price history for ' + game_name); + + if (errorPriceHistory == ERROR_FAILED) + failed += 1; + } + + market.getItemOrdersHistogram(item, + true, + function(errorHistogram, histogram, cachedListings) { + if (errorHistogram) { + logConsole('Failed to get orders histogram for ' + game_name); + + if (errorHistogram == ERROR_FAILED) + failed += 1; + } + + if (failed > 0 && !ignoreErrors) { + return callback(false, cachedHistory && cachedListings); + } + + // Shows the highest buy order price on the market listings. + // The 'histogram.highest_buy_order' is not reliable as Steam is caching this value, but it gives some idea for older titles/listings. + var highestBuyOrderPrice = (histogram == null || histogram.highest_buy_order == null ? + '-' : + ((histogram.highest_buy_order / 100) + currencySymbol)); + $('.market_table_value > span:nth-child(1) > span:nth-child(1) > span:nth-child(1)', + listingUI).append(' ➤ ' + + highestBuyOrderPrice + + ''); + + logConsole('============================') + logConsole(JSON.stringify(listing)); + logConsole(game_name + ': ' + asset.name); + logConsole('Current price: ' + price / 100.0); + + // Calculate two prices here, one without the offset and one with the offset. + // The price without the offset is required to not relist the item constantly when you have the lowest price (i.e., with a negative offset). + // The price with the offset should be used for relisting so it will still apply the user-set offset. + + var sellPriceWithoutOffset = calculateSellPriceBeforeFees(history, + histogram, + false, + priceInfo.minPriceBeforeFees, + priceInfo.maxPriceBeforeFees); + var sellPriceWithOffset = calculateSellPriceBeforeFees(history, + histogram, + true, + priceInfo.minPriceBeforeFees, + priceInfo.maxPriceBeforeFees); + + var sellPriceWithoutOffsetWithFees = market.getPriceIncludingFees(sellPriceWithoutOffset); + + logConsole('Calculated price: ' + + sellPriceWithoutOffsetWithFees / 100.0 + + ' (' + + sellPriceWithoutOffset / 100.0 + + ')'); + + listingUI.addClass('price_' + sellPriceWithOffset); + + $('.market_listing_my_price', listingUI).last().prop('title', + 'The best price is ' + (sellPriceWithoutOffsetWithFees / 100.0) + currencySymbol + '.'); + + if (sellPriceWithoutOffsetWithFees < price) { + logConsole('Sell price is too high.'); + + $('.market_listing_my_price', listingUI).last() + .css('background', COLOR_PRICE_EXPENSIVE); + listingUI.addClass('overpriced'); + + if (getSettingWithDefault(SETTING_RELIST_AUTOMATICALLY) == 1) { + queueOverpricedItemListing(listing.listingid); + } + } else if (sellPriceWithoutOffsetWithFees > price) { + logConsole('Sell price is too low.'); + + $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_CHEAP); + listingUI.addClass('underpriced'); + } else { + logConsole('Sell price is fair.'); + + $('.market_listing_my_price', listingUI).last().css('background', COLOR_PRICE_FAIR); + listingUI.addClass('fair'); + } + + return callback(true, cachedHistory && cachedListings); + }); + }); + } + + var marketOverpricedQueue = async.queue(function(item, next) { + marketOverpricedQueueWorker(item, + false, + function(success) { + if (success) { + setTimeout(function() { + next(); + }, + getRandomInt(1000, 1500)); + } else { + setTimeout(function() { + marketOverpricedQueueWorker(item, + true, + function(success) { + next(); // Go to the next queue item, regardless of success. + }); + }, + getRandomInt(30000, 45000)); + } + }); + }, + 1); + + function marketOverpricedQueueWorker(item, ignoreErrors, callback) { + var listingUI = getListingFromLists(item.listing).elm; + + market.removeListing(item.listing, + function(errorRemove, data) { + if (!errorRemove) { + $('.actual_content', listingUI).css('background', COLOR_PENDING); + + setTimeout(function() { + var baseUrl = $('.header_notification_items').first().attr('href') + 'json/'; + var itemName = $('.market_listing_item_name_link', listingUI).first().attr('href'); + var marketHashNameIndex = itemName.lastIndexOf('/') + 1; + var marketHashName = itemName.substring(marketHashNameIndex); + var decodedMarketHashName = decodeURIComponent(itemName.substring(marketHashNameIndex)); + var newAssetId = -1; + + unsafeWindow.RequestFullInventory(baseUrl + item.appid + "/" + item.contextid + "/", {}, null, null, function(transport) { + if (transport.responseJSON && transport.responseJSON.success) { + var inventory = transport.responseJSON.rgInventory; + + for (var child in inventory) { + if (marketListingsRelistedAssets.indexOf(child) == -1 && inventory[child].appid == item.appid && (inventory[child].market_hash_name == decodedMarketHashName || inventory[child].market_hash_name == marketHashName)) { + newAssetId = child; + break; + } + } + + if (newAssetId == -1) { + $('.actual_content', listingUI).css('background', COLOR_ERROR); + return callback(false); + } + + item.assetid = newAssetId; + marketListingsRelistedAssets.push(newAssetId); + + market.sellItem(item, + item.sellPrice, + function(errorSell) { + if (!errorSell) { + $('.actual_content', listingUI).css('background', COLOR_SUCCESS); + + setTimeout(function() { + removeListingFromLists(item.listing) + }, 3000); + + return callback(true); + } else { + $('.actual_content', listingUI).css('background', COLOR_ERROR); + return callback(false); + } + }); + + } else { + $('.actual_content', listingUI).css('background', COLOR_ERROR); + return callback(false); + } + }); + }, getRandomInt(1500, 2500)); // Wait a little to make sure the item is returned to inventory. + } else { + $('.actual_content', listingUI).css('background', COLOR_ERROR); + return callback(false); + } + }); + } + + // Queue an overpriced item listing to be relisted. + function queueOverpricedItemListing(listingid) { + var assetInfo = getAssetInfoFromListingId(listingid); + var listingUI = $(getListingFromLists(listingid).elm); + var price = -1; + + var items = $(listingUI).attr('class').split(' '); + for (var i in items) { + if (items[i].toString().includes('price_')) + price = parseInt(items[i].toString().replace('price_', '')); + } + + if (price > 0) { + marketOverpricedQueue.push({ + listing: listingid, + assetid: assetInfo.assetid, + contextid: assetInfo.contextid, + appid: assetInfo.appid, + sellPrice: price + }); + } + } + + var marketRemoveQueue = async.queue(function(listingid, next) { + marketRemoveQueueWorker(listingid, + false, + function(success) { + if (success) { + setTimeout(function() { + next(); + }, + getRandomInt(50, 100)); + } else { + setTimeout(function() { + marketRemoveQueueWorker(listingid, + true, + function(success) { + next(); // Go to the next queue item, regardless of success. + }); + }, + getRandomInt(30000, 45000)); + } + }); + }, + 10); + + function marketRemoveQueueWorker(listingid, ignoreErrors, callback) { + var listingUI = getListingFromLists(listingid).elm; + + market.removeListing(listingid, + function(errorRemove, data) { + if (!errorRemove) { + $('.actual_content', listingUI).css('background', COLOR_SUCCESS); + + setTimeout(function() { + removeListingFromLists(listingid); + + var numberOfListings = marketLists[0].size; + if (numberOfListings > 0) { + $('#my_market_selllistings_number').text((numberOfListings).toString()); + + // This seems identical to the number of sell listings. + $('#my_market_activelistings_number').text((numberOfListings).toString()); + } + }, + 3000); + + return callback(true); + } else { + $('.actual_content', listingUI).css('background', COLOR_ERROR); + + return callback(false); + } + }); + } + + var marketListingsItemsQueue = async.queue(function(listing, next) { + $.get(window.location.protocol + '//steamcommunity.com/market/mylistings?count=100&start=' + listing, + function(data) { + if (!data || !data.success) { + next(); + return; + } + + var myMarketListings = $('#tabContentsMyActiveMarketListingsRows'); + + var nodes = $.parseHTML(data.results_html); + var rows = $('.market_listing_row', nodes); + myMarketListings.append(rows); + + // g_rgAssets + unsafeWindow.MergeWithAssetArray(data.assets); // This is a method from Steam. + + next(); + }, + 'json') + .fail(function(data) { + next(); + return; + }); + }, + 1); + + marketListingsItemsQueue.drain = function() { + var myMarketListings = $('#tabContentsMyActiveMarketListingsRows'); + myMarketListings.checkboxes('range', true); + + // Sometimes the Steam API is returning duplicate entries (especially during item listing), filter these. + var seen = {}; + $('.market_listing_row', myMarketListings).each(function() { + var item_id = $(this).attr('id'); + if (seen[item_id]) + $(this).remove(); + else + seen[item_id] = true; + + // Remove listings awaiting confirmations, they are already listed separately. + if ($('.item_market_action_button', this).attr('href').toLowerCase() + .includes('CancelMarketListingConfirmation'.toLowerCase())) + $(this).remove(); + + // Remove buy order listings, they are already listed separately. + if ($('.item_market_action_button', this).attr('href').toLowerCase() + .includes('CancelMarketBuyOrder'.toLowerCase())) + $(this).remove(); + }); + + // Now add the market checkboxes. + addMarketCheckboxes(); + + // Show the listings again, rendering is done. + $('#market_listings_spinner').remove(); + myMarketListings.show(); + + fillMarketListingsQueue(); + + injectJs(function() { + g_bMarketWindowHidden = + true; // Limits the number of requests made to steam by stopping constant polling of popular listings. + }); + }; + + + function fillMarketListingsQueue() { + $('.market_home_listing_table').each(function(e) { + + // Not for popular / new / recently sold items (bottom of page). + if ($('.my_market_header', $(this)).length == 0) + return; + + // Buy orders and listings confirmations are not grouped like the sell listings, add this so pagination works there as well. + if (!$(this).attr('id')) { + $(this).attr('id', 'market-listing-' + e); + + $(this).append('
') + $('.market_listing_row', $(this)).appendTo($('#market-listing-container-' + e)); + } else { + $(this).children().last().addClass("market_listing_see"); + } + + addMarketPagination($('.market_listing_see', this).last()); + sortMarketListings($(this), false, false, true); + }); + + var totalPriceBuyer = 0; + var totalPriceSeller = 0; + // Add the listings to the queue to be checked for the price. + for (var i = 0; i < marketLists.length; i++) { + for (var j = 0; j < marketLists[i].items.length; j++) { + var listingid = replaceNonNumbers(marketLists[i].items[j].values().market_listing_item_name); + var assetInfo = getAssetInfoFromListingId(listingid); + + if (!isNaN(assetInfo.priceBuyer)) + totalPriceBuyer += assetInfo.priceBuyer; + if (!isNaN(assetInfo.priceSeller)) + totalPriceSeller += assetInfo.priceSeller; + + marketListingsQueue.push({ + listingid, + appid: assetInfo.appid, + contextid: assetInfo.contextid, + assetid: assetInfo.assetid + }); + } + } + + $('#my_market_selllistings_number').append(', ' + (totalPriceBuyer / 100.0).toFixed(2) + currencySymbol + ' ➤ ' + (totalPriceSeller / 100.0).toFixed(2) + currencySymbol + ''); + } + + + // Gets the asset info (appid/contextid/assetid) based on a listingid. + function getAssetInfoFromListingId(listingid) { + var listing = getListingFromLists(listingid); + if (listing == null) { + return {}; + } + + var actionButton = $('.item_market_action_button', listing.elm).attr('href'); + // Market buy orders have no asset info. + if (actionButton == null || actionButton.toLowerCase().includes('cancelmarketbuyorder')) + return {}; + + var priceBuyer = getPriceFromMarketListing($('.market_listing_price > span:nth-child(1) > span:nth-child(1)', listing.elm).text()); + var priceSeller = getPriceFromMarketListing($('.market_listing_price > span:nth-child(1) > span:nth-child(3)', listing.elm).text()); + var itemIds = actionButton.split(','); + var appid = replaceNonNumbers(itemIds[2]); + var contextid = replaceNonNumbers(itemIds[3]); + var assetid = replaceNonNumbers(itemIds[4]); + return { + appid, + contextid, + assetid, + priceBuyer, + priceSeller + }; + } + + // Adds pagination and search options to the market item listings. + function addMarketPagination(market_listing_see) { + market_listing_see.addClass('list'); + + market_listing_see.before('
    '); + market_listing_see.after('
      '); + + $('.market_listing_table_header', market_listing_see.parent()) + .append(''); + + var options = { + valueNames: [ + 'market_listing_game_name', 'market_listing_item_name_link', 'market_listing_price', + 'market_listing_listed_date', { + name: 'market_listing_item_name', + attr: 'id' + } + ], + pagination: [{ + name: "paginationTop", + paginationClass: "paginationTop", + innerWindow: 100, + outerWindow: 100, + left: 100, + right: 100 + }, { + name: "paginationBottom", + paginationClass: "paginationBottom", + innerWindow: 100, + outerWindow: 100, + left: 100, + right: 100 + }], + page: parseInt(getSettingWithDefault(SETTING_MARKET_PAGE_COUNT)) + }; + + var list = new List(market_listing_see.parent().attr('id'), options); + list.on('searchComplete', updateMarketSelectAllButton); + marketLists.push(list); + } + + // Adds checkboxes to market listings. + function addMarketCheckboxes() { + $('.market_listing_row').each(function() { + // Don't add it again, one time is enough. + if ($('.market_listing_select', this).length == 0) { + $('.market_listing_cancel_button', $(this)).append('
      ' + + '' + + '
      '); + + $('.market_select_item', this).change(function(e) { + updateMarketSelectAllButton(); + }); + } + }); + } + + // Process the market listings. + function processMarketListings() { + addMarketCheckboxes(); + + if (currentPage == PAGE_MARKET) { + // Load the market listings. + var currentCount = 0; + var totalCount = 0; + + if (typeof unsafeWindow.g_oMyListings !== 'undefined' && unsafeWindow.g_oMyListings != null && unsafeWindow.g_oMyListings.m_cTotalCount != null) + totalCount = unsafeWindow.g_oMyListings.m_cTotalCount; + else { + totalCount = parseInt($('#my_market_selllistings_number').text()); + } + + if (isNaN(totalCount) || totalCount == 0) { + fillMarketListingsQueue(); + return; + } + + $('#tabContentsMyActiveMarketListingsRows').html(''); // Clear the default listings. + $('#tabContentsMyActiveMarketListingsRows').hide(); // Hide all listings until everything has been loaded. + + // Hide Steam's paging controls. + $('#tabContentsMyActiveMarketListings_ctn').hide(); + $('.market_pagesize_options').hide(); + + // Show the spinner so the user knows that something is going on. + $('.my_market_header').eq(0).append('
      ' + + spinnerBlock + + '
      Loading market listings
      ' + + '
      '); + + while (currentCount < totalCount) { + marketListingsItemsQueue.push(currentCount); + currentCount += 100; + } + } else { + // This is on a market item page. + $('.market_home_listing_table').each(function(e) { + // Not on 'x requests to buy at y,yy or lower'. + if ($('#market_buyorder_info_show_details', $(this)).length > 0) + return; + + $(this).children().last().addClass("market_listing_see"); + + addMarketPagination($('.market_listing_see', this).last()); + sortMarketListings($(this), false, false, true); + }); + + $('#tabContentsMyActiveMarketListingsRows > .market_listing_row').each(function() { + var listingid = $(this).attr('id').replace('mylisting_', '').replace('mybuyorder_', '').replace('mbuyorder_', ''); + var assetInfo = getAssetInfoFromListingId(listingid); + + // There's only one item in the g_rgAssets on a market listing page. + var existingAsset = null; + for (var appid in unsafeWindow.g_rgAssets) { + for (var contextid in unsafeWindow.g_rgAssets[appid]) { + for (var assetid in unsafeWindow.g_rgAssets[appid][contextid]) { + existingAsset = unsafeWindow.g_rgAssets[appid][contextid][assetid]; + break; + } + } + } + + // appid and contextid are identical, only the assetid is different for each asset. + unsafeWindow.g_rgAssets[appid][contextid][assetInfo.assetid] = existingAsset; + marketListingsQueue.push({ + listingid, + appid: assetInfo.appid, + contextid: assetInfo.contextid, + assetid: assetInfo.assetid + }); + }) + } + } + + // Update the select/deselect all button on the market. + function updateMarketSelectAllButton() { + $('.market_listing_buttons').each(function() { + var selectionGroup = $(this).parent().parent(); + var invert = $('.market_select_item:checked', selectionGroup).length == $('.market_select_item', selectionGroup).length; + if ($('.market_select_item', selectionGroup).length == 0) // If there are no items to select, keep it at Select all. + invert = false; + $('.select_all > span', selectionGroup).text(invert ? 'Deselect all' : 'Select all'); + }); + } + + // Sort the market listings. + function sortMarketListings(elem, isPrice, isDate, isName) { + var list = getListFromContainer(elem); + if (list == null) { + console.log('Invalid parameter, could not find a list matching elem.'); + return; + } + + // Change sort order (asc/desc). + var nextSort = isPrice ? 1 : (isDate ? 2 : 3); + var asc = true; + + // (Re)set the asc/desc arrows. + const arrow_down = '🡻'; + const arrow_up = '🡹'; + + $('.market_listing_table_header > span', elem).each(function() { + if ($(this).hasClass('market_listing_edit_buttons')) + return; + + if ($(this).text().includes(arrow_up)) + asc = false; + + $(this).text($(this).text().replace(' ' + arrow_down, '').replace(' ' + arrow_up, '')); + }) + + var market_listing_selector; + if (isPrice) { + market_listing_selector = $('.market_listing_table_header', elem).children().eq(1); + } else if (isDate) { + market_listing_selector = $('.market_listing_table_header', elem).children().eq(2); + } else if (isName) { + market_listing_selector = $('.market_listing_table_header', elem).children().eq(3); + } + market_listing_selector.text(market_listing_selector.text() + ' ' + (asc ? arrow_up : arrow_down)); + + if (list.sort == null) + return; + + if (isName) { + list.sort('', { + order: asc ? "asc" : "desc", + sortFunction: function(a, b) { + if (a.values().market_listing_game_name.toLowerCase() + .localeCompare(b.values().market_listing_game_name.toLowerCase()) == + 0) { + return a.values().market_listing_item_name_link.toLowerCase() + .localeCompare(b.values().market_listing_item_name_link.toLowerCase()); + } + return a.values().market_listing_game_name.toLowerCase() + .localeCompare(b.values().market_listing_game_name.toLowerCase()); + } + }); + } else if (isDate) { + var currentMonth = DateTime.local().month; + + list.sort('market_listing_listed_date', { + order: asc ? "asc" : "desc", + sortFunction: function(a, b) { + var firstDate = DateTime.fromString((a.values().market_listing_listed_date).trim(), 'd MMM'); + var secondDate = DateTime.fromString((b.values().market_listing_listed_date).trim(), 'd MMM'); + + if (firstDate == null || secondDate == null) { + return 0; + } + + if (firstDate.month > currentMonth) + firstDate = firstDate.plus({ years: -1}); + if (secondDate.month > currentMonth) + secondDate = secondDate.plus({ years: -1}); + + if (firstDate > secondDate) + return 1; + if (firstDate === secondDate) + return 0; + return -1; + } + }) + } else if (isPrice) { + list.sort('market_listing_price', { + order: asc ? "asc" : "desc", + sortFunction: function(a, b) { + var listingPriceA = $(a.values().market_listing_price).text(); + listingPriceA = listingPriceA.substr(0, listingPriceA.indexOf('(')); + listingPriceA = listingPriceA.replace('--', '00'); + + var listingPriceB = $(b.values().market_listing_price).text(); + listingPriceB = listingPriceB.substr(0, listingPriceB.indexOf('(')); + listingPriceB = listingPriceB.replace('--', '00'); + + var firstPrice = parseInt(replaceNonNumbers(listingPriceA)); + var secondPrice = parseInt(replaceNonNumbers(listingPriceB)); + + return firstPrice - secondPrice; + } + }) + } + } + + function getListFromContainer(group) { + for (var i = 0; i < marketLists.length; i++) { + if (group.attr('id') == $(marketLists[i].listContainer).attr('id')) + return marketLists[i]; + } + } + + function getListingFromLists(listingid) { + // Sometimes listing ids are contained in multiple lists (?), use the last one available as this is the one we're most likely interested in. + for (var i = marketLists.length - 1; i >= 0; i--) { + var values = marketLists[i].get("market_listing_item_name", 'mylisting_' + listingid + '_name'); + if (values != null && values.length > 0) { + return values[0]; + } + + values = marketLists[i].get("market_listing_item_name", 'mbuyorder_' + listingid + '_name'); + if (values != null && values.length > 0) { + return values[0]; + } + } + + + } + + function removeListingFromLists(listingid) { + for (var i = 0; i < marketLists.length; i++) { + marketLists[i].remove("market_listing_item_name", 'mylisting_' + listingid + '_name'); + marketLists[i].remove("market_listing_item_name", 'mbuyorder_' + listingid + '_name'); + } + } + + // Initialize the market UI. + function initializeMarketUI() { + // Sell orders. + $('.my_market_header').first().append( + ''); + + // Listings confirmations and buy orders. + $('.my_market_header').slice(1).append( + ''); + + $('.market_listing_table_header').on('click', 'span', function() { + if ($(this).hasClass('market_listing_edit_buttons') || $(this).hasClass('item_market_action_button_contents')) + return; + + var isPrice = $('.market_listing_table_header', $(this).parent().parent()).children().eq(1).text() == $(this).text(); + var isDate = $('.market_listing_table_header', $(this).parent().parent()).children().eq(2).text() == $(this).text(); + var isName = $('.market_listing_table_header', $(this).parent().parent()).children().eq(3).text() == $(this).text(); + + sortMarketListings($(this).parent().parent(), isPrice, isDate, isName); + }); + + $('.select_all').on('click', '*', function() { + var selectionGroup = $(this).parent().parent().parent().parent(); + var marketList = getListFromContainer(selectionGroup); + + var invert = $('.market_select_item:checked', selectionGroup).length == $('.market_select_item', selectionGroup).length; + + for (var i = 0; i < marketList.matchingItems.length; i++) { + $('.market_select_item', marketList.matchingItems[i].elm).prop('checked', !invert); + } + + updateMarketSelectAllButton(); + }); + + + $('#market_removelisting_dialog_accept').on('click', '*', function() { + // This is when a user removed an item through the Remove/Cancel button. + // Ideally, it should remove this item from the list (instead of just the UI element which Steam does), but I'm not sure how to get the current item yet. + window.location.reload(); + }); + + $('.select_overpriced').on('click', '*', function() { + var selectionGroup = $(this).parent().parent().parent().parent(); + var marketList = getListFromContainer(selectionGroup); + + for (var i = 0; i < marketList.matchingItems.length; i++) { + if ($(marketList.matchingItems[i].elm).hasClass('overpriced')) { + $('.market_select_item', marketList.matchingItems[i].elm).prop('checked', true); + } + } + + $('.market_listing_row', selectionGroup).each(function(index) { + if ($(this).hasClass('overpriced')) + $('.market_select_item', $(this)).prop('checked', true); + }); + + updateMarketSelectAllButton(); + }); + + $('.remove_selected').on('click', '*', function() { + var selectionGroup = $(this).parent().parent().parent().parent(); + var marketList = getListFromContainer(selectionGroup); + + for (var i = 0; i < marketList.matchingItems.length; i++) { + if ($('.market_select_item', $(marketList.matchingItems[i].elm)).prop('checked')) { + var listingid = replaceNonNumbers(marketList.matchingItems[i].values().market_listing_item_name); + marketRemoveQueue.push(listingid); + } + } + }); + + $('.market_relist_auto').change(function() { + setSetting(SETTING_RELIST_AUTOMATICALLY, $('.market_relist_auto').is(":checked") ? 1 : 0); + }); + + $('.relist_overpriced').on('click', '*', function() { + var selectionGroup = $(this).parent().parent().parent().parent(); + var marketList = getListFromContainer(selectionGroup); + + for (var i = 0; i < marketList.matchingItems.length; i++) { + if ($(marketList.matchingItems[i].elm).hasClass('overpriced')) { + var listingid = replaceNonNumbers(marketList.matchingItems[i].values().market_listing_item_name); + queueOverpricedItemListing(listingid); + } + } + }); + + $('.relist_selected').on('click', '*', function() { + var selectionGroup = $(this).parent().parent().parent().parent(); + var marketList = getListFromContainer(selectionGroup); + + for (var i = 0; i < marketList.matchingItems.length; i++) { + if ($(marketList.matchingItems[i].elm).hasClass('overpriced') && $('.market_select_item', $(marketList.matchingItems[i].elm)).prop('checked')) { + var listingid = replaceNonNumbers(marketList.matchingItems[i].values().market_listing_item_name); + queueOverpricedItemListing(listingid); + } + } + }); + + $('#see_settings').remove(); + $('#global_action_menu').prepend('⬖ Steam Economy Enhancer'); + $('#see_settings').on('click', '*', () => openSettings()); + + processMarketListings(); + } + } + //#endregion + + //#region Tradeoffers + if (currentPage == PAGE_TRADEOFFER) { + // Gets the trade offer's inventory items from the active inventory. + function getTradeOfferInventoryItems() { + var arr = []; + + for (var child in getActiveInventory().rgChildInventories) { + for (var key in getActiveInventory().rgChildInventories[child].rgInventory) { + var value = getActiveInventory().rgChildInventories[child].rgInventory[key]; + if (typeof value === 'object') { + // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. + Object.assign(value, value.description); + // Includes the id of the inventory item. + value['id'] = key; + arr.push(value); + } + } + } + + // Some inventories (e.g. BattleBlock Theater) do not have child inventories, they have just one. + for (var key in getActiveInventory().rgInventory) { + var value = getActiveInventory().rgInventory[key]; + if (typeof value === 'object') { + // Merges the description in the normal object, this is done to keep the layout consistent with the market page, which is also flattened. + Object.assign(value, value.description); + // Includes the id of the inventory item. + value['id'] = key; + arr.push(value); + } + } + + return arr; + } + + function sumTradeOfferAssets(assets, user) { + var total = {}; + var totalPrice = 0; + for (var i = 0; i < assets.length; i++) { + var rgItem = user.findAsset(assets[i].appid, assets[i].contextid, assets[i].assetid); + + var text = ''; + if (rgItem != null) { + if (rgItem.element) { + var inventoryPriceElements = $('.inventory_item_price', rgItem.element); + if (inventoryPriceElements.length) { + var firstPriceElement = inventoryPriceElements[0]; + var classes = $(firstPriceElement).attr('class').split(' '); + for (var c in classes) { + if (classes[c].toString().includes('price_')) { + var price = parseInt(classes[c].toString().replace('price_', '')); + totalPrice += price; + } + } + + } + } + + if (rgItem.original_amount != null && rgItem.amount != null) { + var originalAmount = parseInt(rgItem.original_amount); + var currentAmount = parseInt(rgItem.amount); + var usedAmount = originalAmount - currentAmount; + text += usedAmount.toString() + 'x '; + } + + text += rgItem.name; + + if (rgItem.type != null && rgItem.type.length > 0) { + text += ' (' + rgItem.type + ')'; + } + } else + text = 'Unknown Item'; + + if (text in total) + total[text] = total[text] + 1; + else + total[text] = 1; + } + + var sortable = []; + for (var item in total) + sortable.push([item, total[item]]) + + sortable.sort(function(a, b) { + return a[1] - b[1]; + }).reverse(); + + var totalText = 'Number of items: ' + sortable.length + ', worth ' + (totalPrice / 100).toFixed(2) + currencySymbol + '

      '; + + for (var i = 0; i < sortable.length; i++) { + totalText += sortable[i][1] + 'x ' + sortable[i][0] + '
      '; + } + + return totalText; + } + } + + + var lastTradeOfferSum = 0; + + function hasLoadedAllTradeOfferItems() { + for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.them.assets.length; i++) { + var asset = UserThem.findAsset(unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].assetid); + if (asset == null) + return false; + } + for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.me.assets.length; i++) { + var asset = UserYou.findAsset(unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].assetid); + if (asset == null) + return false; + } + return true; + + } + + function initializeTradeOfferUI() { + var updateInventoryPrices = function() { + if (getSettingWithDefault(SETTING_TRADEOFFER_PRICE_LABELS) == 1) { + setInventoryPrices(getTradeOfferInventoryItems()); + } + }; + + var updateInventoryPricesInTrade = function() { + var items = []; + for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.them.assets.length; i++) { + var asset = UserThem.findAsset(unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.them.assets[i].assetid); + items.push(asset); + } + for (var i = 0; i < unsafeWindow.g_rgCurrentTradeStatus.me.assets.length; i++) { + var asset = UserYou.findAsset(unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].appid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].contextid, unsafeWindow.g_rgCurrentTradeStatus.me.assets[i].assetid); + items.push(asset); + } + setInventoryPrices(items); + }; + + $('.trade_right > div > div > div > .trade_item_box').observe('childlist subtree', function(record) { + if (!hasLoadedAllTradeOfferItems()) + return; + + var currentTradeOfferSum = unsafeWindow.g_rgCurrentTradeStatus.me.assets.length + unsafeWindow.g_rgCurrentTradeStatus.them.assets.length; + if (lastTradeOfferSum != currentTradeOfferSum) { + updateInventoryPricesInTrade(); + } + + lastTradeOfferSum = currentTradeOfferSum; + + $('#trade_offer_your_sum').remove(); + $('#trade_offer_their_sum').remove(); + + var your_sum = sumTradeOfferAssets(unsafeWindow.g_rgCurrentTradeStatus.me.assets, UserYou); + var their_sum = sumTradeOfferAssets(unsafeWindow.g_rgCurrentTradeStatus.them.assets, UserThem); + + $('div.offerheader:nth-child(1) > div:nth-child(3)').append('
      ' + your_sum + '
      '); + $('div.offerheader:nth-child(3) > div:nth-child(3)').append('
      ' + their_sum + '
      '); + }); + + + // Load after the inventory is loaded. + updateInventoryPrices(); + + $('#inventory_pagecontrols').observe('childlist', + '*', + function(record) { + updateInventoryPrices(); + }); + + + // This only works with a new trade offer. + if (!window.location.href.includes('tradeoffer/new')) + return; + + $('#inventory_displaycontrols').append( + '
      ' + + ''); + + $('.select_all').on('click', '*', function() { + $('.inventory_ctn:visible > .inventory_page:visible > .itemHolder:visible').delayedEach(250, function(i, it) { + var item = it.rgItem; + if (item.is_stackable) + return; + + if (!item.tradable) + return; + + unsafeWindow.MoveItemToTrade(it); + }); + }); + } + //#endregion + + //#region Settings + function openSettings() { + var price_options = $('
      ' + + '
      ' + + 'Calculate prices as the: ' + + '
      ' + + '
      ' + + '
      ' + + 'Hours to use for the average history calculated price: ' + + '
      ' + + '
      ' + + 'The value to add to the calculated price (minimum and maximum are respected): ' + + '
      ' + + '
      ' + + '
      ' + + 'Use the second lowest sell listing when the lowest sell listing has a low quantity: ' + + '
      ' + + '
      ' + + '
      ' + + 'Don\'t check market listings with prices of and below: ' + + '
      ' + + '
      ' + + '
      ' + + 'Show price labels in inventory: ' + + '
      ' + + '
      ' + + 'Show price labels in trade offers: ' + + '
      ' + + '
      ' + + '
      ' + + 'Minimum:  ' + + 'and maximum:  price for normal cards' + + '
      ' + + '
      ' + + '
      ' + + 'Minimum:  ' + + 'and maximum:  price for foil cards' + + '
      ' + + '
      ' + + '
      ' + + 'Minimum:  ' + + 'and maximum:  price for other items' + + '
      ' + + '
      ' + + '
      ' + + 'Market items per page: ' + + '
      ' + + '
      ' + + 'Automatically relist overpriced market listings (slow on large inventories): ' + + '' + + '
      ' + + '
      ' + + '
      '); + + var dialog = unsafeWindow.ShowConfirmDialog('Steam Economy Enhancer', price_options).done(function() { + setSetting(SETTING_MIN_NORMAL_PRICE, $('#' + SETTING_MIN_NORMAL_PRICE, price_options).val()); + setSetting(SETTING_MAX_NORMAL_PRICE, $('#' + SETTING_MAX_NORMAL_PRICE, price_options).val()); + setSetting(SETTING_MIN_FOIL_PRICE, $('#' + SETTING_MIN_FOIL_PRICE, price_options).val()); + setSetting(SETTING_MAX_FOIL_PRICE, $('#' + SETTING_MAX_FOIL_PRICE, price_options).val()); + setSetting(SETTING_MIN_MISC_PRICE, $('#' + SETTING_MIN_MISC_PRICE, price_options).val()); + setSetting(SETTING_MAX_MISC_PRICE, $('#' + SETTING_MAX_MISC_PRICE, price_options).val()); + setSetting(SETTING_PRICE_OFFSET, $('#' + SETTING_PRICE_OFFSET, price_options).val()); + setSetting(SETTING_PRICE_MIN_CHECK_PRICE, $('#' + SETTING_PRICE_MIN_CHECK_PRICE, price_options).val()); + setSetting(SETTING_PRICE_ALGORITHM, $('#' + SETTING_PRICE_ALGORITHM, price_options).val()); + setSetting(SETTING_PRICE_IGNORE_LOWEST_Q, $('#' + SETTING_PRICE_IGNORE_LOWEST_Q, price_options).prop('checked') ? 1 : 0); + setSetting(SETTING_PRICE_HISTORY_HOURS, $('#' + SETTING_PRICE_HISTORY_HOURS, price_options).val()); + setSetting(SETTING_MARKET_PAGE_COUNT, $('#' + SETTING_MARKET_PAGE_COUNT, price_options).val()); + setSetting(SETTING_RELIST_AUTOMATICALLY, $('#' + SETTING_RELIST_AUTOMATICALLY, price_options).prop('checked') ? 1 : 0); + setSetting(SETTING_INVENTORY_PRICE_LABELS, $('#' + SETTING_INVENTORY_PRICE_LABELS, price_options).prop('checked') ? 1 : 0); + setSetting(SETTING_TRADEOFFER_PRICE_LABELS, $('#' + SETTING_TRADEOFFER_PRICE_LABELS, price_options).prop('checked') ? 1 : 0); + + window.location.reload(); + }); + } + //#endregion + + //#region UI + injectCss('.ui-selected { outline: 2px dashed #FFFFFF; } ' + + '#logger { color: #767676; font-size: 12px;margin-top:16px; max-height: 200px; overflow-y: auto; }' + + '.trade_offer_sum { color: #767676; font-size: 12px;margin-top:8px; }' + + '.trade_offer_buttons { margin-top: 12px; }' + + '.market_commodity_orders_table { font-size:12px; font-family: "Motiva Sans", Sans-serif; font-weight: 300; }' + + '.market_commodity_orders_table th { padding-left: 10px; }' + + '#listings_group { display: flex; justify-content: space-between; margin-bottom: 8px; }' + + '#listings_sell { text-align: right; color: #589328; font-weight:600; }' + + '#listings_buy { text-align: right; color: #589328; font-weight:600; }' + + '.market_listing_my_price { height: 50px; padding-right:6px; }' + + '.market_listing_edit_buttons.actual_content { width:276px; transition-property: background-color, border-color; transition-timing-function: linear; transition-duration: 0.5s;}' + + '.market_listing_buttons { margin-top: 6px; background: rgba(0, 0, 0, 0.4); padding: 5px 0px 1px 0px; }' + + '.market_listing_button { margin-right: 4px; }' + + '.market_listing_button_right { float:right; }' + + '.market_listing_button:first-child { margin-left: 4px; }' + + '.market_listing_label_right { float:right; font-size:12px; margin-top:1px; }' + + '.market_listing_select { position: absolute; top: 16px;right: 10px; display: flex; }' + + '#market_listing_relist { vertical-align: middle; position: relative; bottom: -1px; right: 2px; }' + + '.pick_and_sell_button > a { vertical-align: middle; }' + + '.market_relist_auto { margin-bottom: 8px; }' + + '.market_relist_auto_label { margin-right: 6px; }' + + '.quick_sell { margin-right: 4px; }' + + '.spinner{margin:10px auto;width:50px;height:40px;text-align:center;font-size:10px;}.spinner > div{background-color:#ccc;height:100%;width:6px;display:inline-block;-webkit-animation:sk-stretchdelay 1.2s infinite ease-in-out;animation:sk-stretchdelay 1.2s infinite ease-in-out}.spinner .rect2{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner .rect3{-webkit-animation-delay:-1s;animation-delay:-1s}.spinner .rect4{-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner .rect5{-webkit-animation-delay:-.8s;animation-delay:-.8s}@-webkit-keyframes sk-stretchdelay{0%,40%,100%{-webkit-transform:scaleY(0.4)}20%{-webkit-transform:scaleY(1.0)}}@keyframes sk-stretchdelay{0%,40%,100%{transform:scaleY(0.4);-webkit-transform:scaleY(0.4)}20%{transform:scaleY(1.0);-webkit-transform:scaleY(1.0)}}' + + '#market_name_search { float: right; background: rgba(0, 0, 0, 0.25); color: white; border: none;height: 25px; padding-left: 6px;}' + + '.price_option_price { width: 100px }' + + '#see_settings { background: #26566c; margin-right: 10px; height: 24px; line-height:24px; display:inline-block; padding: 0px 6px; }' + + '.inventory_item_price { top: 0px;position: absolute;right: 0;background: #3571a5;padding: 2px;color: white; font-size:11px; border: 1px solid #666666;}' + + '.separator-large {display:inline-block;width:6px;}' + + '.separator-small {display:inline-block;width:1px;}' + + '.separator-btn-right {margin-right:12px;}' + + '.pagination { padding-left: 0px; }' + + '.pagination li { display:inline-block; padding: 5px 10px;background: rgba(255, 255, 255, 0.10); margin-right: 6px; border: 1px solid #666666; }' + + '.pagination li.active { background: rgba(255, 255, 255, 0.25); }'); + + $(document).ready(function() { + // Make sure the user is logged in, there's not much we can do otherwise. + if (!isLoggedIn) { + return; + } + + if (currentPage == PAGE_INVENTORY) { + initializeInventoryUI(); + } + + if (currentPage == PAGE_MARKET || currentPage == PAGE_MARKET_LISTING) { + initializeMarketUI(); + } + + if (currentPage == PAGE_TRADEOFFER) { + initializeTradeOfferUI(); + } + }); + + function injectCss(css) { + var head, style; + head = document.getElementsByTagName('head')[0]; + if (!head) { + return; + } + style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = css; + head.appendChild(style); + } + + function injectJs(js) { + var script = document.createElement('script'); + script.setAttribute("type", "application/javascript"); + script.textContent = '(' + js + ')();'; + document.body.appendChild(script); + document.body.removeChild(script); + } + + $.fn.delayedEach = function(timeout, callback, continuous) { + var $els, iterator; + + $els = this; + iterator = function(index) { + var cur; + + if (index >= $els.length) { + if (!continuous) { + return; + } + index = 0; + } + + cur = $els[index]; + callback.call(cur, index, cur); + + setTimeout(function() { + iterator(++index); + }, timeout); + }; + + iterator(0); + }; + + String.prototype.replaceAll = function(search, replacement) { + var target = this; + return target.replace(new RegExp(search, 'g'), replacement); + }; + //#endregion +})(jQuery, async); diff --git a/System.Application.SteamTools.Client.Desktop/Scripts/Steam历史最低价格查询.js b/System.Application.SteamTools.Client.Desktop/Scripts/Steam历史最低价格查询.js new file mode 100644 index 000000000..c0bbe4390 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop/Scripts/Steam历史最低价格查询.js @@ -0,0 +1,319 @@ +// ==UserScript== +// @name Steam历史最低价格查询 +// @namespace SteamHistoryLowestPrice +// @description 在steam商店页显示当前app历史最低价格及进包次数(此脚本需要勾选Steam商店加速才会生效) +// @include https://store.steampowered.com/app/* +// @include https://store.steampowered.com/bundle/* +// @include https://store.steampowered.com/sub/* +// @author 软妹币玩家、byzod +// @license GPL version 3 or any later version +// @version 1.1 +// @grant GM_xmlhttpRequest +// @enable true +// jshint esversion:6 +// ==/UserScript== + +// 显示样式 +// 0 = 显示在购买按钮上面 +// 1 = 显示在购买信息框上面 +const INFO_STYLE = 1; + +// 货币区域覆盖,两个字母的国家代号,大小写均可 +// 空字符("")代表不覆盖,使用steam的cookie中steamCountry的值 +// 见 https://zh.wikipedia.org/wiki/ISO_3166-1 或 https://en.wikipedia.org/wiki/ISO_3166-1 +// 常用 美国USD:"us", 中国CNY: "cn", 英国GBP: "uk", 日本JPY: "jp", 俄国RUS: "ru" +const CC_OVERRIDE = ""; + +// 货币符号 +const CURRENCY_SYMBOLS = { + 'AED': 'DH', + 'AUD': 'A$', + 'BRL': 'R$', + 'CAD': 'CDN$', + 'CHF': 'CHF', + 'CLP': 'CLP$', + 'CNY': '¥', // Chines Yuan + 'COP': 'COL$', + 'CRC': '₡', // Costa Rican Colón + 'EUR': '€', // Euro + 'GBP': '£', // British Pound Sterling + 'HKD': 'HK$', + 'IDR': 'Rp', + 'ILS': '₪', // Israeli New Sheqel + 'INR': '₹', // Indian Rupee + 'JPY': '¥', // Japanese Yen + 'KRW': '₩', // South Korean Won + 'MXN': 'Mex$', + 'MYR': 'RM', + 'NGN': '₦', // Nigerian Naira + 'NOK': 'kr', + 'NZD': 'NZ$', + 'PEN': 'S/.', + 'PHP': '₱', // Philippine Peso + 'PLN': 'zł', // Polish Zloty + 'PYG': '₲', // Paraguayan Guarani + 'RUB': 'pуб', + 'SAR': 'SR', + 'SGD': 'S$', + 'THB': '฿', // Thai Baht + 'TRY': 'TL', + 'TWD': 'NT$', + 'UAH': '₴', // Ukrainian Hryvnia + 'USD': '$', // US Dollar + 'VND': '₫', // Vietnamese Dong + 'ZAR': 'R ', +}; + +// 查询历史低价包括的商店 +const STORES = [ + "steam", + // "amazonus", + // "impulse", + // "gamersgate", + // "direct2drive", + // "origin", + // "uplay", + // "indiegalastore", + // "gamesplanet", + // "indiegamestand", + // "gog", + // "nuuvem", + // "dlgamer", + // "humblestore", + // "squenix", + // "bundlestars", + // "fireflower", + // "humblewidgets", + // "newegg", + // "coinplay", + // "wingamestore", + // "macgamestore", + // "gamebillet", + // "silagames", + // "itchio", + // "gamejolt", + // "paradox" +]; + + +// 在app页和愿望单页显示史低价格 +let urlMatch = location.href.match(/(app|sub|bundle)\/(\d+)/); +let appId = ""; +let type = ""; +let subIds = []; +let bundleids = []; +if (urlMatch && urlMatch.length == 3) { + type = urlMatch[1] + appId = urlMatch[2]; +} + +// console.log('[史低]: ' + type + ' : ' + appId + ', requesting data info...'); // DEBUG + +// 获取subs +document.querySelectorAll("input[name=subid]") + .forEach(sub => subIds.push(sub.value)); +// 获取bundles +document.querySelectorAll("input[name=bundleid]") + .forEach(sub => bundleids.push(sub.value)); + +let cc = "cn"; +if (CC_OVERRIDE.length > 0) { + // 使用覆盖的货币区域 + cc = CC_OVERRIDE; +} else { + // 使用默认的的货币区域 + let ccMatch = document.cookie.match(/steamCountry=([a-z]{2})/i); + if (ccMatch !== null && ccMatch.length == 2) { + cc = ccMatch[1]; + } +} + +AddLowestPriceTag(appId, type, subIds, bundleids, STORES.join(","), cc, location.protocol); + +// 在商店页添加史低信息 +async function AddLowestPriceTag(appId, type = "app", subIds = [], bundleids = [], stores = "steam", cc = "cn", protocol = "https") { + // 史低信息容器们 + let lowestPriceNodes = {}; + + // 统计subid + let findSubIds = []; + if (type == "bundle") { + // bundle就一个, 视作subid + findSubIds.push(appId); + } else if (type == "app" || type == "sub") { + // app/sub/bundle 可能有好多 + findSubIds = subIds.slice(); + if (bundleids.length > 0) { + findSubIds.push.apply(findSubIds, bundleids); + } + } + + // 寻找每一个subid的购买按钮,生成史低信息容器们 + findSubIds.forEach(subId => { + let gameWrapper = null; + try { + gameWrapper = document.querySelector('.game_area_purchase_game input[value="' + subId + '"]'); + switch (INFO_STYLE) { + case 0: + gameWrapper = gameWrapper.parentNode.parentNode.querySelector('.game_purchase_action'); + break; + case 1: + gameWrapper = gameWrapper.parentNode.parentNode; + break; + } + } catch (ex) { + gameWrapper = null; + } + if (gameWrapper) { + let lowestInfo = document.createElement("div"); + lowestInfo.className = "game_lowest_price"; + lowestInfo.innerText = "正在读取历史最低价格..."; + switch (INFO_STYLE) { + case 0: + gameWrapper.prepend(lowestInfo); + break; + case 1: + gameWrapper.append(lowestInfo); + break; + } + lowestPriceNodes[subId] = lowestInfo; + } + }); + + // 获取sub们的数据 + let data = null; + try { + data = await GettingSteamDBAppInfo(appId, type, subIds, stores, cc, protocol); + if ((typeof data == 'string')) + data = JSON.parse(data); + if (data.result == 'success') { + data = data.data; + } else { + console.log('[史低]: ' + err); + } + } catch (err) { + console.log('[史低]: ' + err); + } + + // console.log('[史低]: app ' + appId + ' data : %o', data); // DEBUG + + // 解析data + let appInfos = []; + let metaInfo = data ? data[".meta"] : null; + // 如果是bundle, 除了.meta外只有一个bundle/xxx,否则是一大堆xxx + if (type == "bundle") { + data = data.data; + appInfos.push({ Id: appId, Info: data["bundle/" + appId] }); + } else if (type == "app" || type == "sub") { + data = data.data; + for (let key in data) { + let appid = key.replace(new RegExp('(app|sub|bundle)/'), ""); + if (!isNaN(appid)) { + appInfos.push({ Id: appid, Info: data[key] }); + } + } + } + // console.log('[史低]: app ' + appId + ' metaInfo: %o', metaInfo); // DEBUG + // console.log('[史低]: app ' + appId + ' appInfos: %o', appInfos); // DEBUG + + // 如果查到info,塞到购买按钮上面去 + if (metaInfo && appInfos.length > 0) { + // 获取整体信息 + let currencySymbol = metaInfo.currency in CURRENCY_SYMBOLS + ? CURRENCY_SYMBOLS[metaInfo.currency] + : metaInfo.currency; + + // 为每一个sub或bundle添加史低 + appInfos.forEach(app => { + let lowestInfo = lowestPriceNodes[app.Id]; + + if (lowestInfo) { + lowestInfo.innerHTML = + // 历史最低 + '历史最低价是 ' + + '' + + new Date(app.Info.lowest.recorded * 1e3).toLocaleDateString() + + ' 时在 ' + + ' ' + + app.Info.lowest.store + + ' 中的 ' + + '-' + app.Info.lowest.cut + '%' + + ' ' + + currencySymbol + ' ' + app.Info.lowest.price + + '' + + // 第二行 + + '
      ' + + // 进包次数 + + '进包  0 + ? ' style="color:#0d0" title="' + app.Info.bundles.live.length + ' 个正在销售的慈善包"' + : '') + + '> ' + + app.Info.bundles.count + + '  次,' + + // 当前最低 + + (app.Info.price.price <= app.Info.lowest.price + ? '当前为历史最低价,' + : '当前最低价是') + + '在 ' + + ' ' + + app.Info.price.store + + ' 中的 ' + + '-' + app.Info.price.cut + '%' + + ' ' + + currencySymbol + ' ' + app.Info.price.price + + ''; + } + }); + } else { + // metaInfo为空,或者appInfos无内容 + console.log('[史低]: get lowest price failed, data = %o', data); + for (let id in lowestPriceNodes) { + lowestPriceNodes[id].innerHTML = ""; + } + } + + // 返回史低info + return Promise.resolve(lowestPriceNodes); +} + +// 获取史低信息 +async function GettingSteamDBAppInfo(appId, type = "app", subIds = [], stores = "steam", cc = "cn", protocol = "https") { + let requestPromise = null; + let bundleId = ""; + + if (type == "bundle") { + bundleId = appId; + } else if (type == "app" || type == "sub") { + bundleId = bundleids.join(','); + } + if (!isNaN(appId) && parseInt(appId) > 0) { + let requestUrl = protocol + "//esapi.isthereanydeal.com/v01/prices/?" + + "bundleids=" + bundleId + + "&subids=" + subIds.join(',') + + "&appids=" + appId + + "&stores=" + stores + + "&cc=" + cc + + "&coupon=true"; + // console.log('[史低]: requestUrl: ' + requestUrl); // DEBUG + requestPromise = new Promise((resolve, reject) => { + GM_xmlhttpRequest({ + method: "GET", + url: requestUrl, + onload: function (response) { + resolve(response.response); + }, + onerror: function (error) { + reject(error); + } + }); + }); + } else { + requestPromise = Promise.reject("Invalid appid"); + } + + return requestPromise; +} \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop/Scripts/Steam自动探索队列.js b/System.Application.SteamTools.Client.Desktop/Scripts/Steam自动探索队列.js new file mode 100644 index 000000000..507290c83 --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop/Scripts/Steam自动探索队列.js @@ -0,0 +1,59 @@ +// ==UserScript== +// @name Steam自动探索队列 +// @namespace http://keylol.com/t157861-1-1 +// @version 0.2 +// @description Steam节庆活动用脚本,自动探索3次队列。(此脚本需要勾选Steam商店加速才会生效) +// @author baodongsun +// @match https://store.steampowered.com/ +// @grant SteamCN +// ==/UserScript== + +(function _exec() { + var appids, + running = true, + queueNumber, + progressDialog = ShowAlertDialog('探索中', $J('
      ').append($J('
      ', { 'class': 'waiting_dialog_throbber' })).append($J('
      ', { 'id': 'progressContainer' }).text('获取进度...')), '停止').done(abort); + function abort() { + running = false; + progressDialog.Dismiss(); + } + function retry() { + abort(); + ShowConfirmDialog('错误', '是否重试?', '重试', '放弃').done(_exec) + } + function clearApp() { + if (!running) + return; + showProgress(); + var appid = appids.shift(); + !appid ? generateQueue() : $J.post(appids.length ? '/app/' + appid : '/explore/next/', { sessionid: g_sessionID, appid_to_clear_from_queue: appid }).done(clearApp).fail(retry); + } + function generateQueue() { + running && $J.post('/explore/generatenewdiscoveryqueue', { sessionid: g_sessionID, queuetype: 0 }).done(beginQueue).fail(retry); + } + function beginQueue() { + if (!running) + return; + $J.get('/explore/').done(function (htmlText) { + var cardInfo = htmlText.match(/
      \D+(\d)\D+<\/div>/); + if (!cardInfo) { + abort(); + ShowAlertDialog('完成', '已完成全部3轮探索队列'); + return; + } + var matchedAppids = htmlText.match(/0,\s+(\[.*\])/); + if (!matchedAppids) { + retry(); + return; + } + appids = JSON.parse(matchedAppids[1]); + queueNumber = cardInfo[1]; + appids.length == 0 ? generateQueue() : clearApp(); + showProgress(); + }) + } + function showProgress() { + $J('#progressContainer').html('
      剩余' + queueNumber + '个待探索队列, 当前队列剩余' + appids.length + '个待探索游戏'); + } + beginQueue(); +}()) \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop/Scripts/Steam解锁区域限制.js b/System.Application.SteamTools.Client.Desktop/Scripts/Steam解锁区域限制.js new file mode 100644 index 000000000..d82a4b29e --- /dev/null +++ b/System.Application.SteamTools.Client.Desktop/Scripts/Steam解锁区域限制.js @@ -0,0 +1,397 @@ +// ==UserScript== +// @name Steam解锁区域限制 +// @namespace http://tampermonkey.net/ +// @version 1.0.3 +// @description Bypass some kind of region restrictions.(该脚本未适配完成,暂时无法使用) +// @author CharRun +// @grant GM_xmlhttpRequest +// @run-at document-body +// @connect steampowered.com +// @include https://store.steampowered.com/app/* +// @include https://store.steampowered.com/bundle/* +// @include https://store.steampowered.com/sub/* +// @include https://store.steampowered.com/search/* +// @include https://store.steampowered.com/agecheck/* +// ==/UserScript== + +(()=>{ +let accountID = 0; +let currentAppID = 0; +let currentStoreType = null; +let currentCountry = null; + +let conf = { + autoCheck:true, + advancedSearch:false, + defaultCountry:"美国", + presetCountriesList:{ + "中国":"cn", + "美国":"us", + "加拿大":"ca", + "俄罗斯":"ru", + "澳大利亚":"au", + "英国":"gb", + "新加坡":"sg", + "法国":"fr", + "香港":"hk", + "印度":"in", + "日本":"jp", + "韩国":"kr" + } +}; + +let temp = {}; +let blockingWaitDialog; + +function init() { + conf = JSON.parse(localStorage.getItem("STEAMCC_CONF")) || conf; + if (typeof g_AccountID == "undefined" || typeof g_oSuggestParams == "undefined") { + setTimeout(init,500); + return; + } + accountID = g_AccountID; + currentCountry = g_oSuggestParams ? g_oSuggestParams.cc.toLowerCase() : null; +} + + +function autoCheck() { + if( location.pathname.match( /\/(app|sub|bundle)\/([0-9]{1,7})/ ) ){ + let result = location.pathname.match( /\/(app|sub|bundle)\/([0-9]{1,7})/ ); + currentStoreType = result[1]; + currentAppID = result[2]; + if((document.getElementById("error_box")||document.getElementById("agecheck_form")||document.getElementById("app_agegate")) && conf.autoCheck){ + if( cookieMIssCheck() ){ + blockingWaitDialog = ShowBlockingWaitDialog("检测到访问限制", `${conf.defaultCountry} 区域匿名数据获取中...`); + anonymousAccess(conf.defaultCountry); + }else{ + blockingWaitDialog = ShowBlockingWaitDialog("验证信息缺失", "设置关键Cookies中..."); + setCookie(); + } + } + }else if( /search\//ig.test(location.pathname) ){ + currentStoreType = "search"; + if( conf.advancedSearch ) { + blockingWaitDialog = ShowBlockingWaitDialog("跨区搜索", `${conf.defaultCountry} 区域搜索结果获取中...`); + anonymousAccess(conf.defaultCountry); + } + }else{ + //Not supported yet + } +} + + + +function cookieMIssCheck() { + let cookies = cookieParse(); + let wants_mature_content = cookies.wants_mature_content ? cookies.wants_mature_content == "1" ? true : false : false; + let mature_content = cookies.mature_content ? cookies.mature_content == "1" ? true : false : false; + let birthtime = cookies.birthtime ? cookies.birthtime <= "978278400" ? true : false : false; + let lastagecheckage = cookies.lastagecheckage ? true : false; + if(wants_mature_content&&mature_content&&birthtime&&lastagecheckage) return true; + return false; +} + +function cookieParse() { + let cookieString = document.cookie; + let cookieArray = cookieString.split(";"); + let cookies = {}; + for(let i = 0 , l = cookieArray.length; i < l ; i++){ + let cookie = cookieArray[i].trim(); + if(cookie == "") continue; + let c = cookie.split("="); + cookies[c[0]] = c[1]; + } + return cookies; +} + +function setCookie() { + let date = new Date(); + date.setTime(date.getTime() + 1000 * 3600 * 24 * 365); + let expires = `expires=${date.toUTCString()};`; + let path = "path=/;"; + document.cookie= "wants_mature_content=1;" + path + expires; + document.cookie= "mature_content=1;" + path + expires; + document.cookie= "lastagecheckage=1-January-1970;" + path + expires; + document.cookie= "birthtime=-729000000;" + path + expires; + blockingWaitDialog = blockingWaitDialog && blockingWaitDialog.Dismiss(); + location.href = location.href.replace( /\/agecheck/, "" ); +} + + +function anonymousAccess(region) { + region = region ? conf.presetCountriesList[region] ? conf.presetCountriesList[region] : region : "us"; + if(currentCountry == region){ + blockingWaitDialog = blockingWaitDialog && blockingWaitDialog.Dismiss(); + ShowConfirmDialog("访问暂停","访问区域与当前账户区域一致","继续访问","返回").done( result => { + result && access(region); + } ); + return; + }else{ + access(region); + } + + function access(region) { + blockingWaitDialog = blockingWaitDialog || ShowBlockingWaitDialog("匿名访问中", `${region} 区域匿名数据获取中...`); + + let url = `${location.protocol + "//store.steampowered.com" + location.pathname.replace( /\/agecheck/, "" ) + (location.search == "" ? "?" : (location.search + "&"))}cc=${region}`; + + GM_xmlhttpRequest({ + method:"GET", + url:url, + timeout:5000, + headers:{ + Cookie:"wants_mature_content=1;mature_content=1;lastagecheckage=1-January-1970;birthtime=0" + }, + onload: res => ifarme(res,region), + ontimeout:timeout, + onerror:fail + }); + + function timeout () { + blockingWaitDialog = blockingWaitDialog && blockingWaitDialog.Dismiss(); + ShowConfirmDialog("访问超时","数据获取失败...","再试一次","取消").done( result => { + result && access(region); + }); + } + + function fail() { + blockingWaitDialog = blockingWaitDialog && blockingWaitDialog.Dismiss(); + ShowConfirmDialog("访问失败","数据获取失败...","再试一次","取消").done( result => { + result && access(region); + }); + } + } +} + + +function ifarme(res,region) { + if(/id="error_box"/ig.exec(res.response)){ + blockingWaitDialog = blockingWaitDialog && blockingWaitDialog.Dismiss(); + ShowConfirmDialog("访问失败","访问区域受限制( 如访问区域无限制请反馈 )","确认","反馈"); + return; + }else if( res.finalUrl == "https://store.steampowered.com/"){ + blockingWaitDialog = blockingWaitDialog && blockingWaitDialog.Dismiss(); + ShowConfirmDialog("访问失败","被重定向为主页( 如访问区域无限制请反馈 )","确认","反馈"); + return; + } + + let virtualDom = document.createElement("html"); + virtualDom.innerHTML = res.response; + document.querySelector("title").innerHTML = virtualDom.querySelector("title").innerHTML; + virtualDom.querySelector("head").innerHTML += ``; + let hint = `
      脚本页面  
      此页面为SteamCC脚本生成
      `; + virtualDom.querySelector("body").innerHTML = hint + virtualDom.querySelector(".responsive_page_template_content").innerHTML; + let rNode = document.getElementsByClassName("responsive_page_template_content")[0]; + + if(accountID || currentStoreType == "search"){ + htmlModify(rNode,virtualDom,region); + } + let content = `${virtualDom.innerHTML}`; + + var iframe = document.createElement("iframe"); + iframe.id = "anonymousAccessIframe"; + iframe.style.width = "100%"; + iframe.style.height = "0"; + iframe.style.border = "none"; + iframe.style.overflow = "hidden"; + iframe.scrolling = "no"; + iframe.frameborder = "0"; + + rNode.innerHTML = ""; + rNode.appendChild(iframe); + + let frameWin = iframe; + if( iframe.contentWindow ){ + frameWin = iframe.contentWindow; + } + + frameWin.document.open(); + frameWin.document.writeln(content); + frameWin.document.close(); + frameWin.GM_xmlhttpRequest = GM_xmlhttpRequest; + frameWin.onload = function(){ + if( currentStoreType == "search" ){ + // fix spa histroy, BUG: can't forward; + FillFormFromNavigation = frameWin.FillFormFromNavigation; + OnLocationChange = frameWin.OnLocationChange; + } + blockingWaitDialog = blockingWaitDialog && blockingWaitDialog.Dismiss(); + function resize() { + iframe.style.height = "auto"; // for get actual height + iframe.style.height = frameWin.document.documentElement.scrollHeight + "px"; + } + window.onresize = resize; + let t = setInterval(() => {iframe.style.height = frameWin.document.documentElement.scrollHeight + "px";},600); + setTimeout(()=>clearInterval(t),6000); + virtualDom = content = null; + }; +} + +function htmlModify(rNode,virtualDom,region){ + temp.foryou_tab = rNode.querySelector("#foryou_tab") ? rNode.querySelector("#foryou_tab").innerHTML : temp.foryou_tab; + temp.foryou_flyout = rNode.querySelector("#foryou_flyout") ? rNode.querySelector("#foryou_flyout").innerHTML : temp.foryou_flyout; + temp.foryou_tab && (virtualDom.querySelector("#foryou_tab").innerHTML = temp.foryou_tab); + temp.foryou_flyout && (virtualDom.querySelector("#foryou_flyout").innerHTML = temp.foryou_flyout); + + switch (currentStoreType){ + case "app": + if( document.querySelectorAll(".queue_actions_ctn a").length || (temp.block && temp.game_meta_data) ){ + temp.block = document.querySelector(".block .game_background_glow") ? document.querySelector(".block .game_background_glow").parentNode.innerHTML : temp.block; + temp.game_meta_data = document.querySelector("#responsive_apppage_details_left_ctn") ? document.querySelector("#responsive_apppage_details_left_ctn").parentNode.innerHTML : temp.game_meta_data; + virtualDom.querySelector(".block .game_background_glow").parentNode.innerHTML = temp.block; + virtualDom.querySelector("#responsive_apppage_details_left_ctn").parentNode.innerHTML = temp.game_meta_data; + + }else { + let wish_list_innerHTML = ""; + //add queue + wish_list_innerHTML += `查看您的队列   `; + + //wishlist + if( currentAppID in GDynamicStore.s_rgOwnedApps ){ + const ID_64 = `7656${accountID + 1197960265728}`; + let appName = virtualDom.querySelector(".apphub_AppName").innerText; + let parentNode = virtualDom.querySelector(".block .game_background_glow").parentNode; + let ownApp = document.createElement("div"); + let playStats = document.createElement("div"); + ownApp.className = "game_area_already_owned page_content"; + playStats.className = "game_area_play_stats"; + ownApp.innerHTML = `
      已在库中  
      您的Steam库中已有《${appName}》
      `; + playStats.innerHTML = `
      `; + parentNode.appendChild(ownApp); + parentNode.appendChild(playStats); + }else{ + const ID_64 = `7656${accountID + 1197960265728}`; + let temp = ""; + if( currentAppID in GDynamicStore.s_rgWishlist ){ + temp += ``; + }else{ + temp += ``; + } + temp += ``; + wish_list_innerHTML += temp; + } + //follow + wish_list_innerHTML += `
      关注
      `; + //ignore + wish_list_innerHTML += `
      忽略
      已忽略
      `; + virtualDom.querySelector(".queue_actions_ctn").innerHTML = wish_list_innerHTML; + + let temp = virtualDom.querySelector(".block.responsive_apppage_details_right.recommendation_noinfo"); + temp.parentNode.removeChild(temp); + } + //restart script + virtualDom.querySelector("body").innerHTML += ``; + break; + case "bundle": + case "sub": + break; + case "search": + let td = ''; + virtualDom.querySelector("body").innerHTML += td; + break; + } +} + +function controlBar(){ + let postion = document.getElementById("global_action_menu"); + let menu = document.createElement("div"); + let style = document.createElement("style"); + style.innerHTML = "#cr-control-lable{position:relative;background-color:#171a21;color:#b8b6b4;width:150px;border:2px solid gray;border-radius:5px;}#cr-control-lable #select-box{margin:15px 0;padding:10px 0 5px 0;box-sizing:border-box;border:1px solid #171a21;border-radius:5px;min-width:100px;width:auto;display:none;position:absolute;background-color:#171a21}#cr-control-lable ul{max-height:300px;overflow:auto;margin:0;padding:0;}#cr-control-lable ul::-webkit-scrollbar{width:10px}#cr-control-lable ul::-webkit-scrollbar-thumb{background-color:#b8b6b4;border-radius:5px;}#cr-control-lable li{margin:0 15px;padding:5px 10px;list-style:none;border-bottom:1px solid #b8b6b4;}#cr-control-lable #select-box .modify{text-align:center;margin:5px 5px;border-radius:5px;}#cr-control-lable #select-box button{text-align:center;padding:0 5px;margin:0 2px;}#cr-control-lable .icon{position:absolute;width:7.5px;height:7.5px;box-sizing:border-box;border:7.5px solid;top:5px;left:15px;z-index:1;}#cr-control-lable .icon.on{border-color:#fff transparent transparent;}#cr-control-lable .icon.close{border-color:transparent transparent transparent #b8b6b4;}#cr-control-lable #cr-input{width:100%;text-align:center;padding-left:20px;color:#b8b6b4;background-color:#171a21;box-sizing:border-box;border-color:transparent;}#cr-control-lable #cr-input::placeholder{color:#b8b6b4;}#cr-control-lable #cr-input:focus::placeholder{color:#fff;}#cr-control-lable #cr-input:focus{outline:none;color:#fff;}#cr-control-lable button{outline:none;color:#b8b6b4;background-color:#171a21;border:none;text-align:center;box-sizing:border-box;padding:5 auto;}#cr-control-lable button:hover:active{color:#b8b6b4;}#cr-control-lable button:hover{color:#fff;cursor:pointer;}#cr-control-lable li:hover{color:#fff;cursor:pointer;}"; + // menu.style.display = "inline-block" + document.getElementsByTagName("head")[0].appendChild(style); + menu.innerHTML = `
        `; + postion.appendChild(menu); + + let input = document.getElementById("cr-input"); + let ul = document.getElementById("cr-select-country"); + let check = document.getElementById("cr-check"); + let setDefault = document.getElementById("cr-default"); + let modify = document.getElementById("cr-modify"); + let reset = document.getElementById("cr-reset"); + + input.value = conf.defaultCountry; + loadCountry(); + + function loadCountry(){ + ul.innerHTML = ""; + for(let i in conf.presetCountriesList){ + ul.innerHTML += `
      • ${i}
      • `; + } + } + + + ul.addEventListener("mousedown",function(e){ + if(e.target.tagName == "LI"){ + input.value = e.target.innerHTML + } + e.preventDefault(); + },true); + + input.onfocus = function(){ + document.getElementById("cr-icon").className = "icon on"; + document.getElementById("select-box").style.display = "block"; + }; + + input.onblur = function(){ + document.getElementById("cr-icon").className = "icon close"; + document.getElementById("select-box").style.display = "none"; + }; + + input.onkeypress = function(e){ + if (e.keyCode == 13 || e.which == 13){ + check.onclick(); + } + }; + + check.onclick = function(){ + let region = input.value == "" ? conf.defaultCountry : input.value; + blockingWaitDialog = ShowBlockingWaitDialog("正在跨区访问", `${region} 区域匿名数据获取中...`); + anonymousAccess(region); + }; + + setDefault.onclick = function(){ + conf.defaultCountry = input.value == "" ? conf.defaultCountry : input.value; + localStorage.setItem("STEAMCC_CONF", JSON.stringify(conf)); + }; + + modify.addEventListener("mousedown",function(e){ + ShowEditablePrompt("请按照 JSON 格式修改编辑",conf,trySave,console.log) + function trySave(res){ + try{ + res = JSON.parse(res); + if( (typeof res.autoCheck == "boolean") && (typeof res.advancedSearch == "boolean") && (typeof res.defaultCountry == "string") && (typeof res.presetCountriesList == "object")){ + conf = res; + localStorage.setItem("STEAMCC_CONF", JSON.stringify(conf)); + loadCountry(); + }else{ + ShowConfirmDialog("请勿修改JSON结构","

        └── Root

            ├── autoCheck (Boolean)

            ├── advancedSearch (Boolean)

            ├── defaultCountry (String)

            ├── presetCountriesList (Object)

        ","重新编辑","取消").done( result => { + result && ShowEditablePrompt("请按照 JSON 格式修改编辑",conf,trySave); + }); + return; + } + }catch(err){ + ShowConfirmDialog("JSON 解析出错","请按照 JSON 格式修改配置","重新编辑","取消").done( result => { + result && ShowEditablePrompt("请按照 JSON 格式修改编辑",conf,trySave); + }); + } + } + },true); + + reset.addEventListener("mousedown",function(e){ + localStorage.removeItem("STEAMCC_CONF"); + ShowConfirmDialog("已重置","刷新后生效","刷新","稍后").done( result => { + result && location.reload(); + }); + },true); +} + + +(function main(){ + init(); + autoCheck(); + window.onload = ()=>{ + controlBar(); + }; +})(); + +})() \ No newline at end of file diff --git a/System.Application.SteamTools.Client.Desktop/System.Application.SteamTools.Client.Desktop.csproj b/System.Application.SteamTools.Client.Desktop/System.Application.SteamTools.Client.Desktop.csproj index eb4d906d9..0e36f8b2d 100644 --- a/System.Application.SteamTools.Client.Desktop/System.Application.SteamTools.Client.Desktop.csproj +++ b/System.Application.SteamTools.Client.Desktop/System.Application.SteamTools.Client.Desktop.csproj @@ -18,8 +18,8 @@ - - + + @@ -48,4 +48,10 @@ PreserveNewest + + + + PreserveNewest + + \ No newline at end of file diff --git a/System.Application.SteamTools.Client/Models/OpenSourceLibrary.cs b/System.Application.SteamTools.Client/Models/OpenSourceLibrary.cs new file mode 100644 index 000000000..85dbdb854 --- /dev/null +++ b/System.Application.SteamTools.Client/Models/OpenSourceLibrary.cs @@ -0,0 +1,29 @@ +using MessagePack; + +namespace System.Application.Models +{ + [MessagePackObject(keyAsPropertyName: true)] + public sealed class OpenSourceLibrary + { + public string? Name { get; set; } + + public string? Url { get; set; } + + public string? License { get; set; } + + public string? LicenseText { get; set; } + } + +#if DEBUG + + [Obsolete("", true)] + public static partial class ProductInfo + { + [Obsolete("OpenSourceLibrary", true)] + public class Library + { + } + } + +#endif +} \ No newline at end of file diff --git a/System.Application.SteamTools.Tools.OpenSourceLibraryList/Program.cs b/System.Application.SteamTools.Tools.OpenSourceLibraryList/Program.cs new file mode 100644 index 000000000..6d174be0f --- /dev/null +++ b/System.Application.SteamTools.Tools.OpenSourceLibraryList/Program.cs @@ -0,0 +1,156 @@ +using MessagePack; +using Octokit; +using System.Application.Models; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +//https://github.com/runceel/Livet +//https://github.com/ninject/Ninject +//https://github.com/SteamDB-API/api +//https://github.com/SteamRE/Steam4NET +//https://github.com/neuecc/MessagePack-CSharp +//https://github.com/gfoidl/Base64 +//https://github.com/App-vNext/Polly +//https://github.com/xamarin/essentials + +namespace System +{ + static class Program + { + static async Task Main(string[] args) + { + var projPath = GetProjectPath(); + var readmePath = Path.Combine(projPath, README); + + var file = Path.Combine(projPath, "resources", "OpenSourceLibraryList.mpo"); + if (File.Exists(file)) return; + + using var readmeRead = File.OpenText(readmePath); + var isOpenSourceLibraryList = false; + string? line; + var list = new List(); + var github = new GitHubClient(new ProductHeaderValue("MyAmazingApp")); + + var token = Path.Combine(projPath, "github-token.pfx"); + if (File.Exists(token)) + { + token = File.ReadAllText(token); + var tokenAuth = new Credentials(token); + github.Credentials = tokenAuth; + Console.WriteLine($"Add Token: {token.FirstOrDefault()}"); + Console.WriteLine(); + } + + do + { + line = readmeRead.ReadLine(); + if (line == null) break; + if (line.Contains("##")) + { + if (isOpenSourceLibraryList) + { + break; + } + if (line.Contains("📄")) + { + isOpenSourceLibraryList = true; + } + } + if (isOpenSourceLibraryList) + { + if (!string.IsNullOrWhiteSpace(line)) + { + var arr = line.Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (arr.Length == 2 && arr[0] == "*") + { + var item = new OpenSourceLibrary + { + Name = arr[1].Substring("[", "]"), + Url = arr[1].Substring("(", ")"), + }; + const string githubUrl = "https://github.com/"; + if (item.Url.StartsWith(githubUrl, StringComparison.OrdinalIgnoreCase)) + { + arr = item.Url[githubUrl.Length..].Split("/", StringSplitOptions.RemoveEmptyEntries); + if (arr.Length == 2) + { + var owner = arr[0]; + var repo = arr[1]; + try + { + Console.WriteLine(item.Url); + Console.WriteLine(); + var contents = await github.Repository.GetLicenseContents(owner, repo); + item.License = contents.License.SpdxId; + item.LicenseText = contents.License.Key; + } + catch (NotFoundException) + { + } + } + } + list.Add(item); + } + } + } + } + while (true); + + var licenses = list.Where(x => !string.IsNullOrWhiteSpace(x.LicenseText)).Select(x => x.LicenseText).Distinct(); + var licenses_ = new Dictionary(); + foreach (var license in licenses) + { + if (license == null || license == "other") continue; + Console.WriteLine($"license: {license}"); + Console.WriteLine(); + try + { + var value = await github.Miscellaneous.GetLicense(license); + licenses_.Add(license, value.Body); + } + catch (NotFoundException) + { + licenses_.Add(license, null); + } + } + + foreach (var item in list) + { + if (string.IsNullOrWhiteSpace(item.LicenseText)) + { + item.LicenseText = null; + } + else if (licenses_.ContainsKey(item.LicenseText)) + { + item.LicenseText = licenses_[item.LicenseText]; + } + else + { + item.LicenseText = null; + } + } + + var bytes = MessagePackSerializer.Serialize(list); + File.WriteAllBytes(file, bytes); + + Console.WriteLine("OK"); + Console.ReadLine(); + } + + const string README = "README.md"; + + static string GetProjectPath(string? path = null) + { + path ??= AppContext.BaseDirectory; + if (!Directory.GetFiles(path, README).Any()) + { + var parent = Directory.GetParent(path); + if (parent == null) return string.Empty; + return GetProjectPath(parent.FullName); + } + return path; + } + } +} \ No newline at end of file diff --git a/System.Application.SteamTools.Tools.OpenSourceLibraryList/System.Application.SteamTools.Tools.OpenSourceLibraryList.csproj b/System.Application.SteamTools.Tools.OpenSourceLibraryList/System.Application.SteamTools.Tools.OpenSourceLibraryList.csproj new file mode 100644 index 000000000..4ec6a9f79 --- /dev/null +++ b/System.Application.SteamTools.Tools.OpenSourceLibraryList/System.Application.SteamTools.Tools.OpenSourceLibraryList.csproj @@ -0,0 +1,31 @@ + + + + Exe + net5.0 + System + Steam++.OpenSourceLibraryListGenerate + ..\System.Application.SteamTools.Client.Desktop.Avalonia.App\Assets\Icon.ico + + + + + Properties\AssemblyInfo.cs + + + Properties\ThisAssembly.cs + + + Application\Models\OpenSourceLibrary.cs + + + Extensions\StringExtensions.cs + + + + + + + + + diff --git a/System.Common.CoreLib/Extensions/StringExtensions.Enhance.cs b/System.Common.CoreLib/Extensions/StringExtensions.Enhance.cs new file mode 100644 index 000000000..b0a4f7d74 --- /dev/null +++ b/System.Common.CoreLib/Extensions/StringExtensions.Enhance.cs @@ -0,0 +1,99 @@ +namespace System.Extensions +{ + public static partial class StringExtensions + { + #region System.String 内置函数增强型扩展(用于取代原有函数) + + #region IndexOf + + /// + /// 报告指定 Unicode 字符在此字符串中的第一个匹配项的从零开始的索引。 + /// 注意:如果 value 仅为英文字符或数字符号,则使用原函数 + /// + /// + /// 要查找的 Unicode 字符。 + /// 如果找到该字符,则为 value 的从零开始的索引位置;如果未找到,则为 -1。 + public static int IndexOf_(this string str, char value) => DI.IsRunningOnMono ? str.IndexOf(value) : str.IndexOf(value, StringComparison.Ordinal); + + /// + /// 报告指定 Unicode 字符在此字符串中的第一个匹配项的从零开始的索引。 + /// 注意:如果 value 仅为英文字符或数字符号,则使用原函数 + /// + /// + /// 要查找的 Unicode 字符。 + /// 如果找到该字符,则为 value 的从零开始的索引位置;如果未找到,则为 -1。 + public static int IndexOf_Nullable(this string? str, char value) + { + if (str == null) return -1; + return IndexOf_(str, value); + } + + /// + /// 报告指定字符串在此实例中的第一个匹配项的从零开始的索引。 + /// + /// + /// 要搜寻的字符串。 + /// 如果找到该字符串,则为 value 的从零开始的索引位置;如果未找到该字符串,则为 -1。 如果 value 为 Empty,则返回值为 0。 + public static int IndexOf_(this string str, string value) => str.IndexOf(value, StringComparison.Ordinal); + + /// + /// 报告指定字符串在此实例中的第一个匹配项的从零开始的索引。 + /// + /// + /// 要搜寻的字符串。 + /// 如果找到该字符串,则为 value 的从零开始的索引位置;如果未找到该字符串,则为 -1。 如果 value 为 Empty,则返回值为 0。 + public static int IndexOf_Nullable(this string? str, string value) + { + if (str == null) return -1; + return IndexOf_(str, value); + } + + #endregion + + #region Contains + + /// + /// 返回一个值,该值指示指定的字符是否出现在此字符串中。 + /// + /// + /// 要查找的字符。 + /// 如果 value 参数在此字符串中出现,则为 true;否则为 false。 + public static bool Contains_(this string str, char value) => str.Contains(value, StringComparison.Ordinal); + + /// + /// 返回一个值,该值指示指定的字符是否出现在此字符串中。 + /// + /// + /// 要查找的字符。 + /// 如果 value 参数在此字符串中出现,则为 true;否则为 false。 + public static bool Contains_Nullable(this string? str, char value) + { + if (str == null) return false; + return Contains_(str, value); + } + + /// + /// 返回一个值,该值指示指定的子串是否出现在此字符串中。 + /// + /// + /// 要搜寻的字符串。 + /// 如果 true 参数出现在此字符串中,或者 value 为空字符串 (""),则为 value;否则为 false。 + public static bool Contains_(this string str, string value) => str.Contains(value, StringComparison.Ordinal); + + /// + /// 返回一个值,该值指示指定的子串是否出现在此字符串中。 + /// + /// + /// 要搜寻的字符串。 + /// 如果 true 参数出现在此字符串中,或者 value 为空字符串 (""),则为 value;否则为 false。 + public static bool Contains_Nullable(this string? str, string value) + { + if (str == null) return false; + return Contains_(str, value); + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/System.Common.CoreLib/Extensions/StringExtensions.cs b/System.Common.CoreLib/Extensions/StringExtensions.cs index 4266c6662..1f427d492 100644 --- a/System.Common.CoreLib/Extensions/StringExtensions.cs +++ b/System.Common.CoreLib/Extensions/StringExtensions.cs @@ -10,100 +10,6 @@ namespace System { public static partial class StringExtensions { - #region System.String 内置函数增强型扩展(用于取代原有函数) - - #region IndexOf - - /// - /// 报告指定 Unicode 字符在此字符串中的第一个匹配项的从零开始的索引。 - /// 注意:如果 value 仅为英文字符或数字符号,则使用原函数 - /// - /// - /// 要查找的 Unicode 字符。 - /// 如果找到该字符,则为 value 的从零开始的索引位置;如果未找到,则为 -1。 - public static int IndexOf_(this string str, char value) => DI.IsRunningOnMono ? str.IndexOf(value) : str.IndexOf(value, StringComparison.Ordinal); - - /// - /// 报告指定 Unicode 字符在此字符串中的第一个匹配项的从零开始的索引。 - /// 注意:如果 value 仅为英文字符或数字符号,则使用原函数 - /// - /// - /// 要查找的 Unicode 字符。 - /// 如果找到该字符,则为 value 的从零开始的索引位置;如果未找到,则为 -1。 - public static int IndexOf_Nullable(this string? str, char value) - { - if (str == null) return -1; - return IndexOf_(str, value); - } - - /// - /// 报告指定字符串在此实例中的第一个匹配项的从零开始的索引。 - /// - /// - /// 要搜寻的字符串。 - /// 如果找到该字符串,则为 value 的从零开始的索引位置;如果未找到该字符串,则为 -1。 如果 value 为 Empty,则返回值为 0。 - public static int IndexOf_(this string str, string value) => str.IndexOf(value, StringComparison.Ordinal); - - /// - /// 报告指定字符串在此实例中的第一个匹配项的从零开始的索引。 - /// - /// - /// 要搜寻的字符串。 - /// 如果找到该字符串,则为 value 的从零开始的索引位置;如果未找到该字符串,则为 -1。 如果 value 为 Empty,则返回值为 0。 - public static int IndexOf_Nullable(this string? str, string value) - { - if (str == null) return -1; - return IndexOf_(str, value); - } - - #endregion - - #region Contains - - /// - /// 返回一个值,该值指示指定的字符是否出现在此字符串中。 - /// - /// - /// 要查找的字符。 - /// 如果 value 参数在此字符串中出现,则为 true;否则为 false。 - public static bool Contains_(this string str, char value) => str.Contains(value, StringComparison.Ordinal); - - /// - /// 返回一个值,该值指示指定的字符是否出现在此字符串中。 - /// - /// - /// 要查找的字符。 - /// 如果 value 参数在此字符串中出现,则为 true;否则为 false。 - public static bool Contains_Nullable(this string? str, char value) - { - if (str == null) return false; - return Contains_(str, value); - } - - /// - /// 返回一个值,该值指示指定的子串是否出现在此字符串中。 - /// - /// - /// 要搜寻的字符串。 - /// 如果 true 参数出现在此字符串中,或者 value 为空字符串 (""),则为 value;否则为 false。 - public static bool Contains_(this string str, string value) => str.Contains(value, StringComparison.Ordinal); - - /// - /// 返回一个值,该值指示指定的子串是否出现在此字符串中。 - /// - /// - /// 要搜寻的字符串。 - /// 如果 true 参数出现在此字符串中,或者 value 为空字符串 (""),则为 value;否则为 false。 - public static bool Contains_Nullable(this string? str, string value) - { - if (str == null) return false; - return Contains_(str, value); - } - - #endregion - - #endregion - /// /// 将版本号字符串转换为 对象,传入的字符串不能为 /// diff --git a/System.Common.CoreLib/System.Common.CoreLib.csproj b/System.Common.CoreLib/System.Common.CoreLib.csproj index 1f64c54fd..3a4562ad8 100644 --- a/System.Common.CoreLib/System.Common.CoreLib.csproj +++ b/System.Common.CoreLib/System.Common.CoreLib.csproj @@ -6,16 +6,16 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/resources/OpenSourceLibraryList.mpo b/resources/OpenSourceLibraryList.mpo new file mode 100644 index 000000000..0867a47db Binary files /dev/null and b/resources/OpenSourceLibraryList.mpo differ