diff --git a/AppInstallerReset.sln b/AppInstallerReset.sln index 7aa137a..d300cf8 100644 --- a/AppInstallerReset.sln +++ b/AppInstallerReset.sln @@ -69,6 +69,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IEHelper", "IEHelper\IEHelp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModernNotice", "ModernNotice\ModernNotice.csproj", "{C5587B6E-19C4-4484-AA97-5C20FBB07E43}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Launch", "Launch\Launch.csproj", "{F0288B24-7B84-42A5-9A92-2E16A012E4DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -293,6 +295,18 @@ Global {C5587B6E-19C4-4484-AA97-5C20FBB07E43}.Release|x64.Build.0 = Release|Any CPU {C5587B6E-19C4-4484-AA97-5C20FBB07E43}.Release|x86.ActiveCfg = Release|Any CPU {C5587B6E-19C4-4484-AA97-5C20FBB07E43}.Release|x86.Build.0 = Release|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Debug|x64.Build.0 = Debug|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Debug|x86.Build.0 = Debug|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Release|Any CPU.Build.0 = Release|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Release|x64.ActiveCfg = Release|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Release|x64.Build.0 = Release|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Release|x86.ActiveCfg = Release|Any CPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AppxPackage/ManifestReader.cs b/AppxPackage/ManifestReader.cs index ed71c49..e649481 100644 --- a/AppxPackage/ManifestReader.cs +++ b/AppxPackage/ManifestReader.cs @@ -112,10 +112,10 @@ namespace AppxPackage [ClassInterface (ClassInterfaceType.AutoDual)] public class BaseInfoSectWithPRISingle: BaseInfoSection { - protected Ref m_reader = null; - protected Ref m_pri = null; - protected Ref m_usePri = false; - protected Ref m_enablePri = false; + protected Ref m_reader = new Ref (null); + protected Ref m_pri = new Ref (null); + protected Ref m_usePri = new Ref (false); + protected Ref m_enablePri = new Ref (false); public BaseInfoSectWithPRISingle (ref IntPtr hReader, ManifestReader reader, ref PriReader pri, ref bool usePri, ref bool enablePri) : base (ref hReader) { m_reader.Set (reader); @@ -577,6 +577,10 @@ namespace AppxPackage { dict [(kv.Key?.Trim () ?? "") + "_Base64"] = app.NewAtBase64 (kv.Key); } + else + { + dict [kv.Key] = app.NewAt (kv.Key, m_usePri.Value && m_enablePri.Value) ?? kv.Value; + } } dict ["AppUserModelID"] = app.UserModelID; return dict; @@ -595,7 +599,7 @@ namespace AppxPackage { var ret = new List (); if (!IsValid) return ret; - IntPtr hList = PackageReadHelper.GetCapabilitiesList (m_hReader.Value); + IntPtr hList = PackageReadHelper.GetManifestCapabilitiesList (m_hReader.Value); if (hList == IntPtr.Zero) return ret; try { @@ -622,7 +626,7 @@ namespace AppxPackage { var ret = new List (); if (!IsValid) return ret; - IntPtr hList = PackageReadHelper.GetDeviceCapabilitiesList (m_hReader.Value); + IntPtr hList = PackageReadHelper.GetManifestDeviceCapabilitiesList (m_hReader.Value); if (hList == IntPtr.Zero) return ret; try { @@ -685,7 +689,7 @@ namespace AppxPackage { var output = new List (); if (!IsValid) return output; - IntPtr hList = PackageReadHelper.GetDependencesInfoList (m_hReader); + IntPtr hList = PackageReadHelper.GetManifestDependencesInfoList (m_hReader); if (hList == IntPtr.Zero) return output; try { @@ -737,7 +741,7 @@ namespace AppxPackage var ret = new List (); try { - var dw = PackageReadHelper.GetResourcesDxFeatureLevels (m_hReader); + var dw = PackageReadHelper.GetManifestResourcesDxFeatureLevels (m_hReader); if ((dw & 0x1) != 0) ret.Add (DXFeatureLevel.Level9); if ((dw & 0x2) != 0) ret.Add (DXFeatureLevel.Level10); if ((dw & 0x4) != 0) ret.Add (DXFeatureLevel.Level11); @@ -753,7 +757,7 @@ namespace AppxPackage { var ret = new List (); if (!IsValid) return ret; - IntPtr hList = PackageReadHelper.GetResourcesLanguages (m_hReader.Value); + IntPtr hList = PackageReadHelper.GetManifestResourcesLanguages (m_hReader.Value); if (hList == IntPtr.Zero) return ret; try { @@ -772,7 +776,7 @@ namespace AppxPackage { var ret = new List (); if (!IsValid) return ret; - IntPtr hList = PackageReadHelper.GetResourcesLanguagesToLcid (m_hReader.Value); + IntPtr hList = PackageReadHelper.GetManifestResourcesLanguagesToLcid (m_hReader.Value); if (hList == IntPtr.Zero) return ret; try { @@ -791,7 +795,7 @@ namespace AppxPackage { var ret = new List (); if (!IsValid) return ret; - IntPtr hList = PackageReadHelper.GetResourcesLanguagesToLcid (m_hReader.Value); + IntPtr hList = PackageReadHelper.GetManifestResourcesLanguagesToLcid (m_hReader.Value); if (hList == IntPtr.Zero) return ret; try { @@ -826,7 +830,7 @@ namespace AppxPackage } protected string GetVersionDescription (string name) { - var ptr = PackageReadHelper.GetPackagePrerequistieSystemVersionName (m_hReader, name); + var ptr = PackageReadHelper.GetManifestPrerequistieSystemVersionName (m_hReader, name); return PackageReadHelper.GetStringAndFreeFromPkgRead (ptr) ?? ""; } public string OSMaxVersionDescription { get { return GetVersionDescription ("OSMaxVersionTested"); } } @@ -854,7 +858,7 @@ namespace AppxPackage private bool m_enablePRI = false; private PriReader m_pri = null; public IntPtr Instance => m_hReader; - public string FileRoot{ get { return Path.GetPathRoot (m_filePath); } } + public string FileRoot{ get { return Path.GetDirectoryName (m_filePath); } } private void InitPri () { m_pri?.Dispose (); @@ -916,15 +920,15 @@ namespace AppxPackage public MRDependencies Dependencies { get { return new MRDependencies (ref m_hReader); } } public void Dispose () { + var lastvalue = m_usePRI; + m_usePRI = false; + InitPri (); + m_usePRI = lastvalue; if (m_hReader != IntPtr.Zero) { PackageReadHelper.DestroyManifestReader (m_hReader); m_hReader = IntPtr.Zero; } - var lastvalue = m_usePRI; - m_usePRI = false; - InitPri (); - m_usePRI = lastvalue; } ~ManifestReader () { Dispose (); } public string FilePath @@ -985,5 +989,7 @@ namespace AppxPackage applications = Applications.BuildJSON () }; } + public static bool AddApplicationItem (string itemName) => PackageReadHelper.AddPackageApplicationItemGetName (itemName); + public static bool RemoveApplicationItem (string itemName) => PackageReadHelper.RemovePackageApplicationItemGetName (itemName); } } diff --git a/AppxPackage/PackageManager.cs b/AppxPackage/PackageManager.cs index 307f077..abf5516 100644 --- a/AppxPackage/PackageManager.cs +++ b/AppxPackage/PackageManager.cs @@ -603,5 +603,17 @@ namespace AppxPackage GC.KeepAlive (callback); } } + public static DataUtils._I_HResult ActiveApp (string appUserId) + { + uint processId; + var hr = PackageManageHelper.ActivateAppxApplication (appUserId, out processId); + return new DataUtils._I_HResult (hr); + } + public static DataUtils._I_HResult ActiveApp (string appUserId, string cmdargs) + { + uint processId; + var hr = PackageManageHelper.ActivateAppxApplicationWithArgs (appUserId, cmdargs, out processId); + return new DataUtils._I_HResult (hr); + } } } diff --git a/AppxPackage/PackageReader.cs b/AppxPackage/PackageReader.cs index 05043a0..1ad6f05 100644 --- a/AppxPackage/PackageReader.cs +++ b/AppxPackage/PackageReader.cs @@ -698,6 +698,10 @@ namespace AppxPackage { dict [(kv.Key?.Trim () ?? "") + "_Base64"] = app.NewAtBase64 (kv.Key); } + else + { + dict [kv.Key] = app.NewAt (kv.Key, m_usePri.Value && m_enablePri.Value) ?? kv.Value; + } } dict ["AppUserModelID"] = app.UserModelID; return dict; @@ -1182,5 +1186,7 @@ namespace AppxPackage applications = Applications.BuildJSON () }; } + public static bool AddApplicationItem (string itemName) => PackageReadHelper.AddPackageApplicationItemGetName (itemName); + public static bool RemoveApplicationItem (string itemName) => PackageReadHelper.RemovePackageApplicationItemGetName (itemName); } } diff --git a/AppxPackage/PkgMgrNative.cs b/AppxPackage/PkgMgrNative.cs index 2d63bd3..4a4652a 100644 --- a/AppxPackage/PkgMgrNative.cs +++ b/AppxPackage/PkgMgrNative.cs @@ -143,6 +143,8 @@ namespace NativeWrappers [DllImport (DllName, CallingConvention = CallConv, CharSet = CharSet.Unicode)] public static extern void PackageManagerFreeString (IntPtr lpString); + [DllImport (DllName, CallingConvention = CallConv, CharSet = CharSet.Unicode, ExactSpelling = true)] + public static extern HRESULT ActivateAppxApplicationWithArgs ([MarshalAs (UnmanagedType.LPWStr)] string lpAppUserId, [MarshalAs (UnmanagedType.LPWStr)] string lpArguments, out DWORD pdwProcessId); // ========== 托管辅助 ========== public static string PtrToStringAndFree (IntPtr nativePtr) diff --git a/Bridge/SysInit.cs b/Bridge/SysInit.cs index 3fde593..2e94518 100644 --- a/Bridge/SysInit.cs +++ b/Bridge/SysInit.cs @@ -13,6 +13,8 @@ using Newtonsoft.Json; using AppxPackage; using ModernNotice; using System.Threading; +using System.Threading.Tasks; +using System.Collections.Concurrent; namespace Bridge { @@ -182,8 +184,17 @@ namespace Bridge } internal static class JsAsyncRunner { - public static void Run ( - Func, JsAsyncResult> work, + private static readonly int MaxConcurrency = + Math.Min (8, Environment.ProcessorCount * 2); + + private static readonly SemaphoreSlim _semaphore = + new SemaphoreSlim (MaxConcurrency); + + private static CancellationTokenSource _cts = + new CancellationTokenSource (); + + public static Task Run ( + Func, JsAsyncResult> work, object jsSuccess, object jsFailed, object jsProgress) @@ -191,18 +202,30 @@ namespace Bridge var success = jsSuccess; var failed = jsFailed; var progress = jsProgress; + var token = _cts.Token; - System.Threading.ThreadPool.QueueUserWorkItem (_ => + return Task.Factory.StartNew (() => { + bool entered = false; + try { + _semaphore.Wait (token); + entered = true; + + token.ThrowIfCancellationRequested (); + Action reportProgress = p => { - if (progress != null) + if (!token.IsCancellationRequested && progress != null) CallJS (progress, p); }; - JsAsyncResult result = work (reportProgress); - if (result == null) return; + + var result = work (token, reportProgress); + + if (token.IsCancellationRequested || result == null) + return; + switch (result.Kind) { case JsAsyncResultKind.Success: @@ -212,41 +235,95 @@ namespace Bridge case JsAsyncResultKind.Failed: CallJS (failed, result.Value); break; - - case JsAsyncResultKind.None: - default: - break; } } + catch (OperationCanceledException) + { + // 只对“已进入执行态”的任务回调取消 + if (!entered) + return; + + var cancelObj = new + { + status = false, + canceled = true, + message = "Operation canceled", + jsontext = "" + }; + + CallJS (failed, + Newtonsoft.Json.JsonConvert.SerializeObject (cancelObj)); + } catch (Exception ex) { - // 框架级异常兜底 → failed - CallJS (jsFailed, ex.Message); + var errObj = new + { + status = false, + message = ex.Message, + jsontext = "" + }; + + CallJS (failed, + Newtonsoft.Json.JsonConvert.SerializeObject (errObj)); } - }); + finally + { + if (entered) + _semaphore.Release (); + } + }, + token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); } + private static void CallJS (object jsFunc, params object [] args) { if (jsFunc == null) return; try { object [] invokeArgs = new object [(args?.Length ?? 0) + 1]; - invokeArgs [0] = 1; + invokeArgs [0] = 1; if (args != null) for (int i = 0; i < args.Length; i++) invokeArgs [i + 1] = args [i]; + jsFunc.GetType ().InvokeMember ( "call", - System.Reflection.BindingFlags.InvokeMethod, + BindingFlags.InvokeMethod, null, jsFunc, - invokeArgs - ); + invokeArgs); } catch { } } + // 兼容旧版:只有 reportProgress 的写法 + public static Task Run ( + Func, JsAsyncResult> work, + object jsSuccess, + object jsFailed, + object jsProgress) + { + return Run ( + (token, report) => { + token.ThrowIfCancellationRequested (); + return work (report); + }, + jsSuccess, + jsFailed, + jsProgress + ); + } + + public static void CancelAll () + { + var old = _cts; + _cts = new CancellationTokenSource (); + old.Cancel (); + old.Dispose (); + } } private string BuildJsonText (object obj) { @@ -490,11 +567,17 @@ namespace Bridge null ); } + public _I_HResult ActiveApp (string appUserId, string args) { return PackageManager.ActiveApp (appUserId, args); } + public _I_HResult ActiveAppByIdentity (string idName, string appId, string args) { return ActiveApp ($"{idName?.Trim ()}!{appId?.Trim ()}", args); } + public void CancelAll () { JsAsyncRunner.CancelAll (); } } [ComVisible (true)] [ClassInterface (ClassInterfaceType.AutoDual)] - public class _I_Package + public class _I_PackageReader { + private static readonly int MaxConcurrency = Math.Min (8, Environment.ProcessorCount * 2); + private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim (MaxConcurrency); + private static CancellationTokenSource _cts = new CancellationTokenSource (); private static void CallJS (object jsFunc, params object [] args) { if (jsFunc == null) return; @@ -518,160 +601,267 @@ namespace Bridge // ignore errors in callback invocation } } - public AppxPackage.PackageReader Reader (string packagePath) { return new AppxPackage.PackageReader (packagePath); } - public _I_PackageManager Manager => new _I_PackageManager (); + private static Task RunAsync (Action work, object successCallback, object failedCallback) + { + var token = _cts.Token; + return Task.Factory.StartNew (() => { + _semaphore.Wait (token); + try + { + token.ThrowIfCancellationRequested (); + work (token); + } + catch (OperationCanceledException oce) + { + var errObj = new + { + status = false, + message = "Task is canceled. Message: " + oce.Message, + jsontext = "" + }; + string errJson = Newtonsoft.Json.JsonConvert.SerializeObject (errObj); + if (failedCallback != null) CallJS (failedCallback, errJson); + } + catch (Exception ex) + { + var errObj = new + { + status = false, + message = ex.Message, + jsontext = "" + }; + string errJson = Newtonsoft.Json.JsonConvert.SerializeObject (errObj); + if (failedCallback != null) CallJS (failedCallback, errJson); + } + finally + { + _semaphore.Release (); + } + }, + token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + } + public AppxPackage.PackageReader Package (string packagePath) { return new AppxPackage.PackageReader (packagePath); } public AppxPackage.ManifestReader Manifest (string manifestPath) { return new AppxPackage.ManifestReader (manifestPath); } public AppxPackage.ManifestReader FromInstallLocation (string installLocation) { return Manifest (Path.Combine (installLocation, "AppxManifest.xml")); } - public void ReadFromPackageAsync (string packagePath, bool enablePri, object successCallback, object failedCallback) + public Task ReadFromPackageAsync (string packagePath, bool enablePri, object successCallback, object failedCallback) { - Thread thread = new Thread (() => { - try + return RunAsync (token => { + string cacheKey = BuildCacheKey (packagePath, enablePri, ReadKind.Package); + if (TryHitCache (cacheKey, successCallback)) return; + using (var reader = Package (packagePath)) { - using (var reader = Reader (packagePath)) + if (enablePri) { - if (enablePri) + reader.EnablePri = true; + reader.UsePri = true; + } + token.ThrowIfCancellationRequested (); + if (!reader.IsValid) + { + var failObj = new { - reader.EnablePri = true; - reader.UsePri = true; - } - if (!reader.IsValid) - { - var failObj = new - { - status = false, - message = "Reader invalid", - jsontext = "" - }; - string failJson = Newtonsoft.Json.JsonConvert.SerializeObject (failObj); - if (failedCallback != null) CallJS (failedCallback, failJson); - return; - } - var obj = new - { - status = true, - message = "ok", - jsontext = reader.BuildJsonText () // 你之前写好的函数 + status = false, + message = "Reader invalid", + jsontext = "" }; - string json = Newtonsoft.Json.JsonConvert.SerializeObject (obj); - if (successCallback != null) CallJS (successCallback, json); + string failJson = Newtonsoft.Json.JsonConvert.SerializeObject (failObj); + if (failedCallback != null) CallJS (failedCallback, failJson); + return; + } + var obj = new + { + status = true, + message = "ok", + jsontext = reader.BuildJsonText () + }; + string json = Newtonsoft.Json.JsonConvert.SerializeObject (obj); + if (!token.IsCancellationRequested && successCallback != null) CallJS (successCallback, json); + SaveCache (cacheKey, json); + } + }, successCallback, failedCallback); + } + public Task ReadFromManifestAsync (string manifestPath, bool enablePri, object successCallback, object failedCallback) + { + return RunAsync (token => { + string cacheKey = BuildCacheKey (manifestPath, enablePri, ReadKind.Manifest); + if (TryHitCache (cacheKey, successCallback)) return; + using (var reader = Manifest (manifestPath)) + { + if (enablePri) + { + reader.EnablePri = true; + reader.UsePri = true; + } + token.ThrowIfCancellationRequested (); + if (!reader.IsValid) + { + var failObj = new + { + status = false, + message = "Reader invalid", + jsontext = "" + }; + string failJson = Newtonsoft.Json.JsonConvert.SerializeObject (failObj); + if (failedCallback != null) CallJS (failedCallback, failJson); + return; + } + var obj = new + { + status = true, + message = "ok", + jsontext = reader.BuildJsonText () + }; + string json = Newtonsoft.Json.JsonConvert.SerializeObject (obj); + if (!token.IsCancellationRequested && successCallback != null) CallJS (successCallback, json); + SaveCache (cacheKey, json); + } + }, successCallback, failedCallback); + } + public Task ReadFromInstallLocationAsync (string installLocation, bool enablePri, object successCallback, object failedCallback) + { + return RunAsync (token => { + var manifestpath = Path.Combine (installLocation, "AppxManifest.xml"); + string cacheKey = BuildCacheKey (manifestpath, enablePri, ReadKind.Manifest); + if (TryHitCache (cacheKey, successCallback)) return; + using (var reader = FromInstallLocation (installLocation)) + { + if (enablePri) + { + reader.EnablePri = true; + reader.UsePri = true; + } + token.ThrowIfCancellationRequested (); + if (!reader.IsValid) + { + var failObj = new + { + status = false, + message = "Reader invalid", + jsontext = "" + }; + string failJson = Newtonsoft.Json.JsonConvert.SerializeObject (failObj); + if (failedCallback != null) CallJS (failedCallback, failJson); + return; + } + var obj = new + { + status = true, + message = "ok", + jsontext = reader.BuildJsonText () + }; + string json = Newtonsoft.Json.JsonConvert.SerializeObject (obj); + if (!token.IsCancellationRequested && successCallback != null) CallJS (successCallback, json); + SaveCache (cacheKey, json); + } + }, successCallback, failedCallback); + } + public void CancelAll () + { + var old = _cts; + _cts = new CancellationTokenSource (); + old.Cancel (); + old.Dispose (); + } + public bool AddApplicationItem (string itemName) => PackageReader.AddApplicationItem (itemName); + public bool RemoveApplicationItem (string itemName) => PackageReader.RemoveApplicationItem (itemName); + // Cache about + private const int MaxCacheItems = 64; // 最大缓存数量 + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes (30); + internal sealed class CacheEntry + { + public string Json { get; private set; } + public DateTime ExpireAt { get; private set; } + public DateTime LastAccessUtc { get; private set; } + public CacheEntry (string json, DateTime expireAt) + { + Json = json; + ExpireAt = expireAt; + LastAccessUtc = DateTime.UtcNow; + } + public bool IsExpired { get { return DateTime.UtcNow > ExpireAt; } } + public void Touch () { LastAccessUtc = DateTime.UtcNow; } + } + private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary (); + private static readonly object _cacheCleanupLock = new object (); + private static bool TryHitCache (string cacheKey, object successCallback) + { + CacheEntry entry; + if (_cache.TryGetValue (cacheKey, out entry)) + { + if (!entry.IsExpired) + { + entry.Touch (); + if (successCallback != null) CallJS (successCallback, entry.Json); + return true; + } + CacheEntry removed; + _cache.TryRemove (cacheKey, out removed); + } + return false; + } + private static void SaveCache (string cacheKey, string json) + { + var entry = new CacheEntry ( + json, + DateTime.UtcNow.Add (CacheDuration) + ); + _cache [cacheKey] = entry; + EnsureCacheSize (); + } + private static void EnsureCacheSize () + { + if (_cache.Count <= MaxCacheItems) return; + lock (_cacheCleanupLock) + { + if (_cache.Count <= MaxCacheItems) return; + foreach (var kv in _cache) + { + if (kv.Value.IsExpired) + { + CacheEntry removed; + _cache.TryRemove (kv.Key, out removed); } } - catch (Exception ex) + if (_cache.Count <= MaxCacheItems) return; + while (_cache.Count > MaxCacheItems) { - var errObj = new + string oldestKey = null; + DateTime oldest = DateTime.MaxValue; + foreach (var kv in _cache) { - status = false, - message = ex.Message, - jsontext = "" - }; - string errJson = Newtonsoft.Json.JsonConvert.SerializeObject (errObj); - if (failedCallback != null) CallJS (failedCallback, errJson); - } - }); - thread.IsBackground = true; - thread.SetApartmentState (ApartmentState.MTA); - thread.Start (); - } - public void ReadFromManifestAsync (string manifestPath, bool enablePri, object successCallback, object failedCallback) - { - Thread thread = new Thread (() => { - try - { - using (var reader = Manifest (manifestPath)) - { - if (enablePri) + if (kv.Value.LastAccessUtc < oldest) { - reader.EnablePri = true; - reader.UsePri = true; + oldest = kv.Value.LastAccessUtc; + oldestKey = kv.Key; } - if (!reader.IsValid) - { - var failObj = new - { - status = false, - message = "Reader invalid", - jsontext = "" - }; - string failJson = Newtonsoft.Json.JsonConvert.SerializeObject (failObj); - if (failedCallback != null) CallJS (failedCallback, failJson); - return; - } - var obj = new - { - status = true, - message = "ok", - jsontext = reader.BuildJsonText () // 你之前写好的函数 - }; - string json = Newtonsoft.Json.JsonConvert.SerializeObject (obj); - if (successCallback != null) CallJS (successCallback, json); } + if (oldestKey == null) break; + CacheEntry removed; + _cache.TryRemove (oldestKey, out removed); } - catch (Exception ex) - { - var errObj = new - { - status = false, - message = ex.Message, - jsontext = "" - }; - string errJson = Newtonsoft.Json.JsonConvert.SerializeObject (errObj); - if (failedCallback != null) CallJS (failedCallback, errJson); - } - }); - thread.IsBackground = true; - thread.SetApartmentState (ApartmentState.MTA); - thread.Start (); + } } - public void ReadFromInstallLocationAsync (string installLocation, bool enablePri, object successCallback, object failedCallback) + private static string NormalizePath (string path) { - Thread thread = new Thread (() => { - try - { - using (var reader = FromInstallLocation (installLocation)) - { - if (enablePri) - { - reader.EnablePri = true; - reader.UsePri = true; - } - if (!reader.IsValid) - { - var failObj = new - { - status = false, - message = "Reader invalid", - jsontext = "" - }; - string failJson = Newtonsoft.Json.JsonConvert.SerializeObject (failObj); - if (failedCallback != null) CallJS (failedCallback, failJson); - return; - } - var obj = new - { - status = true, - message = "ok", - jsontext = reader.BuildJsonText () // 你之前写好的函数 - }; - string json = Newtonsoft.Json.JsonConvert.SerializeObject (obj); - if (successCallback != null) CallJS (successCallback, json); - } - } - catch (Exception ex) - { - var errObj = new - { - status = false, - message = ex.Message, - jsontext = "" - }; - string errJson = Newtonsoft.Json.JsonConvert.SerializeObject (errObj); - if (failedCallback != null) CallJS (failedCallback, errJson); - } - }); - thread.IsBackground = true; - thread.SetApartmentState (ApartmentState.MTA); - thread.Start (); + if (string.IsNullOrWhiteSpace (path)) return string.Empty; + path = path.Trim (); + while (path.EndsWith ("\\") || path.EndsWith ("/")) path = path.Substring (0, path.Length - 1); + return path.ToLowerInvariant (); } + private enum ReadKind { Package, Manifest } + private static string BuildCacheKey (string path, bool enablePri, ReadKind kind) + { + return NormalizePath (path) + "|" + enablePri.ToString () + "|" + kind.ToString (); + } + } + [ComVisible (true)] + [ClassInterface (ClassInterfaceType.AutoDual)] + public class _I_Package + { + public _I_PackageReader Reader => new _I_PackageReader (); + public _I_PackageManager Manager => new _I_PackageManager (); } [ComVisible (true)] [ClassInterface (ClassInterfaceType.AutoDual)] diff --git a/Launch/Launch.csproj b/Launch/Launch.csproj new file mode 100644 index 0000000..bd8ba08 --- /dev/null +++ b/Launch/Launch.csproj @@ -0,0 +1,90 @@ + + + + + Debug + AnyCPU + {F0288B24-7B84-42A5-9A92-2E16A012E4DE} + WinExe + Properties + Launch + Launch + v4.0 + 512 + + + x86 + true + full + false + ..\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\Release\ + TRACE + prompt + 4 + + + app.manifest + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + {bd681a4f-eb60-4bb8-90b5-65968fc7da59} + AppxPackage + + + {FFD3FD52-37A8-4F43-883C-DE8D996CB0E0} + DataUtils + + + + + \ No newline at end of file diff --git a/Launch/Program.cs b/Launch/Program.cs new file mode 100644 index 0000000..b81ecfa --- /dev/null +++ b/Launch/Program.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Forms; + +namespace Launch +{ + static class Program + { + + /// + /// 从 args[startIndex..] 生成安全的命令行字符串 + /// + private static string BuildCommandLine (string [] args, int startIndex) + { + if (args.Length <= startIndex) return null; + var sb = new StringBuilder (); + for (int i = startIndex; i < args.Length; i++) + { + if (i > startIndex) sb.Append (' '); + sb.Append (EscapeArgument (args [i])); + } + return sb.ToString (); + } + /// + /// 按 Win32 命令行规则转义单个参数 + /// + private static string EscapeArgument (string arg) + { + if (string.IsNullOrEmpty (arg)) return "\"\""; + bool needQuotes = false; + foreach (char c in arg) + { + if (char.IsWhiteSpace (c) || c == '"') + { + needQuotes = true; + break; + } + } + if (!needQuotes) return arg; + var sb = new StringBuilder (); + sb.Append ('"'); + foreach (char c in arg) + { + if (c == '"') sb.Append ("\\\""); + else sb.Append (c); + } + sb.Append ('"'); + return sb.ToString (); + } + /// + /// 应用程序的主入口点。 + /// + [STAThread] + static void Main (string [] args) + { + if (args == null || args.Length == 0) + { + MessageBox.Show ("Missing AppUserModelId.", "Launch Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + string appUserModelId = args [0]; + string argumentLine = BuildCommandLine (args, 1); + AppxPackage.PackageManager.ActiveApp (appUserModelId, string.IsNullOrEmpty (argumentLine) ? null : argumentLine); + } + } +} diff --git a/Launch/Properties/AssemblyInfo.cs b/Launch/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a601a54 --- /dev/null +++ b/Launch/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle ("Launch")] +[assembly: AssemblyDescription ("")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("Launch")] +[assembly: AssemblyCopyright ("Copyright © 2026")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] + +//将 ComVisible 设置为 false 将使此程序集中的类型 +//对 COM 组件不可见。 如果需要从 COM 访问此程序集中的类型, +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible (false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid ("f0288b24-7b84-42a5-9a92-2e16a012e4de")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值, +// 方法是按如下所示使用“*”: : +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion ("1.0.0.0")] +[assembly: AssemblyFileVersion ("1.0.0.0")] diff --git a/Launch/Properties/Resources.Designer.cs b/Launch/Properties/Resources.Designer.cs new file mode 100644 index 0000000..461edff --- /dev/null +++ b/Launch/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本: 4.0.30319.42000 +// +// 对此文件的更改可能导致不正确的行为,如果 +// 重新生成代码,则所做更改将丢失。 +// +//------------------------------------------------------------------------------ + +namespace Launch.Properties +{ + + + /// + /// 强类型资源类,用于查找本地化字符串等。 + /// + // 此类是由 StronglyTypedResourceBuilder + // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 + // 若要添加或删除成员,请编辑 .ResX 文件,然后重新运行 ResGen + // (以 /str 作为命令选项),或重新生成 VS 项目。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute ("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute ()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute ()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute ("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources () + { + } + + /// + /// 返回此类使用的缓存 ResourceManager 实例。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute (global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager ("Launch.Properties.Resources", typeof (Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 覆盖当前线程的 CurrentUICulture 属性 + /// 使用此强类型的资源类的资源查找。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute (global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/Launch/Properties/Resources.resx b/Launch/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Launch/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Launch/Properties/Settings.Designer.cs b/Launch/Properties/Settings.Designer.cs new file mode 100644 index 0000000..87685fd --- /dev/null +++ b/Launch/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Launch.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute ()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute ("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings: global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized (new Settings ()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/Launch/Properties/Settings.settings b/Launch/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/Launch/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Launch/app.manifest b/Launch/app.manifest new file mode 100644 index 0000000..e830149 --- /dev/null +++ b/Launch/app.manifest @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + diff --git a/Manager/Manager.csproj b/Manager/Manager.csproj index 98d3c77..ee33b0c 100644 --- a/Manager/Manager.csproj +++ b/Manager/Manager.csproj @@ -34,6 +34,24 @@ app.manifest + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + @@ -56,6 +74,7 @@ + ManagerShell.cs diff --git a/Manager/ManagerShell.cs b/Manager/ManagerShell.cs index 0a2aabe..16cef76 100644 --- a/Manager/ManagerShell.cs +++ b/Manager/ManagerShell.cs @@ -1,13 +1,7 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; using System.Drawing; -using System.Linq; -using System.Text; using System.Windows.Forms; using System.IO; -using System.Threading; namespace Manager { public partial class ManagerShell: WAShell.WebAppForm diff --git a/Manager/Program.cs b/Manager/Program.cs index fdd20ad..2f47838 100644 --- a/Manager/Program.cs +++ b/Manager/Program.cs @@ -15,6 +15,7 @@ namespace Manager [STAThread] static void Main () { + AppxPackage.PackageReader.AddApplicationItem ("SmallLogo"); DataUtils.BrowserEmulation.SetWebBrowserEmulation (); Application.EnableVisualStyles (); Application.SetCompatibleTextRenderingDefault (false); diff --git a/Manager/ShortcutCreateForm.cs b/Manager/ShortcutCreateForm.cs new file mode 100644 index 0000000..3cf2412 --- /dev/null +++ b/Manager/ShortcutCreateForm.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Manager +{ + class ShortcutCreateForm + { + } +} diff --git a/PriFormat.zip b/PriFormat.zip deleted file mode 100644 index 55e272a..0000000 Binary files a/PriFormat.zip and /dev/null differ diff --git a/appinstaller/main.cpp b/appinstaller/main.cpp index eb1d022..0bf901f 100644 --- a/appinstaller/main.cpp +++ b/appinstaller/main.cpp @@ -821,7 +821,8 @@ public ref class AppListWnd: public System::Windows::Forms::Form, public IScript if (color.empty () || color.equals (L"transparent")) color = MPStringToStdW (ColorToHtml (GetDwmThemeColor ())); std::wnstring displayName = app [L"DisplayName"]; if (displayName.empty ()) displayName = app [L"ShortName"]; - auto &logo = app [L"Square44x44Logo"]; + auto logo = app [L"Square44x44Logo"]; + if (std::wnstring::empty (logo)) logo = app [L"SmallLogo"]; InvokeCallScriptFunction ( "addAppToList", CStringToMPString (displayName), diff --git a/pkgmgr/pkgmgr.cpp b/pkgmgr/pkgmgr.cpp index dea8dd2..1255f1b 100644 --- a/pkgmgr/pkgmgr.cpp +++ b/pkgmgr/pkgmgr.cpp @@ -1003,3 +1003,33 @@ HRESULT WRTAppDataClearAll (HWRTAPPDATA hAppData, LPWSTR *pErrorCode, LPWSTR *pD } return E_FAIL; } +[STAThread] +HRESULT ActivateAppxApplicationWithArgs (LPCWSTR lpAppUserId, LPCWSTR lpArguments, PDWORD pdwProcessId) +{ + HRESULT hr = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED); + if (FAILED (hr) && hr != RPC_E_CHANGED_MODE) return hr; + if (!lpAppUserId) return E_INVALIDARG; + std::wstring appUserModelId (lpAppUserId); + IApplicationActivationManager *pAam = nullptr; + destruct autoRelease ([&] { + if (pAam) pAam->Release (); + }); + hr = CoCreateInstance ( + CLSID_ApplicationActivationManager, + nullptr, + CLSCTX_LOCAL_SERVER, + IID_IApplicationActivationManager, + (void **)&pAam + ); + if (FAILED (hr)) return hr; + CoAllowSetForegroundWindow (pAam, nullptr); + DWORD pid = 0; + hr = pAam->ActivateApplication ( + appUserModelId.c_str (), + lpArguments && *lpArguments ? lpArguments : nullptr, // + AO_NONE, + &pid + ); + if (SUCCEEDED (hr) && pdwProcessId) *pdwProcessId = pid; + return hr; +} diff --git a/pkgmgr/pkgmgr.h b/pkgmgr/pkgmgr.h index 3dec0ae..b458bf3 100644 --- a/pkgmgr/pkgmgr.h +++ b/pkgmgr/pkgmgr.h @@ -203,6 +203,8 @@ extern "C" PKGMGR_API HRESULT CreateAppDataManager (LPCWSTR lpFamilyName, HWRTAPPDATA *ppApplicationData, LPWSTR *pErrorCode, LPWSTR *pDetailMsg); // ӱءκʱӦݴ洢ɾӦóݡ PKGMGR_API HRESULT WRTAppDataClearAll (HWRTAPPDATA hAppData, LPWSTR *pErrorCode, LPWSTR *pDetailMsg); + + PKGMGR_API HRESULT ActivateAppxApplicationWithArgs (LPCWSTR lpAppUserId, LPCWSTR lpArguments, PDWORD pdwProcessId); #ifdef _DEFAULT_INIT_VALUE_ #undef _DEFAULT_INIT_VALUE_ #endif diff --git a/pkgread/readobj.h b/pkgread/readobj.h index 813d0f0..fe6faec 100644 --- a/pkgread/readobj.h +++ b/pkgread/readobj.h @@ -509,11 +509,137 @@ namespace appx_info class appx_apps: virtual public com_info { using Base = com_info ; + CComPtr manifest = nullptr; + protected: + mutable std::map> xml_cache; + mutable bool xml_parsed = false; + void parse_xml () const + { + if (xml_parsed || !manifest) return; + + CComPtr xmlstream; + if (FAILED (manifest->GetStream (&xmlstream)) || !xmlstream) + return; + + LARGE_INTEGER zero {}; + xmlstream->Seek (zero, STREAM_SEEK_SET, nullptr); + + CComPtr reader; + if (FAILED (CreateXmlReader (__uuidof(IXmlReader), (void**)&reader, nullptr))) + return; + + reader->SetInput (xmlstream); + + XmlNodeType nodeType; + bool inApplication = false; + std::wstring currentId; + std::map currentValues; + + while (S_OK == reader->Read (&nodeType)) + { + if (nodeType == XmlNodeType_Element) + { + LPCWSTR name = nullptr; + reader->GetLocalName (&name, nullptr); + + if (!_wcsicmp (name, L"Application")) + { + inApplication = true; + currentValues.clear (); + currentId.clear (); + + auto read_attr = [&] (const wchar_t* xmlName, const wchar_t* key) + { + if (SUCCEEDED (reader->MoveToAttributeByName (xmlName, nullptr))) + { + LPCWSTR v = nullptr; + reader->GetValue (&v, nullptr); + if (v) currentValues [key] = v; + reader->MoveToElement (); + } + }; + + read_attr (L"Id", L"ID"); + read_attr (L"Executable", L"Executable"); + read_attr (L"EntryPoint", L"EntryPoint"); + read_attr (L"StartPage", L"StartPage"); + + currentId = currentValues [L"ID"]; + } + else if (inApplication) + { + auto read_attr = [&] (const wchar_t* xmlName, const wchar_t* key) + { + if (SUCCEEDED (reader->MoveToAttributeByName (xmlName, nullptr))) + { + LPCWSTR v = nullptr; + reader->GetValue (&v, nullptr); + if (v) currentValues [key] = v; + reader->MoveToElement (); + } + }; + + if (!_wcsicmp (name, L"VisualElements")) + { + read_attr (L"DisplayName", L"DisplayName"); + read_attr (L"Description", L"Description"); + read_attr (L"BackgroundColor", L"BackgroundColor"); + read_attr (L"ForegroundText", L"ForegroundText"); + read_attr (L"Logo", L"Logo"); + read_attr (L"SmallLogo", L"SmallLogo"); + read_attr (L"Square44x44Logo", L"Square44x44Logo"); + read_attr (L"Square150x150Logo", L"Square150x150Logo"); + } + else if (!_wcsicmp (name, L"DefaultTile")) + { + read_attr (L"ShortName", L"ShortName"); + read_attr (L"WideLogo", L"WideLogo"); + read_attr (L"Wide310x150Logo", L"Wide310x150Logo"); + read_attr (L"Square310x310Logo", L"Square310x310Logo"); + read_attr (L"Square71x71Logo", L"Square71x71Logo"); + } + else if (!_wcsicmp (name, L"LockScreen")) + { + read_attr (L"BadgeLogo", L"LockScreenLogo"); + read_attr (L"Notification", L"LockScreenNotification"); + } + } + } + else if (nodeType == XmlNodeType_EndElement) + { + LPCWSTR name = nullptr; + reader->GetLocalName (&name, nullptr); + if (!_wcsicmp (name, L"Application")) + { + // fallback + if (currentValues [L"Wide310x150Logo"].empty ()) + currentValues [L"Wide310x150Logo"] = currentValues [L"WideLogo"]; + if (currentValues [L"Square70x70Logo"].empty ()) + currentValues [L"Square70x70Logo"] = currentValues [L"Square71x71Logo"]; + if (!currentId.empty ()) xml_cache [currentId] = currentValues; + inApplication = false; + currentValues.clear (); + currentId.clear (); + } + } + } + xml_parsed = true; + } + std::wstring get_value_ext (const std::wstring& appId, const std::wstring& item) const + { + parse_xml (); // ֻһ + auto it = xml_cache.find (appId); + if (it == xml_cache.end ()) return L""; + + auto jt = it->second.find (item); + return (jt != it->second.end ()) ? jt->second : L""; + } public: - using Base::Base; + appx_apps (IAppxManifestApplicationsEnumerator *ptr, IAppxManifestReader *mptr): + Base (ptr), manifest (mptr) {} size_t applications (_Out_ std::vector &output) const { - return EnumerateComInterface (pointer (), output, [] (IAppxManifestApplication *&p) -> app_info { + auto ret = EnumerateComInterface (pointer (), output, [] (IAppxManifestApplication *&p) -> app_info { raii rel ([&p] () { if (p) p->Release (); p = nullptr; @@ -544,6 +670,16 @@ namespace appx_info } return app; }); + for (auto &it : output) + { + auto id = it [L"Id"]; + for (auto &kv : it) + { + if (IsNormalizeStringEmpty (kv.second)) + kv.second = get_value_ext (id, kv.first); + } + } + return ret; } size_t app_user_model_ids (_Out_ std::vector &output) const { @@ -910,9 +1046,11 @@ class appxreader: virtual public com_info_quote HRESULT get_applications (_Outptr_ IAppxManifestApplicationsEnumerator **output) const { return get_from_manifest (&Manifest::GetApplications, output); } appx_info::appx_apps applications () const { + IAppxManifestReader *m = nullptr; IAppxManifestApplicationsEnumerator *ip = nullptr; + manifest (&m); get_applications (&ip); - return appx_info::appx_apps (ip); + return appx_info::appx_apps (ip, m); } HRESULT get_capabilities (_Outptr_ APPX_CAPABILITIES *output) const { return get_from_manifest (&Manifest::GetCapabilities, output); } HRESULT get_device_capabilities (_Outptr_ IAppxManifestDeviceCapabilitiesEnumerator **output) const { return get_from_manifest (&Manifest::GetDeviceCapabilities, output); } @@ -1377,7 +1515,7 @@ class appxmanifest: virtual public com_info_quote { IAppxManifestApplicationsEnumerator *ip = nullptr; get_applications (&ip); - return appx_info::appx_apps (ip); + return appx_info::appx_apps (ip, pointer ()); } HRESULT get_capabilities (_Outptr_ APPX_CAPABILITIES *output) const { return get_from_manifest (&Manifest::GetCapabilities, output); } HRESULT get_device_capabilities (_Outptr_ IAppxManifestDeviceCapabilitiesEnumerator **output) const { return get_from_manifest (&Manifest::GetDeviceCapabilities, output); } diff --git a/pkgread/stringres.h b/pkgread/stringres.h index 69ba372..42a6811 100644 --- a/pkgread/stringres.h +++ b/pkgread/stringres.h @@ -101,7 +101,8 @@ std::vector appitems = L"BackgroundColor", L"ForegroundText", L"ShortName", - L"Square44x44Logo" + L"Square44x44Logo", + L"SmallLogo" }; std::vector &GetApplicationAttributeItems () { return appitems; } static CriticalSection g_appcs; diff --git a/shared/html/css/statusbar.css b/shared/html/css/statusbar.css new file mode 100644 index 0000000..12d986d --- /dev/null +++ b/shared/html/css/statusbar.css @@ -0,0 +1,29 @@ +.statusbar { + overflow-x: hidden; + overflow-y: hidden; +} + +.statusbar.x { + width: 0; +} + +.statusbar.y { + height: 0; +} + +.statusbar.both { + width: 0; + height: 0; +} + +.statusbar.fast { + transition: all 0.3s cubic-bezier(0.1, 0.9, 0.2, 1); +} + +.statusbar.medium { + transition: all 0.5s cubic-bezier(0.1, 0.9, 0.2, 1); +} + +.statusbar.slow { + transition: all 0.7s cubic-bezier(0.1, 0.9, 0.2, 1); +} \ No newline at end of file diff --git a/shared/html/js/appbar.js b/shared/html/js/appbar.js index 3bf4dc3..57f5538 100644 --- a/shared/html/js/appbar.js +++ b/shared/html/js/appbar.js @@ -312,9 +312,15 @@ function hideTouchHide() { touchHide.style.display = "none"; } + var timerHide = null; this.show = function() { try { if (!_enable) return; + if (timerHide) { + clearTimeout(timerHide); + timerHide = null; + } + this.element.style.display = ""; if (!this.element.classList.contains("show")) this.element.classList.add("show"); waitTimer(500); @@ -323,8 +329,16 @@ }; this.hide = function() { try { + if (timerHide) { + clearTimeout(timerHide); + timerHide = null; + } if (this.element.classList.contains("show")) this.element.classList.remove("show"); + timerHide = setTimeout(function() { + timerHide = null; + self.element.style.display = "none"; + }, 500); waitTimer(500); hideTouchHide(); } catch (e) {} diff --git a/shared/html/js/bridge.js b/shared/html/js/bridge.js index e0e4118..f602c0b 100644 --- a/shared/html/js/bridge.js +++ b/shared/html/js/bridge.js @@ -102,4 +102,20 @@ get: function() { return ext.System.UI.SplashImage; }, }); } catch (e) {} + // 下面是有关 String 方法的补充 + if (typeof String.prototype.trim !== "function") { + String.prototype.trim = function() { + return Bridge.String.trim(this); + }; + } + if (typeof String.prototype.toLowerCase !== "function") { + String.prototype.toLowerCase = function() { + return Bridge.String.tolower(this); + }; + } + if (typeof String.prototype.toUpperCase !== "function") { + String.prototype.toUpperCase = function() { + return Bridge.String.toupper(this); + }; + } })(this); \ No newline at end of file diff --git a/shared/html/js/datasrc.js b/shared/html/js/datasrc.js index 7b242a6..24fccb3 100644 --- a/shared/html/js/datasrc.js +++ b/shared/html/js/datasrc.js @@ -13,6 +13,7 @@ var childAnimeDuration = 120; var parentAnimeDuration = 400; + function showItemAmine(node) { return Windows.UI.Animation.runAsync(node, [ Windows.UI.Animation.Keyframes.Scale.up, @@ -27,6 +28,23 @@ ], childAnimeDuration); } + function noAnime(node) { + return Promise.resolve(node); + } + + function runShowAnime(node, enable) { + if (enable === void 0) enable = true; + if (!enable) return noAnime(node); + return showItemAmine(node); + } + + function runHideAnime(node, enable) { + if (enable === void 0) enable = true; + if (!enable) return noAnime(node); + return hideItemAmine(node); + } + + function updateItemAmine(node, updateCallback) { return Windows.UI.Animation.runAsync(node, [ Windows.UI.Animation.Keyframes.Opacity.hidden, @@ -53,6 +71,43 @@ function PMDataSource() { var _list = []; var _listeners = []; + + var _keySelector = null; + var _autoKeySeed = 1; + this.setKeySelector = function(fn) { + _keySelector = (typeof fn === "function") ? fn : null; + }; + + function getKey(item) { + if (!item) return null; + + // 用户提供 + if (_keySelector) { + return _keySelector(item); + } + + // 自动注入(对象) + if (typeof item === "object") { + if (item.__pm_key !== void 0) { + return item.__pm_key; + } + + try { + Object.defineProperty(item, "__pm_key", { + value: "pm_" + (_autoKeySeed++), + enumerable: false + }); + } catch (e) { + // IE10 兜底 + item.__pm_key = "pm_" + (_autoKeySeed++); + } + return item.__pm_key; + } + + // 原始类型兜底 + return typeof item + ":" + item; + } + this.subscribe = function(fn) { if (typeof fn === "function") { _listeners.push(fn); @@ -64,26 +119,47 @@ _listeners[i](evt); } } + this.indexOf = function(item) { + var key = getKey(item); + for (var i = 0; i < _list.length; i++) { + if (getKey(_list[i]) === key) { + return i; + } + } + return -1; + }; this.add = function(item) { _list.push(item); emit(new PMChangeEvent( - DataView.ChangeType.add, [item], { index: _list.length - 1 } + DataView.ChangeType.add, [{ item: item, index: _list.length - 1, key: getKey(item) }] )); }; this.removeAt = function(index) { if (index < 0 || index >= _list.length) return; var item = _list.splice(index, 1)[0]; emit(new PMChangeEvent( - DataView.ChangeType.remove, [item], { index: index } + DataView.ChangeType.remove, [{ item: item, index: index, key: getKey(item) }] )); }; + this.remove = function(item) { + var index = this.indexOf(item); + if (index >= 0) { + this.removeAt(index); + } + }; this.changeAt = function(index, newItem) { if (index < 0 || index >= _list.length) return; _list[index] = newItem; emit(new PMChangeEvent( - DataView.ChangeType.change, [newItem], { index: index } + DataView.ChangeType.change, [{ item: newItem, index: index, key: getKey(newItem) }] )); }; + this.change = function(oldItem, newItem) { + var index = this.indexOf(oldItem); + if (index >= 0) { + this.changeAt(index, newItem); + } + }; this.clear = function() { _list.length = 0; emit(new PMChangeEvent( @@ -224,7 +300,6 @@ var changed = []; var removed = []; - // 1️⃣ 找 remove for (i = oldList.length - 1; i >= 0; i--) { var oldItem = oldList[i]; var oldKey = getKey(oldItem); @@ -238,7 +313,6 @@ } } - // 2️⃣ 找 add / change for (i = 0; i < newList.length; i++) { var newItem = newList[i]; var newKey = getKey(newItem); @@ -263,7 +337,6 @@ } } - // 3️⃣ 执行 remove(从后往前) if (removed.length > 0) { for (i = 0; i < removed.length; i++) { _list.splice(removed[i].index, 1); @@ -274,7 +347,6 @@ )); } - // 4️⃣ 执行 add / change(重建顺序) _list = newList.slice(0); if (added.length > 0) { @@ -291,25 +363,50 @@ )); } }; - + this._getKey = getKey; } + var MAX_ANIMATE_COUNT = 100; function PMDataListView(container, templateFn) { this.container = container; this.templateFn = templateFn; this.listViewControl = this; + this._emptyView = null; + // === 新增 === + this._filter = null; + + this._searchHandler = null; + this._searchText = null; + this._searchSuggestProvider = null; + + this.onSearchSuggest = null; // function(text, list) + this._isSearching = false; + this.onsearchstart = null; + this.onsearchend = null; } PMDataListView.prototype.bind = function(ds) { var self = this; - var items = ds.get(); + this._ds = ds; + self.container.innerHTML = ""; + var items = ds.get(); + // 动画队列,保证异步操作不会乱序 var queue = Promise.resolve(); function renderItem(data, index) { var el = self.templateFn(data, index); + var key = ds && ds._getKey ? ds._getKey(data) : null; + + el.__pm_item = data; + el.__pm_key = key; + + if (key != null) { + el.setAttribute("data-pm-key", key); + } + el.addEventListener("click", function() { self._toggleSelect(el); }); @@ -317,10 +414,13 @@ return el; } + // 初始化渲染 for (var i = 0; i < items.length; i++) { self.container.appendChild(renderItem(items[i], i)); } + // 初始化 emptyView 状态 + self._updateEmptyView(); ds.subscribe(function(evt) { @@ -337,37 +437,58 @@ var nodes = []; for (var i = 0; i < datas.length; i++) { var n = renderItem(datas[i].item, datas[i].index); + n.style.display = "none"; nodes.push(n); self.container.appendChild(n); } - - // 如果数量>=20,动画并行,否则串行 - if (datas.length >= 20) { + var enableAnime = datas.length <= MAX_ANIMATE_COUNT; + if (!enableAnime) { + return Promise.resolve(); + } + // 如果数量>=20,动画串行,否则并行 + if (datas.length <= 20) { var promises = []; for (var j = 0; j < nodes.length; j++) { - promises.push(showItemAmine(nodes[j])); + promises.push((function(node) { + node.style.display = ""; + return showItemAmine(node); + })(nodes[j])); } return Promise.all(promises); } else { // 串行 var p = Promise.resolve(); + var group = []; for (var k = 0; k < nodes.length; k++) { - (function(node) { - p = p.then(function() { + group.push((function(node) { + node.style.display = ""; return showItemAmine(node); - }); - })(nodes[k]); + }) + (nodes[k])); + if (group.length === 20 || k === nodes.length - 1) { + (function(g) { + p = p.then(function() { + return Promise.join(g); + }); + })(group); + group = []; + } } + (function(g) { + p = p.then(function() { + return Promise.join(g); + }); + })(group); return p; } } case DataView.ChangeType.remove: { - var node = self.container.children[evt.detail.index]; + var info = evt.datas[0]; + var node = self._findNodeByKey(info.key); if (!node) return; - // 隐藏动画完成后再移除 return hideItemAmine(node).then(function() { self.container.removeChild(node); }); @@ -375,29 +496,30 @@ case DataView.ChangeType.change: { - var oldNode = self.container.children[evt.detail.index]; + var info = evt.datas[0]; + var oldNode = self._findNodeByKey(info.key); if (!oldNode) return; - // 先淡出旧节点 return hideItemAmine(oldNode).then(function() { - // 替换节点 - var newNode = renderItem(evt.datas[0], evt.detail.index); + var newNode = renderItem(info.item); self.container.replaceChild(newNode, oldNode); - - // 再淡入新节点 return showItemAmine(newNode); }); } + case DataView.ChangeType.clear: self.container.innerHTML = ""; return Promise.resolve(); case DataView.ChangeType.move: { - var node = self.container.children[evt.detail.from]; + var info = evt.datas[0]; + var node = self._findNodeByKey(info.key); + if (!node) return; + var ref = self.container.children[evt.detail.to] || null; - if (node) self.container.insertBefore(node, ref); + self.container.insertBefore(node, ref); return Promise.resolve(); } @@ -410,9 +532,22 @@ return Promise.resolve(); } } + promises.push(self._refreshVisibility()); + return Promise.join(promises); }); }); }; + PMDataListView.prototype._findNodeByKey = function(key) { + if (key == null) return null; + + var children = this.container.children; + for (var i = 0; i < children.length; i++) { + if (children[i].__pm_key === key) { + return children[i]; + } + } + return null; + }; PMDataListView.prototype._toggleSelect = function(ele) { // 如果选择模式为 none,则不处理 @@ -472,7 +607,202 @@ return Array.prototype.slice.call(this.container.querySelectorAll(".selected")); } }); + PMDataListView.prototype._updateEmptyView = function() { + if (!this._emptyView) return; + // container 中是否还有 item + var hasItem = this.container.children.length > 0; + + if (hasItem) { + if (this._emptyView.parentNode) { + this._emptyView.style.display = "none"; + } + } else { + if (!this._emptyView.parentNode) { + this.container.appendChild(this._emptyView); + } + this._emptyView.style.display = ""; + } + }; + Object.defineProperty(PMDataListView.prototype, "emptyView", { + get: function() { + return this._emptyView; + }, + set: function(value) { + // 只接受 HTMLElement 或 null / undefined + if (value !== null && value !== void 0 && !(value instanceof HTMLElement)) { + return; + } + + // 移除旧的 + if (this._emptyView && this._emptyView.parentNode) { + this._emptyView.parentNode.removeChild(this._emptyView); + } + + this._emptyView = value || null; + + // 设置后立刻刷新一次 + this._updateEmptyView(); + } + }); + PMDataListView.prototype._isItemVisible = function(item) { + // 1️⃣ filter + if (this._filter) { + if (!this._filter(item)) return false; + } + + // 2️⃣ search(自动启用 / 禁用) + var handler = this._searchHandler; + var text = this._searchText; + + if (typeof handler === "function") { + if (text != null) { + text = ("" + text).replace(/^\s+|\s+$/g, ""); + } + + if (text && text.length > 0) { + if (!handler(text, item)) { + return false; + } + } + } + + return true; + }; + PMDataListView.prototype._refreshVisibility = function() { + var self = this; + var children = self.container.children; + var animes = []; + for (var i = 0; i < children.length; i++) { + (function(node) { + var item = node.__pm_item; + if (!item) return; + + var visible = self._isItemVisible(item); + + var enableAnime = animes.length < MAX_ANIMATE_COUNT; + if (visible) { + if (node.style.display === "none") { + node.style.display = ""; + animes.push(runShowAnime(node, enableAnime)); + } + } else { + if (node.style.display !== "none") { + // 移除选择状态 + node.classList.remove("selected"); + animes.push(runHideAnime(node, enableAnime).then(function() { + node.style.display = "none"; + })); + } + } + })(children[i]); + } + return Promise.join(animes); + }; + Object.defineProperty(PMDataListView.prototype, "filter", { + get: function() { + return this._filter; + }, + set: function(fn) { + this._filter = (typeof fn === "function") ? fn : null; + this._refreshVisibility(); + } + }); + Object.defineProperty(PMDataListView.prototype, "searchHandler", { + get: function() { + return this._searchHandler; + }, + set: function(fn) { + this._searchHandler = (typeof fn === "function") ? fn : null; + this._refreshVisibility(); + } + }); + Object.defineProperty(PMDataListView.prototype, "searchText", { + get: function() { + return this._searchText; + }, + set: function(text) { + var oldText = this._searchText; + + this._searchText = text; + + var oldActive = !!(oldText && oldText.trim()); + var newActive = !!(text && ("" + text).trim()); + + //if (!oldActive && newActive) { + this._isSearching = true; + this._emitSearchEvent("searchstart"); + //} + + var handler = this._searchHandler; + var provider = this._searchSuggestProvider; + var cb = this.onSearchSuggest; + + var t = text; + if (t != null) { + t = ("" + t).replace(/^\s+|\s+$/g, ""); + } + + // 搜索建议 + if ( + typeof handler === "function" && + t && + t.length > 0 && + typeof provider === "function" && + typeof cb === "function" + ) { + var list = provider(t); + if (list && list.length) { + cb(t, list.slice(0, 10)); + } + } + var self = this; + var func = function() { + //if (oldActive && !newActive) { + self._isSearching = false; + self._emitSearchEvent("searchend"); + //} + }; + this._refreshVisibility().done(func, func); + } + }); + Object.defineProperty(PMDataListView.prototype, "searchSuggestProvider", { + get: function() { + return this._searchSuggestProvider; + }, + set: function(fn) { + this._searchSuggestProvider = (typeof fn === "function") ? fn : null; + } + }); + Object.defineProperty(PMDataListView.prototype, "findItemLength", { + get: function() { + var count = 0; + var children = this.container.children; + for (var i = 0; i < children.length; i++) { + var item = children[i].__pm_item; + if (this._isItemVisible(item)) { + count++; + } + } + return count; + } + }); + PMDataListView.prototype._emitSearchEvent = function(type) { + if (typeof this["on" + type] === "function") { + try { + this["on" + type].call(this); + } catch (e) {} + } + + try { + var ev = document.createEvent("Event"); + ev.initEvent(type, true, true); + this.container.dispatchEvent(ev); + } catch (e) {} + }; + PMDataListView.prototype.refresh = function() { + this._refreshVisibility(); + }; global.DataView.ChangeEvent = PMChangeEvent; global.DataView.DataSource = PMDataSource; global.DataView.ListView = PMDataListView; diff --git a/shared/html/js/manager/pages.js b/shared/html/js/manager/pages.js index 6c52587..6aa8f71 100644 --- a/shared/html/js/manager/pages.js +++ b/shared/html/js/manager/pages.js @@ -1,6 +1,7 @@ (function(global) { "use strict"; var pkg_ns = external.Package; + var strres = external.StringResources; function archsToStr(archs) { var arr = []; @@ -28,9 +29,23 @@ } return arr.join(", "); } + var showAppDetailTimer = null; + + function updateAppDataSource(page, result, bar) { + try { + var json = result.json; + console.log(json); + page.appDataSource.updateList(json.applications); + } catch (e) {} + showAppDetailTimer = setTimeout(function() { + showAppDetailTimer = null; + bar.hide(); + }, 3000); + } function setAppInfoPageContent(info) { var page = document.getElementById("page-appinfo"); + page.data = info; page.querySelector(".display-name").textContent = info.Properties.DisplayName; page.querySelector(".publisher-display-name").textContent = info.Properties.Publisher; page.querySelector(".version").textContent = info.Identity.Version.Expression; @@ -42,9 +57,72 @@ page.querySelector(".identity .full-name").textContent = info.Identity.FullName; page.querySelector(".identity .architecture").textContent = archsToStr(info.Identity.ProcessArchitecture); var il = info.InstallLocation; - var pkg = pkg_ns.fromInstallLocation(il); - var json = pkg.jsonText; - console.log(JSON.parse(json)); + try { page.appDataSource.clear(); } catch (e) {} + var appLoading = page.querySelector("#appinfo-loading"); + appLoading.classList.remove("noloading"); + if (showAppDetailTimer) clearTimeout(showAppDetailTimer); + if (typeof appLoading.bar === "undefined") { + appLoading.bar = new TransitionPanel(appLoading, { + axis: 'y', + duration: 500, + }); + } + appLoading.bar.show(); + var appLoadingStatus = page.querySelector(".title"); + appLoadingStatus.textContent = strres.get("MANAGER_APP_INSTALLEDAPPS_LOADING"); + return Package.reader.readFromInstallLocation(il, true).then( + function(result) { + try { + var displayNameNode = page.querySelector(".display-name"); + displayNameNode.textContent = displayNameNode.textContent || result.json.properties.display_name; + if ((displayNameNode.textContent || "").indexOf("ms-resource:") === 0) { + displayNameNode.textContent = ""; + } + if (result.json.applications.length === 1) { + displayNameNode.textContent = displayNameNode.textContent || result.json.applications[0].DisplayName || result.json.applications[0].ShortName; + } + if ((displayNameNode.textContent || "").indexOf("ms-resource:") === 0) { + displayNameNode.textContent = ""; + } + if (result.json.applications.length === 1) { + displayNameNode.textContent = displayNameNode.textContent || result.json.applications[0].ShortName; + } + if ((displayNameNode.textContent || "").indexOf("ms-resource:") === 0) { + displayNameNode.textContent = ""; + } + displayNameNode.textContent = displayNameNode.textContent || info.Identity.FamilyName; + } catch (e) {} + appLoadingStatus.textContent = strres.get("MANAGER_APP_INSTALLEDAPPS_SUCCEED"); + appLoading.classList.add("noloading"); + updateAppDataSource(page, result, appLoading.bar); + }, + function(result) { + try { + var displayNameNode = page.querySelector(".display-name"); + displayNameNode.textContent = displayNameNode.textContent || result.json.properties.display_name; + if ((displayNameNode.textContent || "").indexOf("ms-resource:") === 0) { + displayNameNode.textContent = ""; + } + if (result.json.applications.length === 1) { + displayNameNode.textContent = displayNameNode.textContent || result.json.applications[0].DisplayName || result.json.applications[0].ShortName; + } + if ((displayNameNode.textContent || "").indexOf("ms-resource:") === 0) { + displayNameNode.textContent = ""; + } + if (result.json.applications.length === 1) { + displayNameNode.textContent = displayNameNode.textContent || result.json.applications[0].ShortName; + } + if ((displayNameNode.textContent || "").indexOf("ms-resource:") === 0) { + displayNameNode.textContent = ""; + } + displayNameNode.textContent = displayNameNode.textContent || info.Identity.FamilyName; + } catch (e) {} + var msg = result.message; + appLoadingStatus.textContent = external.String.format(strres.get("MANAGER_APP_INSTALLEDAPPS_FAILED"), msg); + appLoading.classList.add("noloading"); + updateAppDataSource(page, result, appLoading.bar); + } + ); } global.setAppInfoPageContent = setAppInfoPageContent; })(this); \ No newline at end of file diff --git a/shared/html/js/mgrinit.js b/shared/html/js/mgrinit.js index adb0ab4..d660650 100644 --- a/shared/html/js/mgrinit.js +++ b/shared/html/js/mgrinit.js @@ -1,369 +1,5 @@ (function(global) { - function _createImage(src, onload, onerror) { - var img = new Image(); - - img.onload = function() { - onload(img); - }; - - img.onerror = function() { - onerror && onerror(); - }; - - img.src = src; - } - - function getSolidOpaqueBackgroundColor(source, callback) { - - function processImage(img) { - if (!img || !img.complete) { - callback(null); - return; - } - - var canvas = document.createElement("canvas"); - var ctx = canvas.getContext("2d"); - - canvas.width = img.naturalWidth || img.width; - canvas.height = img.naturalHeight || img.height; - - ctx.drawImage(img, 0, 0); - - try { - var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - } catch (e) { - // 跨域导致的安全异常 - callback(null); - return; - } - - var data = imageData.data; - var w = canvas.width; - var h = canvas.height; - - var colors = {}; - var total = 0; - - function pushColor(r, g, b, a) { - if (a !== 255) return; - var key = r + "," + g + "," + b; - colors[key] = (colors[key] || 0) + 1; - total++; - } - - // top + bottom - for (var x = 0; x < w; x++) { - var topIndex = (0 * w + x) * 4; - var botIndex = ((h - 1) * w + x) * 4; - pushColor(data[topIndex], data[topIndex + 1], data[topIndex + 2], data[topIndex + 3]); - pushColor(data[botIndex], data[botIndex + 1], data[botIndex + 2], data[botIndex + 3]); - } - - // left + right - for (var y = 1; y < h - 1; y++) { - var leftIndex = (y * w + 0) * 4; - var rightIndex = (y * w + (w - 1)) * 4; - pushColor(data[leftIndex], data[leftIndex + 1], data[leftIndex + 2], data[leftIndex + 3]); - pushColor(data[rightIndex], data[rightIndex + 1], data[rightIndex + 2], data[rightIndex + 3]); - } - - if (total === 0) { - callback(null); - return; - } - - var bestKey = null; - var bestCount = 0; - - for (var key in colors) { - if (colors.hasOwnProperty(key)) { - if (colors[key] > bestCount) { - bestCount = colors[key]; - bestKey = key; - } - } - } - - // 95% 纯色阈值 - if (bestCount / total < 0.95) { - callback(null); - return; - } - - callback(bestKey); - } - - // 如果传入的是 img 元素 - if (source && source.tagName && source.tagName.toLowerCase() === "img") { - processImage(source); - return; - } - - // 如果传入的是 data url 或普通 url - if (typeof source === "string") { - _createImage(source, processImage, function() { - callback(null); - }); - return; - } - - callback(null); - } - - function getHamonyColor(source, callback) { - - function _createImage(src, onload, onerror) { - var img = new Image(); - img.onload = function() { onload(img); }; - img.onerror = function() { onerror && onerror(); }; - img.src = src; - } - - function _toKey(r, g, b) { - return r + "," + g + "," + b; - } - - function _rgbToHsl(r, g, b) { - r /= 255; - g /= 255; - b /= 255; - var max = Math.max(r, g, b); - var min = Math.min(r, g, b); - var h, s, l = (max + min) / 2; - - if (max === min) { - h = s = 0; - } else { - var d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - case g: - h = (b - r) / d + 2; - break; - case b: - h = (r - g) / d + 4; - break; - } - h /= 6; - } - return { h: h, s: s, l: l }; - } - - function _hslToRgb(h, s, l) { - var r, g, b; - - function hue2rgb(p, q, t) { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1 / 6) return p + (q - p) * 6 * t; - if (t < 1 / 2) return q; - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; - return p; - } - - if (s === 0) { - r = g = b = l; - } else { - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = hue2rgb(p, q, h + 1 / 3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1 / 3); - } - - return { - r: Math.round(r * 255), - g: Math.round(g * 255), - b: Math.round(b * 255) - }; - } - - function _lum(r, g, b) { - function f(x) { - x = x / 255; - return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); - } - return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); - } - - function _contrast(a, b) { - var L1 = _lum(a.r, a.g, a.b); - var L2 = _lum(b.r, b.g, b.b); - var lighter = Math.max(L1, L2); - var darker = Math.min(L1, L2); - return (lighter + 0.05) / (darker + 0.05); - } - - function _tryPureBackground(data, w, h) { - var edgeColors = {}; - var edgeTotal = 0; - - function push(r, g, b, a) { - if (a !== 255) return; - var k = _toKey(r, g, b); - edgeColors[k] = (edgeColors[k] || 0) + 1; - edgeTotal++; - } - - for (var x = 0; x < w; x++) { - var top = (0 * w + x) * 4; - var bot = ((h - 1) * w + x) * 4; - push(data[top], data[top + 1], data[top + 2], data[top + 3]); - push(data[bot], data[bot + 1], data[bot + 2], data[bot + 3]); - } - for (var y = 1; y < h - 1; y++) { - var left = (y * w + 0) * 4; - var right = (y * w + (w - 1)) * 4; - push(data[left], data[left + 1], data[left + 2], data[left + 3]); - push(data[right], data[right + 1], data[right + 2], data[right + 3]); - } - - if (edgeTotal === 0) return null; - - var best = null, - bestCount = 0; - for (var k in edgeColors) { - if (edgeColors.hasOwnProperty(k) && edgeColors[k] > bestCount) { - bestCount = edgeColors[k]; - best = k; - } - } - if (best && bestCount / edgeTotal >= 0.95) return best; - return null; - } - - function _process(img) { - if (!img || !img.complete) { callback(null); return; } - - var canvas = document.createElement("canvas"); - var ctx = canvas.getContext("2d"); - canvas.width = img.naturalWidth || img.width; - canvas.height = img.naturalHeight || img.height; - ctx.drawImage(img, 0, 0); - - var imageData; - try { - imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - } catch (e) { - callback(null); - return; - } - - var data = imageData.data; - var w = canvas.width, - h = canvas.height; - - // 1) 尝试纯色背景 - var pure = _tryPureBackground(data, w, h); - if (pure) { callback(pure); return; } - - // 2) 统计不透明像素(抽样) - var sumR = 0, - sumG = 0, - sumB = 0, - count = 0; - var samples = 0; - var step = 4; // 4x抽样,减少性能消耗 - for (var y = 0; y < h; y += step) { - for (var x = 0; x < w; x += step) { - var i = (y * w + x) * 4; - var a = data[i + 3]; - if (a === 255) { - sumR += data[i]; - sumG += data[i + 1]; - sumB += data[i + 2]; - count++; - } - samples++; - } - } - if (count === 0) { callback(null); return; } - - var avgR = sumR / count, - avgG = sumG / count, - avgB = sumB / count; - - // 3) 生成候选色(借鉴流行配色) - var base = _rgbToHsl(avgR, avgG, avgB); - - function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } - - var candidates = []; - - // 中性色(低饱和) - candidates.push(_hslToRgb(base.h, 0.05, 0.5)); - candidates.push(_hslToRgb(base.h, 0.1, 0.6)); - candidates.push(_hslToRgb(base.h, 0.1, 0.4)); - - // 平均色去饱和 - candidates.push(_hslToRgb(base.h, clamp(base.s * 0.4, 0.05, 0.2), clamp(base.l, 0.2, 0.8))); - - // 互补色(活泼) - candidates.push(_hslToRgb((base.h + 0.5) % 1, clamp(base.s * 0.6, 0.1, 0.8), clamp(base.l, 0.35, 0.7))); - - // 类似色 - candidates.push(_hslToRgb((base.h + 0.083) % 1, clamp(base.s * 0.5, 0.1, 0.8), clamp(base.l, 0.35, 0.7))); - candidates.push(_hslToRgb((base.h - 0.083 + 1) % 1, clamp(base.s * 0.5, 0.1, 0.8), clamp(base.l, 0.35, 0.7))); - - // 三分色 - candidates.push(_hslToRgb((base.h + 0.333) % 1, clamp(base.s * 0.6, 0.1, 0.8), clamp(base.l, 0.35, 0.7))); - candidates.push(_hslToRgb((base.h - 0.333 + 1) % 1, clamp(base.s * 0.6, 0.1, 0.8), clamp(base.l, 0.35, 0.7))); - - // 4) 计算最小对比度(与所有不透明像素) - function minContrastWithImage(bg) { - var bgObj = { r: bg.r, g: bg.g, b: bg.b }; - var minC = Infinity; - - for (var y = 0; y < h; y += step) { - for (var x = 0; x < w; x += step) { - var i = (y * w + x) * 4; - if (data[i + 3] !== 255) continue; - var px = { r: data[i], g: data[i + 1], b: data[i + 2] }; - var c = _contrast(bgObj, px); - if (c < minC) minC = c; - } - } - return minC; - } - - var best = null; - for (var i = 0; i < candidates.length; i++) { - var c = candidates[i]; - var minC = minContrastWithImage(c); - if (minC >= 4.5) { - best = c; - break; - } - } - - if (best) { - callback(_toKey(best.r, best.g, best.b)); - } else { - callback(null); - } - } - - if (source && source.tagName && source.tagName.toLowerCase() === "img") { - _process(source); - } else if (typeof source === "string") { - _createImage(source, _process, function() { callback(null); }); - } else { - callback(null); - } - } - - function getSuitableBackgroundColor(source, callback) { - getSolidOpaqueBackgroundColor(source, function(color) { - if (color) { - callback(color); - } else { - getHamonyColor(source, callback); - } - }); - } + var strres = external.StringResources; function createLocalizedCompare(locale) { return function(a, b) { @@ -383,9 +19,17 @@ var mgr = Package.manager; var nstr = Bridge.NString; var datasrc = new DataView.DataSource(); + datasrc.setKeySelector(function(item) { + if (item === null || item === void 0) return null; + return Bridge.String.tolower(Bridge.String.trim(item.Identity.FullName)); + }); var themeColor = Bridge.UI.themeColor; var loadingDisplay = document.getElementById("applist-loading"); var loadingStatus = loadingDisplay.querySelector(".title"); + var emptyDisplay = document.createElement("div"); + var dataLengthDisplay = document.getElementById("applist-datalen"); + var appSearchList = document.getElementById("applist-search"); + emptyDisplay.textContent = strres.get("MANAGER_MANAGE_LISTEMPTY"); var listView = new DataView.ListView(listContainer, function(item) { var appItem = appItemTemplate.cloneNode(true); appItem.id = ""; @@ -407,17 +51,102 @@ appItem.setAttribute("data-version", item.Identity.Version.Expression); appItem.setAttribute("data-users", item.Users); appItem.setAttribute("data-publisher-id", item.Identity.PublisherId); - setTimeout(function(a, b) { - getSolidOpaqueBackgroundColor(a, function(color) { - try { - var pipes = color.split(","); - var colorobj = new Color(parseInt(pipes[0]), parseInt(pipes[1]), parseInt(pipes[2])); - if (colorobj.hex == "#ffffff" || colorobj.hex == "#000000") throw "too white or black"; - var rgbstr = colorobj.RGB.toString(); - b.style.backgroundColor = rgbstr; - } catch (e) {} - }); - }, 0, item.Properties.LogoBase64, logoimg.parentElement); + logoimg.parentElement.style.backgroundColor = item.BackgroundColor; + if (item.BackgroundColor === "transparent") { + logoimg.parentElement.style.backgroundColor = themeColor; + } + var uninstallButton = appItem.querySelector("div[role=control] .uninstall"); + Windows.UI.Event.Util.addEvent(uninstallButton, "click", function(e) { + e.stopPropagation(); + this.disabled = true; + var itemNode = this.parentNode.parentNode.parentNode; + var flyout = document.getElementById("app-uninstall-flyout"); + if (typeof flyout.appDataSource !== "undefined") flyout.appDataSource.clear(); + if (typeof flyout.appDataSource !== "undefined") { + Package.reader.readFromInstallLocation(this.parentNode.parentNode.parentNode.data.InstallLocation, true).then(function(result) { + try { + if (typeof result.json.applications === "undefined" || result.json.applications.length === 0) { + result.json.applications = [{ + DisplayName: item.Properties.DisplayName || item.Identity.Name, + SmallLogo_Base64: item.Properties.LogoBase64, + }]; + } + flyout.appDataSource.updateList(result.json.applications); + + } catch (e) {} + }, function(result) { + try { flyout.appDataSource.updateList(result.json.applications); } catch (e) {} + }); + } + var self = this; + var confirm = flyout.querySelector(".confirm"); + confirm.onclick = null; + confirm.onclick = function() { + self.disabled = true; + flyout.winControl.hide(); + var fullName = itemNode.getAttribute("data-full-name"); + itemNode.classList.add("uninstalling"); + var progressPart = itemNode.querySelector("div[role=progress]"); + var statusDisplay = progressPart.querySelector(".status"); + statusDisplay.textContent = strres.get("MANAGER_APP_UNINSTALL_ING"); + var progressDisplay = progressPart.querySelector(".progress"); + progressDisplay.removeAttribute("value"); + self.disabled = true; + (function(itemNode, statusDisplay, progressDisplay, self) { + mgr.remove(fullName).then(function(_s) { + itemNode.classList.remove("uninstalling"); + itemNode.classList.add("uninstalled"); + if (_s.succeeded) { + statusDisplay.textContent = strres.get("MANAGER_APP_UNINSTALL_SUCCEED"); + datasrc.remove(itemNode.data); + } else { + statusDisplay.textContent = _s.message; + setTimeout(function(iNode, uButton) { + iNode.classList.remove("uninstalled"); + uButton.disabled = false; + }, 5000, itemNode, self); + } + }, function(_f) { + itemNode.classList.remove("uninstalling"); + itemNode.classList.add("uninstalled"); + try { + if (_f.succeeded) { + statusDisplay.textContent = strres.get("MANAGER_APP_UNINSTALL_SUCCEED"); + datasrc.remove(itemNode.data); + } else { + statusDisplay.textContent = _f.message; + setTimeout(function(iNode, uButton) { + iNode.classList.remove("uninstalled"); + uButton.disabled = false; + }, 5000, itemNode, self); + } + } catch (e) { + statusDisplay.textContent = e.message; + setTimeout(function(iNode, uButton) { + iNode.classList.remove("uninstalled"); + uButton.disabled = false; + }, 5000, itemNode, self); + } + self.disabled = false; + }, function(_p) { + statusDisplay.textContent = Bridge.String.format( + strres.get("MANAGER_APP_UNINSTALL_PROGRESSING"), + _p + ); + progressDisplay.value = _p; + }); + })(itemNode, statusDisplay, progressDisplay, self); + }; + var winFlyout = flyout.winControl; + if (winFlyout._beforehideHandler) { + winFlyout.removeEventListener("beforehide", winFlyout._beforehideHandler); + } + winFlyout._beforehideHandler = function() { + self.disabled = false; + }; + winFlyout.addEventListener("beforehide", winFlyout._beforehideHandler); + flyout.winControl.show(this); + }); Windows.UI.Event.Util.addEvent(appItem.querySelector("div[role=advance] a"), "click", function(e) { e.stopPropagation(); try { @@ -428,93 +157,385 @@ }); listView.selectionMode = "single"; listView.bind(datasrc); + listView.emptyView = emptyDisplay; + listView.searchHandler = function(text, item) { + return ((item.Properties.DisplayName || item.Identity.Name || "") + (item.Properties.Publisher || "")).indexOf(text) >= 0; + }; + appSearchList.control = new Search.Box(appSearchList, { + placeholderText: strres.get("MANAGER_MANAGE_SEARCHPLACEHOLDER"), + chooseSuggestionOnEnter: false + }); + appSearchList.control.ontextchanged = function(ev) { + console.log(ev.text); + listView.searchText = ev.detail.text; + }; + listView.onsearchend = function() { + dataLengthDisplay.textContent = external.String.format(strres.get("MANAGER_MANAGE_FINDAPPS"), listView.findItemLength); + }; var timer = null; function refreshAppList() { + dataLengthDisplay.textContent = ""; + + function processData(manifest, dataitem) { + //if (dataitem.Identity.FamilyName = "Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe") debugger; + dataitem.Properties.DisplayName = dataitem.Properties.DisplayName || manifest.properties.display_name || dataitem.Properties.DisplayName; + if ((dataitem.Properties.DisplayName || "").indexOf("ms-resource:") === 0) { + dataitem.Properties.DisplayName = ""; + } + if (manifest.applications.length === 1) { + dataitem.Properties.DisplayName = dataitem.Properties.DisplayName || manifest.applications[0].DisplayName || ""; + } + if ((dataitem.Properties.DisplayName || "").indexOf("ms-resource:") === 0) { + dataitem.Properties.DisplayName = ""; + } + if (manifest.applications.length === 1) { + dataitem.Properties.DisplayName = dataitem.Properties.DisplayName || manifest.applications[0].ShortName || ""; + } + if ((dataitem.Properties.DisplayName || "").indexOf("ms-resource:") === 0) { + dataitem.Properties.DisplayName = ""; + } + dataitem.Properties.DisplayName = dataitem.Properties.DisplayName || dataitem.Identity.FamilyName; + dataitem.Properties.Puvlisher = dataitem.Properties.Publisher || manifest.properties.publisher_display_name || dataitem.Properties.Publisher; + dataitem.Properties.Framework = dataitem.Properties.Framework || manifest.properties.framework; + dataitem.Properties.Logo = dataitem.Properties.Logo || manifest.properties.logo; + dataitem.Properties.LogoBase64 = dataitem.Properties.LogoBase64 || manifest.properties.logo_base64; + if (manifest.applications.length === 1) { + dataitem.Properties.LogoBase64 = dataitem.Properties.LogoBase64 || manifest.applications[0].Square44x44Logo_Base64 || manifest.applications[0].SmallLogo_Base64; + } + dataitem.Properties.ResourcePackage = dataitem.Properties.ResourcePackage || manifest.properties.resource_package; + dataitem.Properties.Description = dataitem.Properties.Description || manifest.properties.description; + try { + dataitem.BackgroundColor = manifest.applications[0].BackgroundColor || "transparent"; + } catch (e) { + dataitem.BackgroundColor = "transparent"; + } + return dataitem; + } + function update(datas) { var newDatas = []; + var promises = []; for (var i = 0; i < datas.length; i++) { var data = datas[i]; - if (data.Properties.Framework) continue; // 过滤依赖项 - var isfind = false; // 过滤系统应用 - for (var j = 0; data && data.Users && j < data.Users.length; j++) { - if (Bridge.NString.equals(data.Users[j], "NT AUTHORITY\\SYSTEM")) { - isfind = true; - break; + if (external.System.isWindows10) { + if (data.Properties.DisplayName === null || data.Properties.DisplayName === "" || data.Properties.DisplayName === void 0 || + data.Properties.LogoBase64 === null || data.Properties.LogoBase64 === "" || data.Properties.LogoBase64 === void 0 + ) { + promises.push(function(item, arr) { + return Package.reader.readFromInstallLocation(item.InstallLocation, true).then(function(result) { + try { + arr.push(processData(result.json, item)); + } catch (e) { + item.BackgroundColor = "transparent"; + arr.push(item); + } + }, function(result) { + try { + arr.push(processData(result.json, item)); + } catch (e) { + item.BackgroundColor = "transparent"; + arr.push(item); + } + }); + }(data, newDatas)); + } else { + promises.push(function(item, arr) { + return Package.reader.readFromInstallLocation(item.InstallLocation, false).then(function(result) { + try { + item.BackgroundColor = result.json.applications[0].BackgroundColor; + arr.push(item); + } catch (e) { + item.BackgroundColor = "transparent"; + arr.push(item); + } + }, function(result) { + try { + item.BackgroundColor = result.json.applications[0].BackgroundColor; + arr.push(item); + } catch (e) { + item.BackgroundColor = "transparent"; + arr.push(item); + } + }); + }(data, newDatas)); + } + } else { + promises.push(function(item, arr) { + return Package.reader.readFromInstallLocation(item.InstallLocation, true).then(function(result) { + try { + arr.push(processData(result.json, item)); + } catch (e) { + item.BackgroundColor = "transparent"; + arr.push(item); + } + }, function(result) { + try { + arr.push(processData(result.json, item)); + } catch (e) { + item.BackgroundColor = "transparent"; + arr.push(item); + } + }); + }(data, newDatas)); + } + } + + function updateDatas() { + datasrc.updateList(newDatas, function(item) { + return item.Identity.FullName || ""; + }); + var compare = function(a, b) { return a - b; }; + try { + compare = createLocalizedCompare(external.System.Locale.currentLocale); + } catch (e) { + try { + compare = createLocalizedCompare(navigator.language); + } catch (e) { + compare = function(a, b) { + if (a < b) return -1; + if (a > b) return 1; + return 0; + }; } } - if (isfind) continue; - newDatas.push(data); + datasrc.sort(function(a, b) { + return compare(a.Properties.DisplayName, b.Properties.DisplayName); + }); + dataLengthDisplay.textContent = external.String.format(strres.get("MANAGER_MANAGE_FINDAPPS"), listView.findItemLength); } - datasrc.updateList(newDatas, function(item) { - return item.Identity.FullName || ""; - }); - var compare = function(a, b) { return a - b; }; - try { - compare = createLocalizedCompare(external.System.Locale.currentLocale); - } catch (e) { - try { - compare = createLocalizedCompare(navigator.language); - } catch (e) { - compare = function(a, b) { - if (a < b) return -1; - if (a > b) return 1; - return 0; - }; - } - } - datasrc.sort(function(a, b) { - return compare(a.Properties.DisplayName, b.Properties.DisplayName); - }); + return Promise.join(promises).then(updateDatas, updateDatas); } if (timer) clearTimeout(timer); timer = null; loadingDisplay.style.display = ""; loadingDisplay.classList.remove("noloading"); + loadingDisplay.bar.show(); function waitAndHide() { - if (timer) clearTimeout(timer); - timer = null; - timer = setTimeout(function() { - loadingDisplay.style.display = "none"; - }, 10000); + return new Promise(function(resolve, reject) { + if (timer) clearTimeout(timer); + timer = null; + timer = setTimeout(function(rs, rj) { + //loadingDisplay.style.display = "none"; + loadingDisplay.bar.hide(); + rs(); + }, 5000, resolve, reject); + }); } - loadingStatus.textContent = "正在加载数据..."; + loadingStatus.textContent = strres.get("MANAGER_APP_INSTALLEDAPPS_LOADING"); return mgr.get().then(function(result) { - loadingDisplay.classList.add("noloading"); - loadingStatus.textContent = "已经加载了所有数据"; - update(result.list); - waitAndHide(); + return update(result.list).then(function() { + loadingDisplay.classList.add("noloading"); + loadingStatus.textContent = strres.get("MANAGER_APP_INSTALLEDAPPS_SUCCEED"); + setTimeout(function(lv) { + lv.refresh(); + }, 500, listView); + }).then(waitAndHide); }, function(error) { loadingDisplay.classList.add("noloading"); - loadingStatus.textContent = "更新时出错: " + (error.result ? (error.result.message || error.result.ErrorCode || "获取失败") : (error.message || error.error || error)); + var errmsg = (error.result ? (error.result.message || error.result.ErrorCode || "获取失败") : (error.message || error.error || error)); + loadingStatus.textContent = external.String.format(strres.get("MANAGER_APP_INSTALLEDAPPS_FAILED"), errmsg); try { update(error.list); } catch (e) {} - waitAndHide(); - }) + setTimeout(function(lv) { + lv.refresh(); + }, 500, listView); + return waitAndHide(); + }); } var appbar = document.getElementById("appBar"); var appbarControl = new AppBar.AppBar(appbar); var refreshButton = new AppBar.Command(); refreshButton.icon = ""; - refreshButton.label = "刷新"; + refreshButton.label = strres.get("MANAGER_APP_REFRESH"); global.refreshAppList2 = function refreshAppList2() { appbarControl.hide(); refreshButton.disabled = true; - refreshAppList().done(function() { + return refreshAppList().then(function() { refreshButton.disabled = false; }, function(error) { refreshButton.disabled = false; }); } + var showSystemApps = document.getElementById("applist-showsystemapp"); + var showFrameworks = document.getElementById("applist-showframework"); + listView.filter = function(item) { + try { + if (!showFrameworks.checked && item.Properties.Framework) return false; + if (!showSystemApps.checked && item.Users.indexOf("NT AUTHORITY\\SYSTEM") !== -1) return false; + return true; + } catch (e) { + return false; + } + }; + Windows.UI.Event.Util.addEvent(showSystemApps, "change", function() { + listView.refresh(); + dataLengthDisplay.textContent = external.String.format(strres.get("MANAGER_MANAGE_FINDAPPS"), listView.findItemLength); + }); + Windows.UI.Event.Util.addEvent(showFrameworks, "change", function() { + listView.refresh(); + dataLengthDisplay.textContent = external.String.format(strres.get("MANAGER_MANAGE_FINDAPPS"), listView.findItemLength); + }); refreshButton.addEventListener("click", refreshAppList2); appbarControl.add(refreshButton); refreshAppList2(); + var appDetailPage = document.getElementById("page-appinfo"); pagemgr.register("manager", document.getElementById("tag-manager"), document.getElementById("page-manager")); pagemgr.register("appinfo", document.getElementById("tag-appinfo"), document.getElementById("page-appinfo"), setAppInfoPageContent); - var appinfoBackPage = document.getElementById("page-appinfo").querySelector(".win-backbutton"); + var appinfoBackPage = appDetailPage.querySelector(".win-backbutton"); Windows.UI.Event.Util.addEvent(appinfoBackPage, "click", function(e) { pagemgr.back(); }); + appDetailPage.appDataSource = new DataView.DataSource(); + var appListView = new DataView.ListView(appDetailPage.querySelector(".apps"), function(item) { + var appItem = appItemTemplate.cloneNode(true); + appItem.id = ""; + appItem.style.display = ""; + var logoimg = appItem.querySelector("img"); + logoimg.src = item.Square44x44Logo_Base64 || item.SmallLogo_Base64; + if (logoimg.src == "" || logoimg.src == null || logoimg.src == void 0) logoimg.removeAttribute("src"); + logoimg.parentElement.style.backgroundColor = item.BackgroundColor; + if (Bridge.NString.equals(item.BackgroundColor, "transparent")) logoimg.parentElement.style.backgroundColor = themeColor; + var appName = appItem.querySelector(".displayName"); + appName.textContent = item.DisplayName || item.ShortName; + var appPub = appItem.querySelector(".publisher"); + appPub.style.display = "none"; + appItem.querySelector("div[role=advance]").style.display = "none"; + var ctrls = appItem.querySelector("div[role=control]"); + ctrls.innerHTML = ""; + appItem.data = item; + var launchButton = document.createElement("button"); + launchButton.textContent = strres.get("MANAGER_APP_LAUNCH"); + launchButton.setAttribute("data-app-user-model-id", item.AppUserModelID); + var createShortcutButton = document.createElement("button"); + createShortcutButton.textContent = strres.get("MANAGER_APP_CREATESHORTCUT"); + createShortcutButton.style.marginRight = "10px"; + Windows.UI.Event.Util.addEvent(launchButton, "click", function(e) { + e.stopPropagation(); + Package.manager.active(this.getAttribute("data-app-user-model-id")); + }); + ctrls.appendChild(launchButton); + ctrls.appendChild(createShortcutButton); + return appItem; + }); + appListView.selectionMode = "single"; + appListView.bind(appDetailPage.appDataSource); + appListView.emptyView = emptyDisplay.cloneNode(true); + var appDetailUninstall = appDetailPage.querySelector("#detail-uninstall-btn"); + var appDetailUninstallStatusBlock = appDetailPage.querySelector("#appinfo-uninstallstatus"); + var appDetailUninstallProgress = appDetailUninstallStatusBlock.querySelector(".progress"); + var appDetailUninstallProgressStatus = appDetailUninstallStatusBlock.querySelector(".status"); + appDetailUninstallStatusBlock.bar = new TransitionPanel(appDetailUninstallStatusBlock, { + axis: 'y', + duration: 500, + }); + Windows.UI.Event.Util.addEvent(appDetailUninstall, "click", function(e) { + e.stopPropagation(); + appinfoBackPage.disabled = true; + appDetailUninstallProgress.removeAttribute("value"); + var item = appDetailPage.data; + var flyout = document.getElementById("app-uninstall-flyout"); + if (typeof flyout.appDataSource !== "undefined") flyout.appDataSource.clear(); + if (typeof flyout.appDataSource !== "undefined") { + flyout.appDataSource.updateList(appDetailPage.appDataSource.get()); + } + var self = this; + var confirm = flyout.querySelector(".confirm"); + confirm.onclick = null; + confirm.onclick = function() { + self.disabled = true; + flyout.winControl.hide(); + var fullName = item.Identity.FullName; + var progressPart = appDetailUninstallStatusBlock; + var statusDisplay = appDetailUninstallProgressStatus; + statusDisplay.textContent = strres.get("MANAGER_APP_UNINSTALL_ING"); + var progressDisplay = appDetailUninstallProgress; + progressDisplay.style.display = ""; + self.disabled = true; + progressPart.bar.show(); + (function(statusDisplay, progressDisplay, self, item) { + mgr.remove(fullName).then(function(_s) { + if (_s.succeeded) { + statusDisplay.textContent = strres.get("MANAGER_APP_UNINSTALL_SUCCEED"); + datasrc.remove(item); + appinfoBackPage.disabled = false; + } else { + statusDisplay.textContent = _s.message; + } + setTimeout(function(uButton, isSuccess) { + appinfoBackPage.disabled = false; + uButton.disabled = isSuccess; + progressPart.bar.hide(); + }, 5000, self, _s.succeeded); + progressDisplay.style.display = "none"; + }, function(_f) { + try { + if (_f.succeeded) { + statusDisplay.textContent = strres.get("MANAGER_APP_UNINSTALL_SUCCEED"); + datasrc.remove(item); + appinfoBackPage.disabled = false; + } else { + statusDisplay.textContent = _f.message; + } + setTimeout(function(uButton, isSuccess) { + appinfoBackPage.disabled = false; + uButton.disabled = isSuccess; + progressPart.bar.hide(); + }, 5000, self, _f.succeeded); + } catch (e) { + statusDisplay.textContent = e.message; + appinfoBackPage.disabled = false; + setTimeout(function(uButton, isSuccess) { + appinfoBackPage.disabled = false; + uButton.disabled = isSuccess; + progressPart.bar.hide(); + }, 5000, self, _f.succeeded); + } + self.disabled = false; + progressDisplay.style.display = "none"; + }, function(_p) { + statusDisplay.textContent = Bridge.String.format( + strres.get("MANAGER_APP_UNINSTALL_PROGRESSING"), + _p + ); + progressDisplay.value = _p; + }); + })(statusDisplay, progressDisplay, self, item); + }; + var winFlyout = flyout.winControl; + if (winFlyout._beforehideHandler) { + winFlyout.removeEventListener("beforehide", winFlyout._beforehideHandler); + } + winFlyout._beforehideHandler = function() { + self.disabled = false; + }; + winFlyout.addEventListener("beforehide", winFlyout._beforehideHandler); + flyout.winControl.show(this); + }); + var uninstallFlyout = document.getElementById("app-uninstall-flyout"); + uninstallFlyout.appListView = new DataView.ListView(uninstallFlyout.querySelector(".applist"), function(item) { + var appItem = appItemTemplate.cloneNode(true); + appItem.id = ""; + appItem.style.display = ""; + var logoimg = appItem.querySelector("img"); + logoimg.src = item.Square44x44Logo_Base64 || item.SmallLogo_Base64; + if (logoimg.src == "" || logoimg.src == null || logoimg.src == void 0) logoimg.removeAttribute("src"); + logoimg.parentElement.style.backgroundColor = item.BackgroundColor; + if (Bridge.NString.equals(item.BackgroundColor, "transparent")) logoimg.parentElement.style.backgroundColor = themeColor; + var appName = appItem.querySelector(".displayName"); + appName.style.wordBreak = "normal"; + appName.style.wordWrap = "normal"; + appName.textContent = item.DisplayName || item.ShortName; + var appPub = appItem.querySelector(".publisher"); + appPub.style.display = "none"; + appItem.querySelector("div[role=advance]").style.display = "none"; + var ctrls = appItem.querySelector("div[role=control]"); + ctrls.innerHTML = ""; + appItem.data = item; + return appItem; + }); + uninstallFlyout.appDataSource = new DataView.DataSource(); + uninstallFlyout.appListView.bind(uninstallFlyout.appDataSource); pagemgr.addEventListener("load", function(e) { appbarControl.enabled = e == "manager"; refreshButton.style.display = e == "manager" ? "" : "none"; diff --git a/shared/html/js/pkginfo.js b/shared/html/js/pkginfo.js index a165b10..afca0b8 100644 --- a/shared/html/js/pkginfo.js +++ b/shared/html/js/pkginfo.js @@ -16,7 +16,44 @@ if (callback) callback(ret); } global.Package = { - reader: function(pkgPath) { return external.Package.reader(pkgPath); }, + reader: { + package: function(pkgPath) { return external.Package.Reader.package(pkgPath); }, + manifest: function(swManifestPath) { return external.Package.Reader.manifest(swManifestPath); }, + manifestFromInstallLocation: function(swInstallLocation) { return external.Package.Reader.fromInstallLocation(swInstallLocation); }, + readFromPackage: function(swPkgPath, bUsePri) { + if (bUsePri === null || bUsePri === void 0) bUsePri = false; + return new Promise(function(resolve, reject) { + external.Package.Reader.readFromPackageAsync(swPkgPath, bUsePri, function(result) { + parseJsonCallback(result, resolve); + }, function(error) { + parseJsonCallback(error, reject); + }); + }); + }, + readFromManifest: function(swManifestPath, bUsePri) { + if (bUsePri === null || bUsePri === void 0) bUsePri = false; + return new Promise(function(resolve, reject) { + external.Package.Reader.readFromManifestAsync(swManifestPath, bUsePri, function(result) { + parseJsonCallback(result, resolve); + }, function(error) { + parseJsonCallback(error, reject); + }); + }); + }, + readFromInstallLocation: function(swInstallLocation, bUsePri) { + if (bUsePri === null || bUsePri === void 0) bUsePri = false; + return new Promise(function(resolve, reject) { + external.Package.Reader.readFromInstallLocationAsync(swInstallLocation, bUsePri, function(result) { + parseJsonCallback(result, resolve); + }, function(error) { + parseJsonCallback(error, reject); + }); + }); + }, + cancelAll: function() { external.Package.Reader.cancelAll(); }, + addApplicationReadItem: function(swItemName) { return external.Package.Reader.addApplicationItem(swItemName); }, + removeApplicationReadItem: function(swItemName) { return external.Package.Reader.removeApplicationItem(swItemName); } + }, manager: { add: function(swPkgPath, uOptions) { return new Promise(function(resolve, reject, progress) { @@ -129,38 +166,8 @@ }); }); }, - }, - manifest: function(swManifestPath) { return external.Package.manifest(swManifestPath); }, - manifestFromInstallLocation: function(swInstallLocation) { return external.Package.fromInstallLocation(swInstallLocation); }, - readFromPackage: function(swPkgPath, bUsePri) { - if (bUsePri === null || bUsePri === void 0) bUsePri = false; - return new Promise(function(resolve, reject) { - external.Package.readFromPackageAsync(swPkgPath, bUsePri, function(result) { - parseJsonCallback(result, resolve); - }, function(error) { - parseJsonCallback(error, reject); - }); - }); - }, - readFromManifest: function(swPkgPath, bUsePri) { - if (bUsePri === null || bUsePri === void 0) bUsePri = false; - return new Promise(function(resolve, reject) { - external.Package.readFromManifestAsync(swPkgPath, bUsePri, function(result) { - parseJsonCallback(result, resolve); - }, function(error) { - parseJsonCallback(error, reject); - }); - }); - }, - readFromInstallLocation: function(swPkgPath, bUsePri) { - if (bUsePri === null || bUsePri === void 0) bUsePri = false; - return new Promise(function(resolve, reject) { - external.Package.readFromInstallLocationAsync(swPkgPath, bUsePri, function(result) { - parseJsonCallback(result, resolve); - }, function(error) { - parseJsonCallback(error, reject); - }); - }); + cancelAll: function() { mgr.cancelAll(); }, + active: function(swAppUserModelID, swArgs) { return mgr.activeApp(swAppUserModelID, swArgs || null); } }, }; })(this); \ No newline at end of file diff --git a/shared/html/js/resources.js b/shared/html/js/resources.js index 79258c6..729a1b1 100644 --- a/shared/html/js/resources.js +++ b/shared/html/js/resources.js @@ -13,7 +13,11 @@ var byName = el.getAttribute('data-res-byname'); var byId = el.getAttribute('data-res-byid'); var fromFile = el.getAttribute('data-res-fromfile'); - if ((byName && !Bridge.NString.empty(byName)) || (byId && parseInt(byId, 10) > 0) || (fromFile && !Bridge.NString.empty(fromFile))) { + var byXml = el.getAttribute('data-res-resxml'); + if ((byName && !Bridge.NString.empty(byName)) || + (byId && parseInt(byId, 10) > 0) || + (fromFile && !Bridge.NString.empty(fromFile)) || + (byXml && !Bridge.NString.empty(byXml))) { result.push(el); } } @@ -47,6 +51,16 @@ } catch (e) { nodes[i].textContent = ""; } + } else if (nodes[i].hasAttribute('data-res-resxml')) { + try { + var obj = nodes[i].getAttribute('data-res-resxml'); + var strres = external.StringResources; + if (strres && strres.isValid) { + nodes[i].textContent = strres.get(obj); + } + } catch (e) { + nodes[i].textContent = ""; + } } else { nodes[i].textContent = ""; } diff --git a/shared/html/js/search.js b/shared/html/js/search.js new file mode 100644 index 0000000..ec1d6ea --- /dev/null +++ b/shared/html/js/search.js @@ -0,0 +1,720 @@ +/*! + * Search.Box - standalone SearchBox control (ES5, IE10 compatible) + * Exposes constructor as global.Search.Box + * + * Features: + * - API compatible-ish with WinJS.UI.SearchBox: properties (placeholderText, queryText, chooseSuggestionOnEnter, disabled) + * - Events: querychanged, querysubmitted, resultsuggestionchosen, suggestionsrequested + * - supports both `element.addEventListener("querychanged", handler)` and `instance.onquerychanged = handler` + * - Methods: setSuggestions(array), clearSuggestions(), dispose(), setLocalContentSuggestionSettings(settings) (noop) + * - Suggestions kinds: Query (0), Result (1), Separator (2) OR string names 'query'/'result'/'separator' + * - Hit highlighting: uses item.hits if provided, otherwise simple substring match of current input + * - No WinRT / WinJS dependency + * + * Usage: + * var box = new Search.Box(hostElement, options); + * box.setSuggestions([{ kind: 0, text: "hello" }, ...]); + */ + +(function(global) { + "use strict"; + + // Ensure namespace + if (!global.Search) { + global.Search = {}; + } + + // Suggestion kinds + var SuggestionKind = { + Query: 0, + Result: 1, + Separator: 2 + }; + + // Utility: create id + function uniqueId(prefix) { + return prefix + Math.random().toString(36).slice(2); + } + + // Simple CustomEvent fallback for IE10 + function createCustomEvent(type, detail) { + var ev; + try { + ev = document.createEvent("CustomEvent"); + ev.initCustomEvent(type, true, true, detail || {}); + } catch (e) { + ev = document.createEvent("Event"); + ev.initEvent(type, true, true); + ev.detail = detail || {}; + } + return ev; + } + + // Constructor + function SearchBox(element, options) { + element = element || document.createElement("div"); + + if (element.__searchBoxInstance) { + throw new Error("Search.Box: duplicate construction on same element"); + } + element.__searchBoxInstance = this; + + // DOM elements + this._root = element; + this._input = document.createElement("input"); + this._input.type = "search"; + this._button = document.createElement("div"); + this._button.tabIndex = -1; + this._flyout = document.createElement("div"); + this._repeater = document.createElement("div"); // container for suggestion items + this._flyout.style.display = "none"; + // state + this._suggestions = []; + this._currentSelectedIndex = -1; // fake focus/selection index + this._currentFocusedIndex = -1; // navigation focus index + this._prevQueryText = ""; + this._chooseSuggestionOnEnter = false; + this._disposed = false; + this._lastKeyPressLanguage = ""; + + // classes follow WinJS naming where convenient (so your existing CSS can still be used) + this._root.className = (this._root.className ? this._root.className + " " : "") + "win-searchbox"; + this._input.className = "win-searchbox-input"; + this._button.className = "win-searchbox-button"; + this._flyout.className = "win-searchbox-flyout"; + this._repeater.className = "win-searchbox-repeater"; + + // assemble + this._flyout.appendChild(this._repeater); + this._root.appendChild(this._input); + this._root.appendChild(this._button); + this._root.appendChild(this._flyout); + + // accessibility basics + this._root.setAttribute("role", "group"); + this._input.setAttribute("role", "textbox"); + this._button.setAttribute("role", "button"); + this._repeater.setAttribute("role", "listbox"); + if (!this._repeater.id) { + this._repeater.id = uniqueId("search_repeater_"); + } + this._input.setAttribute("aria-controls", this._repeater.id); + this._repeater.setAttribute("aria-live", "polite"); + + // user-assignable event handlers (older style) + this.onquerychanged = null; + this.onquerysubmitted = null; + this.onresultsuggestionchosen = null; + this.onsuggestionsrequested = null; + + // wire events + this._wireEvents(); + + // options + options = options || {}; + if (options.placeholderText) this.placeholderText = options.placeholderText; + if (options.queryText) this.queryText = options.queryText; + if (options.chooseSuggestionOnEnter) this.chooseSuggestionOnEnter = !!options.chooseSuggestionOnEnter; + if (options.disabled) this.disabled = !!options.disabled; + + // new events + this.ontextchanged = null; + } + + // Prototype + SearchBox.prototype = { + // Properties + get element() { + return this._root; + }, + + get placeholderText() { + return this._input.placeholder; + }, + set placeholderText(value) { + this._input.placeholder = value || ""; + }, + + get queryText() { + return this._input.value; + }, + set queryText(value) { + this._input.value = value == null ? "" : value; + }, + + get chooseSuggestionOnEnter() { + return this._chooseSuggestionOnEnter; + }, + set chooseSuggestionOnEnter(v) { + this._chooseSuggestionOnEnter = !!v; + this._updateButtonClass(); + }, + + get disabled() { + return !!this._input.disabled; + }, + set disabled(v) { + var val = !!v; + if (val === this.disabled) return; + this._input.disabled = val; + try { this._button.disabled = val; } catch (e) {} + if (val) { + this._root.className = (this._root.className + " win-searchbox-disabled").trim(); + this.hideFlyout(); + } else { + this._root.className = this._root.className.replace(/\bwin-searchbox-disabled\b/g, "").trim(); + } + }, + + // Public methods + setSuggestions: function(arr) { + // Expect array of objects with keys: kind (0/1/2 or 'query'/'result'/'separator'), text, detailText, tag, imageUrl, hits + this._suggestions = (arr && arr.slice(0)) || []; + this._currentSelectedIndex = -1; + this._currentFocusedIndex = -1; + this._renderSuggestions(); + if (this._suggestions.length) this.showFlyout(); + else this.hideFlyout(); + }, + + clearSuggestions: function() { + this.setSuggestions([]); + }, + + showFlyout: function() { + if (!this._suggestions || this._suggestions.length === 0) return; + this._flyout.style.display = "block"; + this._updateButtonClass(); + }, + + hideFlyout: function() { + this._flyout.style.display = "none"; + this._updateButtonClass(); + }, + + dispose: function() { + if (this._disposed) return; + // detach event listeners by cloning elements (simple way) + var newRoot = this._root.cloneNode(true); + if (this._root.parentNode) { + this._root.parentNode.replaceChild(newRoot, this._root); + } + try { + delete this._root.__searchBoxInstance; + } catch (e) {} + this._disposed = true; + }, + + setLocalContentSuggestionSettings: function(settings) { + // No-op in non-WinRT environment; kept for API compatibility. + }, + + // Internal / rendering + _wireEvents: function() { + var that = this; + + this._input.addEventListener("input", function(ev) { + that._onInputChange(ev); + }, false); + + this._input.addEventListener("keydown", function(ev) { + that._onKeyDown(ev); + }, false); + + this._input.addEventListener("keypress", function(ev) { + // capture locale if available + try { that._lastKeyPressLanguage = ev.locale || that._lastKeyPressLanguage; } catch (e) {} + }, false); + + this._input.addEventListener("focus", function() { + if (that._suggestions.length) { + that.showFlyout(); + that._updateFakeFocus(); + } + that._root.className = (that._root.className + " win-searchbox-input-focus").trim(); + that._updateButtonClass(); + }, false); + + this._input.addEventListener("blur", function() { + // small timeout to allow suggestion click to process + setTimeout(function() { + if (!that._root.contains(document.activeElement)) { + that.hideFlyout(); + that._root.className = that._root.className.replace(/\bwin-searchbox-input-focus\b/g, "").trim(); + that._currentFocusedIndex = -1; + that._currentSelectedIndex = -1; + } + }, 0); + }, false); + + this._button.addEventListener("click", function(ev) { + that._input.focus(); + that._submitQuery(that._input.value, ev); + that.hideFlyout(); + }, false); + + // delegate click for suggestions: attach on repeater container (works in IE10) + this._repeater.addEventListener("click", function(ev) { + var el = ev.target; + // climb until we find child with data-index + while (el && el !== that._repeater) { + if (el.hasAttribute && el.hasAttribute("data-index")) break; + el = el.parentNode; + } + if (el && el !== that._repeater) { + var idx = parseInt(el.getAttribute("data-index"), 10); + var item = that._suggestions[idx]; + if (item) { + that._input.focus(); + that._processSuggestionChosen(item, ev); + } + } + }, false); + }, + + _onInputChange: function(ev) { + if (this.disabled) return; + var v = this._input.value; + + this._emit("textchanged", { + text: v + }, this.ontextchanged); + + var changed = (v !== this._prevQueryText); + this._prevQueryText = v; + + // fire querychanged + var evDetail = { + language: this._getBrowserLanguage(), + queryText: v, + linguisticDetails: { queryTextAlternatives: [], queryTextCompositionStart: 0, queryTextCompositionLength: 0 } + }; + this._emit("querychanged", evDetail, this.onquerychanged); + + // fire suggestionsrequested - allow client to call setSuggestions + var suggestionsDetail = { + queryText: v, + language: this._getBrowserLanguage(), + setSuggestions: (function(thatRef) { + return function(arr) { + thatRef.setSuggestions(arr || []); + }; + })(this) + }; + this._emit("suggestionsrequested", suggestionsDetail, this.onsuggestionsrequested); + }, + + _submitQuery: function(queryText, ev) { + var detail = { + language: this._getBrowserLanguage(), + queryText: queryText, + keyModifiers: this._getKeyModifiers(ev) + }; + this._emit("querysubmitted", detail, this.onquerysubmitted); + }, + + _processSuggestionChosen: function(item, ev) { + // normalize kind + var kind = item.kind; + if (typeof kind === "string") { + if (kind.toLowerCase() === "query") kind = SuggestionKind.Query; + else if (kind.toLowerCase() === "result") kind = SuggestionKind.Result; + else if (kind.toLowerCase() === "separator") kind = SuggestionKind.Separator; + } + + this.queryText = item.text || ""; + if (kind === SuggestionKind.Query || kind === undefined) { + // choose query -> submit + this._submitQuery(item.text || "", ev); + } else if (kind === SuggestionKind.Result) { + this._emit("resultsuggestionchosen", { + tag: item.tag, + keyModifiers: this._getKeyModifiers(ev), + storageFile: null + }, this.onresultsuggestionchosen); + } + this.hideFlyout(); + }, + + _renderSuggestions: function() { + // clear repeater + while (this._repeater.firstChild) this._repeater.removeChild(this._repeater.firstChild); + + var frag = document.createDocumentFragment(); + for (var i = 0; i < this._suggestions.length; i++) { + var s = this._suggestions[i]; + var itemEl = this._renderSuggestion(s, i); + frag.appendChild(itemEl); + } + this._repeater.appendChild(frag); + this._updateFakeFocus(); + }, + + _renderSuggestion: function(item, index) { + var that = this; + var kind = item.kind; + if (typeof kind === "string") { + kind = kind.toLowerCase() === "query" ? SuggestionKind.Query : + kind.toLowerCase() === "result" ? SuggestionKind.Result : + kind.toLowerCase() === "separator" ? SuggestionKind.Separator : kind; + } + + var root = document.createElement("div"); + root.setAttribute("data-index", index); + root.id = this._repeater.id + "_" + index; + + if (kind === SuggestionKind.Separator) { + root.className = "win-searchbox-suggestion-separator"; + if (item.text) { + var textEl = document.createElement("div"); + textEl.innerText = item.text; + textEl.setAttribute("aria-hidden", "true"); + root.appendChild(textEl); + } + root.insertAdjacentHTML("beforeend", "
"); + root.setAttribute("role", "separator"); + root.setAttribute("aria-label", item.text || ""); + return root; + } + + if (kind === SuggestionKind.Result) { + root.className = "win-searchbox-suggestion-result"; + // image + var img = document.createElement("img"); + img.setAttribute("aria-hidden", "true"); + if (item.imageUrl) { + img.onload = function() { + img.style.opacity = "1"; + }; + img.style.opacity = "0"; + img.src = item.imageUrl; + } else { + img.style.display = "none"; + } + root.appendChild(img); + + var textDiv = document.createElement("div"); + textDiv.className = "win-searchbox-suggestion-result-text"; + textDiv.setAttribute("aria-hidden", "true"); + this._addHitHighlightedText(textDiv, item, item.text || ""); + textDiv.title = item.text || ""; + root.appendChild(textDiv); + + var detail = document.createElement("span"); + detail.className = "win-searchbox-suggestion-result-detailed-text"; + detail.setAttribute("aria-hidden", "true"); + this._addHitHighlightedText(detail, item, item.detailText || ""); + textDiv.appendChild(document.createElement("br")); + textDiv.appendChild(detail); + + root.setAttribute("role", "option"); + root.setAttribute("aria-label", (item.text || "") + " " + (item.detailText || "")); + return root; + } + + // default / query + root.className = "win-searchbox-suggestion-query"; + this._addHitHighlightedText(root, item, item.text || ""); + root.title = item.text || ""; + root.setAttribute("role", "option"); + root.setAttribute("aria-label", item.text || ""); + return root; + }, + + _addHitHighlightedText: function(container, item, text) { + // Remove existing children + while (container.firstChild) container.removeChild(container.firstChild); + if (!text) return; + + // Build hits from item.hits if present (array of {startPosition, length}), otherwise simple substring matches of current input + var hits = []; + if (item && item.hits && item.hits.length) { + for (var i = 0; i < item.hits.length; i++) { + hits.push({ startPosition: item.hits[i].startPosition, length: item.hits[i].length }); + } + } else { + var q = this._input.value || ""; + if (q) { + var low = text.toLowerCase(); + var lq = q.toLowerCase(); + var pos = 0; + while (true) { + var idx = low.indexOf(lq, pos); + if (idx === -1) break; + hits.push({ startPosition: idx, length: q.length }); + pos = idx + q.length; + } + } + } + + // Merge overlapping hits & sort + hits.sort(function(a, b) { return a.startPosition - b.startPosition; }); + var merged = []; + for (var j = 0; j < hits.length; j++) { + if (merged.length === 0) { + merged.push({ startPosition: hits[j].startPosition, length: hits[j].length }); + } else { + var cur = merged[merged.length - 1]; + var curEnd = cur.startPosition + cur.length; + if (hits[j].startPosition <= curEnd) { + var nextEnd = hits[j].startPosition + hits[j].length; + if (nextEnd > curEnd) { + cur.length = nextEnd - cur.startPosition; + } + } else { + merged.push({ startPosition: hits[j].startPosition, length: hits[j].length }); + } + } + } + + var last = 0; + for (var k = 0; k < merged.length; k++) { + var h = merged[k]; + if (h.startPosition > last) { + var pre = document.createElement("span"); + pre.innerText = text.substring(last, h.startPosition); + pre.setAttribute("aria-hidden", "true"); + container.appendChild(pre); + } + var hitSpan = document.createElement("span"); + hitSpan.innerText = text.substring(h.startPosition, h.startPosition + h.length); + hitSpan.className = "win-searchbox-flyout-highlighttext"; + hitSpan.setAttribute("aria-hidden", "true"); + container.appendChild(hitSpan); + last = h.startPosition + h.length; + } + if (last < text.length) { + var post = document.createElement("span"); + post.innerText = text.substring(last); + post.setAttribute("aria-hidden", "true"); + container.appendChild(post); + } + + if (merged.length === 0) { + // no hits - append plain text + var whole = document.createElement("span"); + whole.innerText = text; + whole.setAttribute("aria-hidden", "true"); + container.appendChild(whole); + } + }, + + _updateFakeFocus: function() { + var firstIndex = -1; + if ((this._flyout.style.display !== "none") && this._chooseSuggestionOnEnter) { + for (var i = 0; i < this._suggestions.length; i++) { + var s = this._suggestions[i]; + var kind = s.kind; + if (typeof kind === "string") { + kind = kind.toLowerCase() === "query" ? SuggestionKind.Query : + kind.toLowerCase() === "result" ? SuggestionKind.Result : + kind.toLowerCase() === "separator" ? SuggestionKind.Separator : kind; + } + if (kind === SuggestionKind.Query || kind === SuggestionKind.Result) { + firstIndex = i; + break; + } + } + } + this._selectSuggestionAtIndex(firstIndex); + }, + + _selectSuggestionAtIndex: function(index) { + for (var i = 0; i < this._repeater.children.length; i++) { + var el = this._repeater.children[i]; + if (!el) continue; + if (i === index) { + if (el.className.indexOf("win-searchbox-suggestion-selected") === -1) { + el.className = (el.className + " win-searchbox-suggestion-selected").trim(); + } + el.setAttribute("aria-selected", "true"); + try { + this._input.setAttribute("aria-activedescendant", el.id); + } catch (e) {} + // ensure visible + try { + var top = el.offsetTop; + var bottom = top + el.offsetHeight; + var scrollTop = this._flyout.scrollTop; + var height = this._flyout.clientHeight; + if (bottom > scrollTop + height) this._flyout.scrollTop = bottom - height; + else if (top < scrollTop) this._flyout.scrollTop = top; + } catch (e) {} + } else { + el.className = el.className.replace(/\bwin-searchbox-suggestion-selected\b/g, "").trim(); + el.setAttribute("aria-selected", "false"); + } + } + if (index === -1) { + try { this._input.removeAttribute("aria-activedescendant"); } catch (e) {} + } + this._currentSelectedIndex = index; + this._updateButtonClass(); + }, + + _updateButtonClass: function() { + if ((this._currentSelectedIndex !== -1) || (document.activeElement !== this._input)) { + this._button.className = this._button.className.replace(/\bwin-searchbox-button-input-focus\b/g, "").trim(); + } else if (document.activeElement === this._input) { + if (this._button.className.indexOf("win-searchbox-button-input-focus") === -1) { + this._button.className = (this._button.className + " win-searchbox-button-input-focus").trim(); + } + } + }, + + _onKeyDown: function(ev) { + // Normalize key + var key = ev.key || ""; + if (!key && ev.keyCode) { + if (ev.keyCode === 13) key = "Enter"; + else if (ev.keyCode === 27) key = "Esc"; + else if (ev.keyCode === 38) key = "Up"; + else if (ev.keyCode === 40) key = "Down"; + else if (ev.keyCode === 9) key = "Tab"; + } + + if (key === "Tab") { + // handle tab navigation into suggestions + if (ev.shiftKey) { + // shift+tab: allow default behavior + } else { + if (this._currentFocusedIndex === -1) { + this._currentFocusedIndex = this._findNextSuggestionElementIndex(-1); + } + if (this._currentFocusedIndex !== -1) { + this._selectSuggestionAtIndex(this._currentFocusedIndex); + this._updateQueryTextWithSuggestionText(this._currentFocusedIndex); + ev.preventDefault(); + ev.stopPropagation(); + } + } + } else if (key === "Esc") { + if (this._currentFocusedIndex !== -1) { + this.queryText = this._prevQueryText; + this._currentFocusedIndex = -1; + this._selectSuggestionAtIndex(-1); + this._updateButtonClass(); + ev.preventDefault(); + ev.stopPropagation(); + } else if (this.queryText !== "") { + this.queryText = ""; + // trigger querychanged handlers + this._onInputChange(null); + this._updateButtonClass(); + ev.preventDefault(); + ev.stopPropagation(); + } + } else if (key === "Up") { + var prev; + if (this._currentSelectedIndex !== -1) { + prev = this._findPreviousSuggestionElementIndex(this._currentSelectedIndex); + if (prev === -1) this.queryText = this._prevQueryText; + } else { + prev = this._findPreviousSuggestionElementIndex(this._suggestions.length); + } + this._currentFocusedIndex = prev; + this._selectSuggestionAtIndex(prev); + this._updateQueryTextWithSuggestionText(this._currentFocusedIndex); + this._updateButtonClass(); + ev.preventDefault(); + ev.stopPropagation(); + } else if (key === "Down") { + var next = this._findNextSuggestionElementIndex(this._currentSelectedIndex); + if ((this._currentSelectedIndex !== -1) && (next === -1)) this.queryText = this._prevQueryText; + this._currentFocusedIndex = next; + this._selectSuggestionAtIndex(next); + this._updateQueryTextWithSuggestionText(this._currentFocusedIndex); + this._updateButtonClass(); + ev.preventDefault(); + ev.stopPropagation(); + } else if (key === "Enter") { + if (this._currentSelectedIndex === -1) { + this._submitQuery(this._input.value, ev); + } else { + var chosen = this._suggestions[this._currentSelectedIndex]; + if (chosen) this._processSuggestionChosen(chosen, ev); + else this._submitQuery(this._input.value, ev); + } + this.hideFlyout(); + ev.preventDefault(); + ev.stopPropagation(); + } else { + // typing -> clear selection + if (this._currentFocusedIndex !== -1) { + this._currentFocusedIndex = -1; + this._selectSuggestionAtIndex(-1); + this._updateFakeFocus(); + } + } + }, + + _findNextSuggestionElementIndex: function(curIndex) { + var start = curIndex + 1; + if (start < 0) start = 0; + for (var i = start; i < this._suggestions.length; i++) { + var s = this._suggestions[i]; + var k = s.kind; + if (typeof k === "string") { + k = k.toLowerCase() === "query" ? SuggestionKind.Query : + k.toLowerCase() === "result" ? SuggestionKind.Result : + k.toLowerCase() === "separator" ? SuggestionKind.Separator : k; + } + if (k === SuggestionKind.Query || k === SuggestionKind.Result) return i; + } + return -1; + }, + + _findPreviousSuggestionElementIndex: function(curIndex) { + var start = curIndex - 1; + if (start >= this._suggestions.length) start = this._suggestions.length - 1; + for (var i = start; i >= 0; i--) { + var s = this._suggestions[i]; + var k = s.kind; + if (typeof k === "string") { + k = k.toLowerCase() === "query" ? SuggestionKind.Query : + k.toLowerCase() === "result" ? SuggestionKind.Result : + k.toLowerCase() === "separator" ? SuggestionKind.Separator : k; + } + if (k === SuggestionKind.Query || k === SuggestionKind.Result) return i; + } + return -1; + }, + + _updateQueryTextWithSuggestionText: function(idx) { + if ((idx >= 0) && (idx < this._suggestions.length)) { + this.queryText = this._suggestions[idx].text || ""; + } + }, + + _emit: function(type, detail, handler) { + var ev = createCustomEvent(type, detail); + // call handler property first + if (typeof handler === "function") { + try { handler.call(this, ev); } catch (e) { /* swallow */ } + } + try { this._root.dispatchEvent(ev); } catch (e) { /* swallow */ } + return ev; + }, + + _getBrowserLanguage: function() { + try { + return (navigator && (navigator.language || navigator.userLanguage)) || ""; + } catch (e) { + return ""; + } + }, + + _getKeyModifiers: function(ev) { + var m = 0; + if (!ev) return m; + if (ev.ctrlKey) m |= 1; + if (ev.altKey) m |= 2; + if (ev.shiftKey) m |= 4; + return m; + } + }; + + // export + global.Search.Box = SearchBox; + +})(this); \ No newline at end of file diff --git a/shared/html/js/statusbar.js b/shared/html/js/statusbar.js new file mode 100644 index 0000000..be8bbb9 --- /dev/null +++ b/shared/html/js/statusbar.js @@ -0,0 +1,184 @@ +(function(global) { + "use strict"; + + /** + * TransitionPanel + * axis: 'x' | 'y' | 'both' + * speed: 'fast' | 'medium' | 'slow' + * el: DOM 元素 + */ + function TransitionPanel(el, options) { + if (!el) throw new Error("TransitionPanel requires a DOM element."); + + this.el = el; + this.opts = options || {}; + this.axis = this.opts.axis || 'y'; + this.speed = this.opts.speed || 'medium'; + + // 初始化状态 + this._shown = false; + this._events = {}; + + // 确保基础类 statusbar + if (!el.classList.contains('statusbar')) el.classList.add('statusbar'); + + // 确保 axis 类存在(x/y/both) + if (this.axis === 'x' && !el.classList.contains('x')) el.classList.add('x'); + else if (this.axis === 'y' && !el.classList.contains('y')) el.classList.add('y'); + else if (this.axis === 'both' && !el.classList.contains('both')) el.classList.add('both'); + + // 添加速度类 + if (this.speed === 'fast') el.classList.add('fast'); + else if (this.speed === 'medium') el.classList.add('medium'); + else el.classList.add('slow'); + + // 内容变化自动刷新 + this._bindContentChange(); + } + + function maxWidth(el) { + var cw = 0; + var ow = 0; + var rw = 0; + var sw = 0; + try { cw = el.clientWidth; } catch (e) {} + try { ow = el.offsetWidth; } catch (e) {} + try { rw = el.getBoundingClientRect().width; } catch (e) {} + try { sw = el.scrollWidth; } catch (e) {} + return Math.max(cw, ow, rw, sw); + } + + function maxHeight(el) { + var ch = 0; + var oh = 0; + var rh = 0; + var sh = 0; + try { ch = el.clientHeight; } catch (e) {} + try { oh = el.offsetHeight; } catch (e) {} + try { rh = el.getBoundingClientRect().height; } catch (e) {} + try { sh = el.scrollHeight; } catch (e) {} + return Math.max(ch, oh, rh, sh); + } + // 显示 + TransitionPanel.prototype.show = function() { + if (this._shown) return; + this._emit('beforeshow'); + this._shown = true; + + var el = this.el; + + setTimeout(function() { + this._emit('show'); + + if (this.axis !== 'x') el.style.height = maxHeight(el) + 'px'; + if (this.axis !== 'y') el.style.width = maxWidth(el) + 'px'; + if (this.axis === 'both') { + el.style.height = maxHeight(el) + 'px'; + el.style.width = maxWidth(el) + 'px'; + } + this._afterTransition('aftershow'); + }.bind(this), 16); + }; + + // 隐藏 + TransitionPanel.prototype.hide = function() { + if (!this._shown) return; + this._emit('beforehide'); + this._shown = false; + + var el = this.el; + + // 锁定当前尺寸 + if (this.axis !== 'x') el.style.height = maxHeight(el) + 'px'; + if (this.axis !== 'y') el.style.width = maxWidth(el) + 'px'; + if (this.axis === 'both') { + el.style.height = maxHeight(el) + 'px'; + el.style.width = maxWidth(el) + 'px'; + } + setTimeout(function() { + this._emit('hide'); + + // 回到折叠状态尺寸(依赖 x/y/both 类) + if (this.axis !== 'x') el.style.height = ''; + if (this.axis !== 'y') el.style.width = ''; + if (this.axis === 'both') { + el.style.height = ''; + el.style.width = ''; + } + this._afterTransition('afterhide'); + }.bind(this), 16); + }; + + // 刷新尺寸(显示中) + TransitionPanel.prototype.refresh = function() { + if (!this._shown) return; + var el = this.el; + if (this.axis !== 'x') el.style.height = el.scrollHeight + 'px'; + if (this.axis !== 'y') el.style.width = el.scrollWidth + 'px'; + }; + + // 内容变化自动刷新 + TransitionPanel.prototype._bindContentChange = function() { + if (!global.setTextChangeEvent) return; + var self = this; + global.setTextChangeEvent(this.el, function() { + if (self._shown) self.refresh(); + }); + }; + + // transitionend 回调处理 + TransitionPanel.prototype._afterTransition = function(evt) { + var el = this.el; + var called = false; + var duration = this.speed === 'fast' ? 300 : (this.speed === 'medium' ? 500 : 700); + + function done() { + if (called) return; + called = true; + el.removeEventListener('transitionend', done); + if (evt) this._emit(evt); + } + el.addEventListener('transitionend', done.bind(this)); + setTimeout(done.bind(this), duration + 30); + }; + + // 生命周期事件绑定 + TransitionPanel.prototype.on = function(name, fn) { + (this._events[name] || (this._events[name] = [])).push(fn); + }; + + // 事件触发 + TransitionPanel.prototype._emit = function(name) { + var list = this._events[name]; + if (!list) return; + for (var i = 0; i < list.length; i++) { + try { list[i].call(this); } catch (e) { console.error(e); } + } + }; + + // 只读属性 shown + Object.defineProperty(TransitionPanel.prototype, 'shown', { + get: function() { return this._shown; } + }); + + // 销毁 + TransitionPanel.prototype.dispose = function() { + // 移除所有事件回调 + this._events = {}; + // 清理 el 内联样式 + if (this.el) { + this.el.style.height = ''; + this.el.style.width = ''; + } + this._shown = false; + // 可选:删除内容变化监听(如果使用全局 setTextChangeEvent) + if (global.Windows && global.Windows.UI && global.Windows.UI.Event && global.Windows.UI.Event.Monitor) { + // 这里可以 detach 所有回调 + // 视具体实现可扩展 + } + }; + + // 全局暴露 + global.TransitionPanel = TransitionPanel; + +})(this); \ No newline at end of file diff --git a/shared/html/manager.html b/shared/html/manager.html index 657f656..67b3945 100644 --- a/shared/html/manager.html +++ b/shared/html/manager.html @@ -33,6 +33,9 @@ + + + @@ -42,15 +45,39 @@