diff --git a/AwqatSalaat.Common/Data/CalendarType.cs b/AwqatSalaat.Common/Data/CalendarType.cs new file mode 100644 index 0000000..956ecc1 --- /dev/null +++ b/AwqatSalaat.Common/Data/CalendarType.cs @@ -0,0 +1,8 @@ +namespace AwqatSalaat.Data +{ + public enum CalendarType + { + UmAlQura, + Hijri + } +} diff --git a/AwqatSalaat.Common/Helpers/HijriDateHelper.cs b/AwqatSalaat.Common/Helpers/HijriDateHelper.cs index 202a259..e547bc0 100644 --- a/AwqatSalaat.Common/Helpers/HijriDateHelper.cs +++ b/AwqatSalaat.Common/Helpers/HijriDateHelper.cs @@ -1,37 +1,109 @@ -using System; +using AwqatSalaat.Properties; +using System; using System.Globalization; +using System.Linq; namespace AwqatSalaat.Helpers { public static class HijriDateHelper { - private static readonly DateTimeFormatInfo FallbackHijriDateTimeFormatInfo; + private static readonly DateTimeFormatInfo s_ArabicUmAlQuraDateTimeFormatInfo; + private static readonly DateTimeFormatInfo s_EnglishUmAlQuraDateTimeFormatInfo; + private static readonly DateTimeFormatInfo s_ArabicHijriDateTimeFormatInfo; + private static readonly DateTimeFormatInfo s_EnglishHijriDateTimeFormatInfo; + private static readonly HijriCalendar s_HijriCalendar = new HijriCalendar(); static HijriDateHelper() { - // Start with the Arabic culture info since it provide all features - var dateTimeFormat = new CultureInfo("ar-SA").DateTimeFormat; - // Use English names as a fallback - dateTimeFormat.MonthNames = new string[] + var englishHijriMonths = new string[] { - "Muharram", - "Safar", - "Rabiʻ I", - "Rabiʻ II", - "Jumada I", - "Jumada II", - "Rajab", - "Shaʻban", - "Ramadan", - "Shawwal", - "Dhuʻl-Qiʻdah", - "Dhuʻl-Hijjah", - "" + "Muharram", + "Safar", + "Rabiʻ I", + "Rabiʻ II", + "Jumada I", + "Jumada II", + "Rajab", + "Shaʻban", + "Ramadan", + "Shawwal", + "Dhuʻl-Qiʻdah", + "Dhuʻl-Hijjah", + "" }; - // MonthGenitiveNames is what provide values for formatter - dateTimeFormat.MonthGenitiveNames = dateTimeFormat.MonthNames; - FallbackHijriDateTimeFormatInfo = dateTimeFormat; + // Init Arabc DateTimeFormatInfo for Um Al Qura calendar + var arSA = new CultureInfo("ar-SA"); + s_ArabicUmAlQuraDateTimeFormatInfo = arSA.DateTimeFormat; + + if (!(arSA.Calendar is UmAlQuraCalendar)) + { + var umalqura = arSA.OptionalCalendars.Single(c => c is UmAlQuraCalendar); + s_ArabicUmAlQuraDateTimeFormatInfo.Calendar = umalqura; + } + + + + // Init English DateTimeFormatInfo for Um Al Qura calendar + var enSA = new CultureInfo("en-SA"); + + if (enSA.OptionalCalendars.Any(c => c is UmAlQuraCalendar)) + { + s_EnglishUmAlQuraDateTimeFormatInfo = enSA.DateTimeFormat; + + if (!(enSA.Calendar is UmAlQuraCalendar)) + { + var umalqura = enSA.OptionalCalendars.Single(c => c is UmAlQuraCalendar); + s_EnglishUmAlQuraDateTimeFormatInfo.Calendar = umalqura; + } + } + else + { + // Start with the Arabic culture info since it provide all features + var dateTimeFormat = new CultureInfo("ar-SA").DateTimeFormat; + // Set English names + dateTimeFormat.MonthNames = englishHijriMonths; + // MonthGenitiveNames is what provide values for formatter + dateTimeFormat.MonthGenitiveNames = dateTimeFormat.MonthNames; + + s_EnglishUmAlQuraDateTimeFormatInfo = dateTimeFormat; + } + + + + // Init Arabic DateTimeFormatInfo for Hijri calendar + arSA = new CultureInfo("ar-SA"); + arSA.DateTimeFormat.Calendar = s_HijriCalendar; + s_ArabicHijriDateTimeFormatInfo = arSA.DateTimeFormat; + + + + // Init English DateTimeFormatInfo for Hijri calendar + enSA = new CultureInfo("en-SA"); + + if (enSA.OptionalCalendars.Any(c => c is HijriCalendar)) + { + enSA.DateTimeFormat.Calendar = s_HijriCalendar; + s_EnglishHijriDateTimeFormatInfo = enSA.DateTimeFormat; + } + else + { + // Start with the Arabic culture info since it provide all features + var dateTimeFormat = new CultureInfo("ar-SA").DateTimeFormat; + dateTimeFormat.Calendar = s_HijriCalendar; + // Set English names + dateTimeFormat.MonthNames = englishHijriMonths; + // MonthGenitiveNames is what provide values for formatter + dateTimeFormat.MonthGenitiveNames = dateTimeFormat.MonthNames; + + s_EnglishHijriDateTimeFormatInfo = dateTimeFormat; + } + + + + Settings.Default.SettingsLoaded += (_, __) => s_HijriCalendar.HijriAdjustment = Settings.Default.HijriAdjustment; + Settings.Default.SettingsSaving += (_, __) => s_HijriCalendar.HijriAdjustment = Settings.Default.HijriAdjustment; + s_HijriCalendar.HijriAdjustment = Settings.Default.HijriAdjustment; } public static string Format(DateTime dateTime, string format, string language) @@ -41,30 +113,27 @@ public static string Format(DateTime dateTime, string format, string language) language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; } - if (language.Length > 2) - { - language = language.Substring(0, 2); - } + CultureInfo culture = CultureInfo.GetCultureInfo(language); - language += "-SA"; + DateTimeFormatInfo dateTimeFormat = GetProperDateTimeFormatInfo(culture); - CultureInfo culture = CultureInfo.GetCultureInfo(language); + return dateTime.ToString(format, dateTimeFormat); + } - DateTimeFormatInfo dateTimeFormat = culture.DateTimeFormat; + private static DateTimeFormatInfo GetProperDateTimeFormatInfo(CultureInfo culture) + { + var calendarType = Settings.Default.CalendarType; - if (dateTimeFormat.Calendar.GetType() != typeof(UmAlQuraCalendar)) + if (calendarType == Data.CalendarType.UmAlQura) { - if (culture.TextInfo.IsRightToLeft) - { - dateTimeFormat = CultureInfo.GetCultureInfo("ar-SA").DateTimeFormat; - } - else - { - dateTimeFormat = FallbackHijriDateTimeFormatInfo; - } + return culture.TextInfo.IsRightToLeft ? s_ArabicUmAlQuraDateTimeFormatInfo : s_EnglishUmAlQuraDateTimeFormatInfo; + } + else if (calendarType == Data.CalendarType.Hijri) + { + return culture.TextInfo.IsRightToLeft ? s_ArabicHijriDateTimeFormatInfo : s_EnglishHijriDateTimeFormatInfo; } - return dateTime.ToString(format, dateTimeFormat); + throw new InvalidOperationException("Invalid calendar type."); } } } diff --git a/AwqatSalaat.Common/Helpers/SystemInfos.cs b/AwqatSalaat.Common/Helpers/SystemInfos.cs index a18deca..b6f00f3 100644 --- a/AwqatSalaat.Common/Helpers/SystemInfos.cs +++ b/AwqatSalaat.Common/Helpers/SystemInfos.cs @@ -93,10 +93,13 @@ public static bool IsTaskBarWidgetsEnabled() { if (key != null) { - int value = Convert.ToInt32(key.GetValue("TaskbarDa", 0)); + int value = Convert.ToInt32(key.GetValue("TaskbarDa", 1)); return value == 1; } } + + // Widgets button is enabled by default in Windows 11 + return true; } return false; diff --git a/AwqatSalaat.Common/Interop/Native.cs b/AwqatSalaat.Common/Interop/Native.cs index d628365..3a7c8d2 100644 --- a/AwqatSalaat.Common/Interop/Native.cs +++ b/AwqatSalaat.Common/Interop/Native.cs @@ -16,10 +16,6 @@ public static class User32 [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern int MessageBox(IntPtr HWND, string lpText, string lpCaption, uint uType); - public static int MessageBox(IntPtr HWND, string lpText, string lpCaption, MessageBoxButtons buttons) - => MessageBox(HWND, lpText, lpCaption, (uint)buttons); - public static int MessageBox(IntPtr HWND, string lpText, string lpCaption, MessageBoxButtons buttons, MessageBoxIcon icon) - => MessageBox(HWND, lpText, lpCaption, (uint)buttons | (uint)icon); [DllImport("user32.dll")] public static extern uint GetDpiForWindow([In] IntPtr hWnd); @@ -88,6 +84,18 @@ public static extern IntPtr CreateWindowEx( [DllImport("user32.dll", SetLastError = true)] public static extern bool SetWindowPos([In] IntPtr hWnd, [In, Optional] IntPtr hWndInsertAfter, [In] int X, [In] int Y, [In] int cx, [In] int cy, [In] SWP uFlags); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetCursorPos([In] int x, [In] int y); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetCursorPos([Out] out POINT lpPoint); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetForegroundWindow([In] IntPtr hWnd); } public static class Dwmapi @@ -112,6 +120,9 @@ public static class Kernel32 { [DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string module); + + [DllImport("kernel32.dll")] + public static extern IntPtr RegisterApplicationRestart(string pwzCommandline, ApplicationRestart dwFlags = ApplicationRestart.None); } [Flags] @@ -174,6 +185,32 @@ public enum MessageBoxIcon : uint MB_ICONSTOP = MB_ICONHAND, } + public enum MessageBoxResult : uint + { + // NONE is not a standard value, I added it to simplify things + NONE = 0, + IDOK = 1, + IDCANCEL = 2, + IDABORT = 3, + IDRETRY = 4, + IDIGNORE = 5, + IDYES = 6, + IDNO = 7, + } + + [Flags] + public enum MessageBoxOptions : uint + { + // NONE is not a standard value, I added it to simplify things + NONE = 0, + MB_SETFOREGROUND = 0x00010000, + MB_DEFAULT_DESKTOP_ONLY = 0x00020000, + MB_TOPMOST = 0x00040000, + MB_RIGHT = 0x00080000, + MB_RTLREADING = 0x00100000, + MB_SERVICE_NOTIFICATION = 0x00200000, + } + [Flags] public enum SWP : uint { @@ -344,6 +381,7 @@ public enum AlphaFormat : byte public enum WindowMessage : uint { WM_SETREDRAW = 0x000B, + WM_QUERYENDSESSION = 0x0011, WM_SETTINGCHANGE = 0x001A } @@ -460,4 +498,26 @@ public enum DWMNCRENDERINGPOLICY /// DWMNCRP_LAST }; + + [Flags] + public enum ApplicationRestart + { + None = 0, + /// + /// Do not restart the process if it terminates due to an unhandled exception. + /// + RESTART_NO_CRASH = 1, + /// + /// Do not restart the process if it terminates due to the application not responding. + /// + RESTART_NO_HANG = 2, + /// + /// Do not restart the process if it terminates due to the installation of an update. + /// + RESTART_NO_PATCH = 4, + /// + /// Do not restart the process if the computer is restarted as the result of an update. + /// + RESTART_NO_REBOOT = 8, + } } diff --git a/AwqatSalaat.Common/Properties/Resources.Designer.cs b/AwqatSalaat.Common/Properties/Resources.Designer.cs index 2020556..cbe8d32 100644 --- a/AwqatSalaat.Common/Properties/Resources.Designer.cs +++ b/AwqatSalaat.Common/Properties/Resources.Designer.cs @@ -78,6 +78,24 @@ public static string Data_AppName { } } + /// + /// Looks up a localized string similar to Hijri. + /// + public static string Data_CalendarType_Hijri { + get { + return ResourceManager.GetString("Data.CalendarType.Hijri", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Um Al Qura. + /// + public static string Data_CalendarType_UmAlQura { + get { + return ResourceManager.GetString("Data.CalendarType.UmAlQura", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hanafi. /// @@ -150,6 +168,54 @@ public static string Data_Salaat_Shuruq { } } + /// + /// Looks up a localized string similar to The application is already running.. + /// + public static string Dialog_AppAlreadyRunning { + get { + return ResourceManager.GetString("Dialog.AppAlreadyRunning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not check for updates.. + /// + public static string Dialog_CheckingUpdatesFailed { + get { + return ResourceManager.GetString("Dialog.CheckingUpdatesFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new release is available! + ///Version: {0} + /// + ///Would you like to visit download page?. + /// + public static string Dialog_NewUpdateAvailableFormat { + get { + return ResourceManager.GetString("Dialog.NewUpdateAvailableFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The widget is already up-to-date.. + /// + public static string Dialog_WidgetUpToDate { + get { + return ResourceManager.GetString("Dialog.WidgetUpToDate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Check for Updates. + /// + public static string UI_About_CheckForUpdates { + get { + return ResourceManager.GetString("UI.About.CheckForUpdates", resourceCulture); + } + } + /// /// Looks up a localized string similar to Contact. /// @@ -177,6 +243,15 @@ public static string UI_ContextMenu_Hide { } } + /// + /// Looks up a localized string similar to Manual position. + /// + public static string UI_ContextMenu_ManualPosition { + get { + return ResourceManager.GetString("UI.ContextMenu.ManualPosition", resourceCulture); + } + } + /// /// Looks up a localized string similar to Quit. /// @@ -267,6 +342,16 @@ public static string UI_Panel_Refreshing { } } + /// + /// Looks up a localized string similar to Service down? + ///Try switching to other service by going to. + /// + public static string UI_Panel_ServiceDownHint { + get { + return ResourceManager.GetString("UI.Panel.ServiceDownHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Settings. /// @@ -285,6 +370,24 @@ public static string UI_Settings_Browse { } } + /// + /// Looks up a localized string similar to Calendar type. + /// + public static string UI_Settings_Calendar { + get { + return ResourceManager.GetString("UI.Settings.Calendar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The calendar which is used to determine Hijri date. + /// + public static string UI_Settings_CalendarDescription { + get { + return ResourceManager.GetString("UI.Settings.CalendarDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cancel. /// @@ -402,6 +505,15 @@ public static string UI_Settings_File { } } + /// + /// Looks up a localized string similar to Adjust by (day). + /// + public static string UI_Settings_HijriCalendarAdjustment { + get { + return ResourceManager.GetString("UI.Settings.HijriCalendarAdjustment", resourceCulture); + } + } + /// /// Looks up a localized string similar to Juristic school. /// diff --git a/AwqatSalaat.Common/Properties/Resources.ar.resx b/AwqatSalaat.Common/Properties/Resources.ar.resx index 4c53717..dcfb392 100644 --- a/AwqatSalaat.Common/Properties/Resources.ar.resx +++ b/AwqatSalaat.Common/Properties/Resources.ar.resx @@ -363,4 +363,44 @@ استعراض... + + الهجري + + + أم القرى + + + نوع التقويم + + + التقويم الذي يستخدم لمعرفة التاريخ الهجري + + + تعديل بـ (يوم) + + + تموقع يدوي + + + تحقق من التحديثات + + + التطبيق قيد التشغيل بالفعل. + + + الأداة بالفعل محدثة. + + + إصدار جديد متوفر! +نسخة: {0} + +هل تود زيارة صفحة التحميل؟ + + + لم يمكن التحقق من وجود تحديثات. + + + الخدمة متوقفة؟ +جرب التبديل إلى خدمة أخرى بالذهاب إلى + \ No newline at end of file diff --git a/AwqatSalaat.Common/Properties/Resources.resx b/AwqatSalaat.Common/Properties/Resources.resx index 0c7f649..670f188 100644 --- a/AwqatSalaat.Common/Properties/Resources.resx +++ b/AwqatSalaat.Common/Properties/Resources.resx @@ -343,4 +343,44 @@ Browse... + + Hijri + + + Um Al Qura + + + Calendar type + + + The calendar which is used to determine Hijri date + + + Adjust by (day) + + + Manual position + + + Check for Updates + + + The application is already running. + + + The widget is already up-to-date. + + + A new release is available! +Version: {0} + +Would you like to visit download page? + + + Could not check for updates. + + + Service down? +Try switching to other service by going to + \ No newline at end of file diff --git a/AwqatSalaat.Common/Properties/Settings.Designer.cs b/AwqatSalaat.Common/Properties/Settings.Designer.cs index ce199a9..fbe0e1d 100644 --- a/AwqatSalaat.Common/Properties/Settings.Designer.cs +++ b/AwqatSalaat.Common/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace AwqatSalaat.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.9.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")] public sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -286,5 +286,41 @@ public bool EnableNotificationSound { this["EnableNotificationSound"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("UmAlQura")] + public global::AwqatSalaat.Data.CalendarType CalendarType { + get { + return ((global::AwqatSalaat.Data.CalendarType)(this["CalendarType"])); + } + set { + this["CalendarType"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public int HijriAdjustment { + get { + return ((int)(this["HijriAdjustment"])); + } + set { + this["HijriAdjustment"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("-1")] + public int CustomPosition { + get { + return ((int)(this["CustomPosition"])); + } + set { + this["CustomPosition"] = value; + } + } } } diff --git a/AwqatSalaat.Common/Properties/Settings.settings b/AwqatSalaat.Common/Properties/Settings.settings index 3ceec54..b63e8f7 100644 --- a/AwqatSalaat.Common/Properties/Settings.settings +++ b/AwqatSalaat.Common/Properties/Settings.settings @@ -68,5 +68,14 @@ False + + UmAlQura + + + 0 + + + -1 + \ No newline at end of file diff --git a/AwqatSalaat.Common/Services/AlAdhan/Meta.cs b/AwqatSalaat.Common/Services/AlAdhan/Meta.cs index da3229f..9171711 100644 --- a/AwqatSalaat.Common/Services/AlAdhan/Meta.cs +++ b/AwqatSalaat.Common/Services/AlAdhan/Meta.cs @@ -2,8 +2,9 @@ { internal class Meta { - public double Latitude { get; set; } - public double Longitude { get; set; } + //https://github.com/Khiro95/Awqat-Salaat/issues/25 + //public double Latitude { get; set; } + //public double Longitude { get; set; } public string TimeZone { get; set; } } } diff --git a/AwqatSalaat.Common/Services/GitHub/GitHubClient.cs b/AwqatSalaat.Common/Services/GitHub/GitHubClient.cs new file mode 100644 index 0000000..9e68251 --- /dev/null +++ b/AwqatSalaat.Common/Services/GitHub/GitHubClient.cs @@ -0,0 +1,93 @@ +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace AwqatSalaat.Services.GitHub +{ + internal class GitHubClient + { + // Use a static HttpClient to avoid port-exhaustion problem + private static readonly HttpClient _httpClient = new HttpClient(); + + static GitHubClient() + { + _httpClient.BaseAddress = new Uri("https://api.github.com/repos/Khiro95/Awqat-Salaat/"); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "Awqat Salaat"); + _httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json"); + } + + public static async Task GetReleases(CancellationToken cancellationToken = default) + { + var httpResponse = await _httpClient.GetAsync("releases", cancellationToken); + + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (httpResponse.IsSuccessStatusCode) + { + string responseBody = await httpResponse.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject(responseBody); + } + else + { + GitHubError error = null; + + try + { + string responseBody = await httpResponse.Content.ReadAsStringAsync(); + + error = JsonConvert.DeserializeObject(responseBody); + } + catch (Exception ex) + { +#if DEBUG + throw; +#endif + } + + throw new Exception($"GitHub Error: {error?.Message ?? httpResponse.StatusCode.ToString()}"); + } + } + + public static async Task GetLatestRelease(CancellationToken cancellationToken = default) + { + var httpResponse = await _httpClient.GetAsync("releases/latest", cancellationToken); + + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (httpResponse.IsSuccessStatusCode) + { + string responseBody = await httpResponse.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject(responseBody); + } + else + { + GitHubError error = null; + + try + { + string responseBody = await httpResponse.Content.ReadAsStringAsync(); + + error = JsonConvert.DeserializeObject(responseBody); + } + catch (Exception ex) + { +#if DEBUG + throw; +#endif + } + + throw new Exception($"GitHub Error: {error?.Message ?? httpResponse.StatusCode.ToString()}"); + } + } + } +} diff --git a/AwqatSalaat.Common/Services/GitHub/GitHubError.cs b/AwqatSalaat.Common/Services/GitHub/GitHubError.cs new file mode 100644 index 0000000..7107c15 --- /dev/null +++ b/AwqatSalaat.Common/Services/GitHub/GitHubError.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace AwqatSalaat.Services.GitHub +{ + internal class GitHubError + { + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/AwqatSalaat.Common/Services/GitHub/Release.cs b/AwqatSalaat.Common/Services/GitHub/Release.cs new file mode 100644 index 0000000..d93272e --- /dev/null +++ b/AwqatSalaat.Common/Services/GitHub/Release.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; +using System; + +namespace AwqatSalaat.Services.GitHub +{ + public class Release + { + [JsonProperty("tag_name")] + public string Tag { get; set; } + + [JsonProperty("html_url")] + public string HtmlUrl { get; set; } + + [JsonProperty("draft")] + public bool IsDraft { get; set; } + + [JsonProperty("prerelease")] + public bool IsPreRelease { get; set; } + + public Version GetVersion() + { + if (string.IsNullOrEmpty(Tag)) + { + throw new InvalidOperationException("Tag is missing."); + } + + var v = new Version(Tag.Replace("v", "").Replace("V", "")); + + return new Version( + v.Major, + v.Minor, + v.Build == -1 ? 0 : v.Build, + v.Revision == -1 ? 0 : v.Revision); + } + } +} diff --git a/AwqatSalaat.Common/Services/Nominatim/Address.cs b/AwqatSalaat.Common/Services/Nominatim/Address.cs index 6f2f635..523b151 100644 --- a/AwqatSalaat.Common/Services/Nominatim/Address.cs +++ b/AwqatSalaat.Common/Services/Nominatim/Address.cs @@ -8,6 +8,9 @@ public class Address [JsonProperty("city")] public string City { get; private set; } + [JsonProperty("town")] + public string Town { get; private set; } + [JsonProperty("province")] public string Province { get; private set; } @@ -23,7 +26,7 @@ public class Address [OnDeserialized] private void CityFallback(StreamingContext context) { - City = City ?? Province; + City = City ?? Province ?? Town; } } } diff --git a/AwqatSalaat.Common/ViewModels/WidgetSettingsViewModel.cs b/AwqatSalaat.Common/ViewModels/WidgetSettingsViewModel.cs index 4a76511..9a0bcdf 100644 --- a/AwqatSalaat.Common/ViewModels/WidgetSettingsViewModel.cs +++ b/AwqatSalaat.Common/ViewModels/WidgetSettingsViewModel.cs @@ -1,7 +1,9 @@ using AwqatSalaat.Data; using AwqatSalaat.Helpers; +using AwqatSalaat.Services.GitHub; using System; - +using System.Linq; +using System.Threading.Tasks; using Settings = AwqatSalaat.Properties.Settings; namespace AwqatSalaat.ViewModels @@ -9,6 +11,7 @@ namespace AwqatSalaat.ViewModels public class WidgetSettingsViewModel : ObservableObject { private bool isOpen = !Settings.Default.IsConfigured; + private bool isCheckingNewVersion; private ( PrayerTimesService service, School school, @@ -23,6 +26,7 @@ public class WidgetSettingsViewModel : ObservableObject public static Country[] AvailableCountries => CountriesProvider.GetCountries(); public bool IsOpen { get => isOpen; set => Open(value); } + public bool IsCheckingNewVersion { get => isCheckingNewVersion; set => SetProperty(ref isCheckingNewVersion, value); } public bool UseArabic { get => Settings.DisplayLanguage == "ar"; @@ -82,6 +86,45 @@ public WidgetSettingsViewModel() }; } + public async Task CheckForNewVersion(Version currentVersion) + { + try + { + IsCheckingNewVersion = true; + + var latest = await GitHubClient.GetLatestRelease(); + + if (latest is null) + { + return null; + } + + if (latest.IsDraft || latest.IsPreRelease) + { + var allReleases = await GitHubClient.GetReleases(); + + if (allReleases?.Length > 1) + { + latest = allReleases + .Where(r => !r.IsDraft && !r.IsPreRelease) + .DefaultIfEmpty(new Release { Tag = "0.0" }) + .OrderByDescending(r => r.GetVersion()) + .First(); + } + else + { + return null; + } + } + + return latest.GetVersion() > currentVersion ? latest : null; + } + finally + { + IsCheckingNewVersion = false; + } + } + private void SaveExecute(object obj) { var currentServiceSettings = ( diff --git a/AwqatSalaat.Common/ViewModels/WidgetViewModel.cs b/AwqatSalaat.Common/ViewModels/WidgetViewModel.cs index 7dfb65c..b6ed14a 100644 --- a/AwqatSalaat.Common/ViewModels/WidgetViewModel.cs +++ b/AwqatSalaat.Common/ViewModels/WidgetViewModel.cs @@ -78,7 +78,7 @@ public WidgetViewModel() SettingsUpdated(false); UpdateServiceClient(); - var cached = JsonConvert.DeserializeObject(WidgetSettings.Settings.ApiCache); + var cached = JsonConvert.DeserializeObject(WidgetSettings.Settings.ApiCache ?? ""); if (cached != null) { @@ -106,6 +106,8 @@ private void SettingsUpdated(bool hasServiceSettingsChanged) WidgetSettings.Settings.Save(); RefreshData(); } + + OnPropertyChanged(nameof(DisplayedDate)); } private void TimeEntered(object sender, EventArgs e) @@ -254,6 +256,11 @@ private bool OnDataLoaded(ServiceData response) private async Task RefreshData() { + if (IsRefreshing) + { + return; + } + try { ErrorMessage = null; @@ -309,8 +316,10 @@ private async Task RefreshData() ErrorMessage = ex.Message; } - - IsRefreshing = false; + finally + { + IsRefreshing = false; + } } private void OnNearNotificationStarted() diff --git a/AwqatSalaat.WinUI.MSIX/AwqatSalaat.WinUI.MSIX.wapproj b/AwqatSalaat.WinUI.MSIX/AwqatSalaat.WinUI.MSIX.wapproj new file mode 100644 index 0000000..20b265b --- /dev/null +++ b/AwqatSalaat.WinUI.MSIX/AwqatSalaat.WinUI.MSIX.wapproj @@ -0,0 +1,147 @@ + + + + 15.0 + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM + + + Release + ARM + + + Debug + ARM64 + + + Release + ARM64 + + + Debug + AnyCPU + + + Release + AnyCPU + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + + + + 14a4614a-54f6-4095-b63c-169fe149f0c6 + 10.0.19041.0 + 10.0.17763.0 + en-US + false + True + $(NoWarn);NU1702 + ..\AwqatSalaat.WinUI\AwqatSalaat.WinUI.csproj + False + 58CE94A1AF1A2EB428C1A15D38BFBEC57B03A236 + SHA256 + False + False + x86|x64 + 0 + True + True + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.scale-200.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.scale-200.png new file mode 100644 index 0000000..8eaf9a1 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.scale-200.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16.png new file mode 100644 index 0000000..7888c62 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16_altform-lightunplated.png new file mode 100644 index 0000000..7888c62 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16_altform-unplated.png new file mode 100644 index 0000000..7888c62 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-16_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24.png new file mode 100644 index 0000000..177add0 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24_altform-lightunplated.png new file mode 100644 index 0000000..177add0 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..177add0 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-24_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256.png new file mode 100644 index 0000000..a6035a9 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256_altform-lightunplated.png new file mode 100644 index 0000000..a6035a9 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256_altform-unplated.png new file mode 100644 index 0000000..a6035a9 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-256_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32.png new file mode 100644 index 0000000..fed5e22 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32_altform-lightunplated.png new file mode 100644 index 0000000..fed5e22 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32_altform-unplated.png new file mode 100644 index 0000000..fed5e22 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-32_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40.png new file mode 100644 index 0000000..4df4c66 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40_altform-lightunplated.png new file mode 100644 index 0000000..4df4c66 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40_altform-unplated.png new file mode 100644 index 0000000..4df4c66 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-40_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48.png new file mode 100644 index 0000000..bf692af Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48_altform-lightunplated.png new file mode 100644 index 0000000..bf692af Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48_altform-unplated.png new file mode 100644 index 0000000..bf692af Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-48_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64.png new file mode 100644 index 0000000..fa0c481 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64_altform-lightunplated.png new file mode 100644 index 0000000..fa0c481 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64_altform-unplated.png new file mode 100644 index 0000000..fa0c481 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-64_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80.png new file mode 100644 index 0000000..0d9bcbf Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80_altform-lightunplated.png new file mode 100644 index 0000000..0d9bcbf Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80_altform-unplated.png new file mode 100644 index 0000000..0d9bcbf Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-80_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96.png new file mode 100644 index 0000000..a137964 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96_altform-lightunplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96_altform-lightunplated.png new file mode 100644 index 0000000..a137964 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96_altform-lightunplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96_altform-unplated.png b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96_altform-unplated.png new file mode 100644 index 0000000..a137964 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/AppList.targetsize-96_altform-unplated.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/MedTile.scale-200.png b/AwqatSalaat.WinUI.MSIX/Images/MedTile.scale-200.png new file mode 100644 index 0000000..93b74fe Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/MedTile.scale-200.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/SmallTile.scale-200.png b/AwqatSalaat.WinUI.MSIX/Images/SmallTile.scale-200.png new file mode 100644 index 0000000..3469b7e Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/SmallTile.scale-200.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Images/StoreLogo.scale-200.png b/AwqatSalaat.WinUI.MSIX/Images/StoreLogo.scale-200.png new file mode 100644 index 0000000..fe97248 Binary files /dev/null and b/AwqatSalaat.WinUI.MSIX/Images/StoreLogo.scale-200.png differ diff --git a/AwqatSalaat.WinUI.MSIX/Package.appxmanifest b/AwqatSalaat.WinUI.MSIX/Package.appxmanifest new file mode 100644 index 0000000..05bbe50 --- /dev/null +++ b/AwqatSalaat.WinUI.MSIX/Package.appxmanifest @@ -0,0 +1,57 @@ + + + + + + + + Awqat Salaat WinUI + Khiro + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AwqatSalaat.WinUI/App.xaml.cs b/AwqatSalaat.WinUI/App.xaml.cs index 5641ee6..5267892 100644 --- a/AwqatSalaat.WinUI/App.xaml.cs +++ b/AwqatSalaat.WinUI/App.xaml.cs @@ -1,8 +1,6 @@ using AwqatSalaat.Helpers; -using IWshRuntimeLibrary; using Microsoft.UI.Xaml; using System; -using System.IO; using System.Threading; using System.Windows.Input; using WinRT.Interop; @@ -59,44 +57,11 @@ private static void ExitIfOtherInstanceIsRunning() if (!created) { - ShowError("The application is already running."); + ShowError(Properties.Resources.Dialog_AppAlreadyRunning); Environment.Exit(ExitCodes.AlreadyRunning); } } - public static void SetLaunchOnWindowsStartup(bool launchOnWindowsStartup) - { - var process = System.Diagnostics.Process.GetCurrentProcess(); - var moduleInfo = process.MainModule.FileVersionInfo; - var shortcutPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), moduleInfo.ProductName + ".lnk"); - - if (launchOnWindowsStartup) - { - WshShell wshShell = new WshShell(); - - // Create the shortcut - IWshShortcut shortcut = (IWshShortcut)wshShell.CreateShortcut(shortcutPath); - - shortcut.TargetPath = moduleInfo.FileName; - shortcut.WorkingDirectory = Path.GetDirectoryName(moduleInfo.FileName); - shortcut.Description = $"Launch {moduleInfo.ProductName}"; - shortcut.Save(); - } - else - { - try - { - System.IO.File.Delete(shortcutPath); - } - catch (Exception ex) - { -#if DEBUG - throw; -#endif - } - } - } - private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { ShowUnhandledException(e.Exception); @@ -116,8 +81,7 @@ private static void ShowUnhandledException(object exception) private static void ShowError(string message) { - string caption = LocaleManager.Default.Get("Data.AppName"); - Interop.User32.MessageBox(IntPtr.Zero, message, caption, Interop.MessageBoxButtons.MB_OK, Interop.MessageBoxIcon.MB_ICONERROR); + Helpers.MessageBox.Error(message); } private static void QuitExecute() diff --git a/AwqatSalaat.WinUI/Assets/as_ico_win11.ico b/AwqatSalaat.WinUI/Assets/as_ico_win11.ico index a4cf026..6871eae 100644 Binary files a/AwqatSalaat.WinUI/Assets/as_ico_win11.ico and b/AwqatSalaat.WinUI/Assets/as_ico_win11.ico differ diff --git a/AwqatSalaat.WinUI/Assets/sign_key.snk b/AwqatSalaat.WinUI/Assets/sign_key.snk new file mode 100644 index 0000000..034f0f3 Binary files /dev/null and b/AwqatSalaat.WinUI/Assets/sign_key.snk differ diff --git a/AwqatSalaat.WinUI/AwqatSalaat.WinUI.csproj b/AwqatSalaat.WinUI/AwqatSalaat.WinUI.csproj index 9b059c1..0fc9f07 100644 --- a/AwqatSalaat.WinUI/AwqatSalaat.WinUI.csproj +++ b/AwqatSalaat.WinUI/AwqatSalaat.WinUI.csproj @@ -5,22 +5,36 @@ 10.0.17763.0 AwqatSalaat.WinUI app.manifest - x86;x64;ARM64 - win10-x86;win10-x64;win10-arm64 - win10-$(Platform).pubxml + x86;x64 + win10-x86;win10-x64 + Properties/PublishProfiles/win10-$(Platform).pubxml true true Assets/as_ico_win11.ico - None + None false true Awqat Salaat WinUI + + + $(DefineConstants);PACKAGED + + True + + Assets\sign_key.snk + - + + + + + + + @@ -46,8 +60,9 @@ - + + diff --git a/AwqatSalaat.WinUI/Controls/CustomizedFlyout.cs b/AwqatSalaat.WinUI/Controls/CustomizedFlyout.cs index 5b81cf6..1a6bad0 100644 --- a/AwqatSalaat.WinUI/Controls/CustomizedFlyout.cs +++ b/AwqatSalaat.WinUI/Controls/CustomizedFlyout.cs @@ -13,6 +13,8 @@ public class CustomizedFlyout : Flyout private bool xamlRootHadChanges; private FrameworkElement target; private bool hasClosed; + private bool isFirstTime = true; + private Control flyoutPresenter; public bool ClosedBecauseOfResize { get; private set; } @@ -41,6 +43,8 @@ protected override Control CreatePresenter() presenter.MaxWidth = maxPresenterWidth; } + flyoutPresenter = presenter; + return presenter; } @@ -103,6 +107,14 @@ private void CustomizedFlyout_Opened(object sender, object e) xamlRoot = XamlRoot; xamlRoot.Changed += XamlRoot_Changed; } + + var popup = flyoutPresenter.Parent as Popup; + + if (isFirstTime) + { + popup.GotFocus += (_, _) => flyoutPresenter?.Focus(FocusState.Programmatic); + isFirstTime = false; + } } private void XamlRoot_Changed(XamlRoot sender, XamlRootChangedEventArgs args) diff --git a/AwqatSalaat.WinUI/Controls/GridEx.cs b/AwqatSalaat.WinUI/Controls/GridEx.cs new file mode 100644 index 0000000..3ff0c3d --- /dev/null +++ b/AwqatSalaat.WinUI/Controls/GridEx.cs @@ -0,0 +1,12 @@ +using Microsoft.UI.Input; +using Microsoft.UI.Xaml.Controls; + +namespace AwqatSalaat.WinUI.Controls +{ + internal class GridEx : Grid + { + public void SetCursor(InputSystemCursorShape cursorShape) => ProtectedCursor = InputSystemCursor.Create(cursorShape); + + public void ResetCursor() => ProtectedCursor = null; + } +} diff --git a/AwqatSalaat.WinUI/Helpers/MessageBox.cs b/AwqatSalaat.WinUI/Helpers/MessageBox.cs new file mode 100644 index 0000000..79fbd35 --- /dev/null +++ b/AwqatSalaat.WinUI/Helpers/MessageBox.cs @@ -0,0 +1,194 @@ +using AwqatSalaat.Helpers; +using AwqatSalaat.Interop; +using AwqatSalaat.Properties; +using System; + +namespace AwqatSalaat.WinUI.Helpers +{ + internal static class MessageBox + { + #region Info + public static MessageBoxResult Info(string message) + { + return Info(message, MessageBoxButtons.MB_OK); + } + + public static MessageBoxResult Info(string message, MessageBoxButtons button) + { + return Info(message, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Info(string message, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Info(message, Resources.Data_AppName, button, defaultResult); + } + + public static MessageBoxResult Info(string message, string caption) + { + return Info(message, caption, MessageBoxButtons.MB_OK); + } + + public static MessageBoxResult Info(string message, string caption, MessageBoxButtons button) + { + return Info(message, caption, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Info(string message, string caption, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Show(message, caption, button, MessageBoxIcon.MB_ICONINFORMATION, defaultResult); + } + #endregion + + #region Question + public static MessageBoxResult Question(string message) + { + return Question(message, MessageBoxButtons.MB_YESNO); + } + + public static MessageBoxResult Question(string message, MessageBoxButtons button) + { + return Question(message, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Question(string message, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Question(message, Resources.Data_AppName, button, defaultResult); + } + + public static MessageBoxResult Question(string message, string caption) + { + return Question(message, caption, MessageBoxButtons.MB_YESNO); + } + + public static MessageBoxResult Question(string message, string caption, MessageBoxButtons button) + { + return Question(message, caption, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Question(string message, string caption, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Show(message, caption, button, MessageBoxIcon.MB_ICONQUESTION, defaultResult); + } + #endregion + + #region Warning + public static MessageBoxResult Warning(string message) + { + return Warning(message, MessageBoxButtons.MB_OK); + } + + public static MessageBoxResult Warning(string message, MessageBoxButtons button) + { + return Warning(message, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Warning(string message, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Warning(message, Resources.Data_AppName, button, defaultResult); + } + + public static MessageBoxResult Warning(string message, string caption) + { + return Warning(message, caption, MessageBoxButtons.MB_OK); + } + + public static MessageBoxResult Warning(string message, string caption, MessageBoxButtons button) + { + return Warning(message, caption, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Warning(string message, string caption, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Show(message, caption, button, MessageBoxIcon.MB_ICONWARNING, defaultResult); + } + #endregion + + #region Error + public static MessageBoxResult Error(string message) + { + return Error(message, MessageBoxButtons.MB_OK); + } + + public static MessageBoxResult Error(string message, MessageBoxButtons button) + { + return Error(message, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Error(string message, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Error(message, Resources.Data_AppName, button, defaultResult); + } + + public static MessageBoxResult Error(string message, string caption) + { + return Error(message, caption, MessageBoxButtons.MB_OK); + } + + public static MessageBoxResult Error(string message, string caption, MessageBoxButtons button) + { + return Error(message, caption, button, MessageBoxResult.NONE); + } + + public static MessageBoxResult Error(string message, string caption, MessageBoxButtons button, MessageBoxResult defaultResult) + { + return Show(message, caption, button, MessageBoxIcon.MB_ICONERROR, defaultResult); + } + #endregion + + private static MessageBoxResult Show(string message, string caption, MessageBoxButtons button, MessageBoxIcon icon, MessageBoxResult defaultResult) + { + return Show(IntPtr.Zero, message, caption, button, icon, defaultResult); + } + + private static MessageBoxResult Show(IntPtr HWND, string message, string caption, MessageBoxButtons button, MessageBoxIcon icon, MessageBoxResult defaultResult) + { + var options = MessageBoxOptions.NONE; + + if (LocaleManager.Default.CurrentCulture.TextInfo.IsRightToLeft) + { + options |= MessageBoxOptions.MB_RIGHT | MessageBoxOptions.MB_RTLREADING; + } + + uint type = (uint)button | (uint)icon | DefaultResultToButtonNumber(defaultResult, button) | (uint)options; + + return (MessageBoxResult)User32.MessageBox(HWND, message, caption, type); + } + + private static uint DefaultResultToButtonNumber(MessageBoxResult result, MessageBoxButtons button) + { + if (result == MessageBoxResult.NONE) + { + return 0; + } + + switch (button) + { + case MessageBoxButtons.MB_OK: + return 0; + case MessageBoxButtons.MB_OKCANCEL: + if (result == MessageBoxResult.IDCANCEL) + { + return 256; + } + + return 0; + case MessageBoxButtons.MB_YESNO: + if (result == MessageBoxResult.IDNO) + { + return 256; + } + + return 0; + case MessageBoxButtons.MB_YESNOCANCEL: + return result switch + { + MessageBoxResult.IDNO => 256, + MessageBoxResult.IDCANCEL => 512, + _ => 0, + }; + default: + return 0; + } + } + } +} diff --git a/AwqatSalaat.WinUI/Helpers/StartupSettings.cs b/AwqatSalaat.WinUI/Helpers/StartupSettings.cs new file mode 100644 index 0000000..b269626 --- /dev/null +++ b/AwqatSalaat.WinUI/Helpers/StartupSettings.cs @@ -0,0 +1,105 @@ +using AwqatSalaat.Helpers; +using IWshRuntimeLibrary; +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Windows.ApplicationModel; + +namespace AwqatSalaat.WinUI.Helpers +{ + public class StartupSettings : ObservableObject + { +#if PACKAGED + private bool launchOnStartup; + private bool canSetLaunchOnStartup; + + public bool CanSetLaunchOnStartup { get => canSetLaunchOnStartup; private set => SetProperty(ref canSetLaunchOnStartup, value); } + public bool LaunchOnStartup { get => canSetLaunchOnStartup && launchOnStartup; set => SetProperty(ref launchOnStartup, value); } +#else + public bool CanSetLaunchOnStartup => true; + public bool LaunchOnStartup + { + get => Properties.Settings.Default.LaunchOnWindowsStartup; + set => Properties.Settings.Default.LaunchOnWindowsStartup = value; + } +#endif + +#if PACKAGED + public async Task VerifyStartupTask() + { + StartupTask startupTask = await StartupTask.GetAsync("AwqatSalaatStartupTask"); + CanSetLaunchOnStartup = startupTask.State is StartupTaskState.Enabled or StartupTaskState.Disabled; + + LaunchOnStartup = canSetLaunchOnStartup && startupTask.State == StartupTaskState.Enabled; + } +#else + public StartupSettings() + { + Properties.Settings.Default.PropertyChanged += Settings_PropertyChanged; + } + + private void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Properties.Settings.Default.LaunchOnWindowsStartup)) + { + OnPropertyChanged(nameof(LaunchOnStartup)); + } + } + + ~StartupSettings() + { + Properties.Settings.Default.PropertyChanged -= Settings_PropertyChanged; + } +#endif + + public async Task Commit() + { +#if PACKAGED + if (canSetLaunchOnStartup) + { + StartupTask startupTask = await StartupTask.GetAsync("AwqatSalaatStartupTask"); + + if (launchOnStartup) + { + await startupTask.RequestEnableAsync(); + } + else + { + startupTask.Disable(); + } + } +#else + var process = System.Diagnostics.Process.GetCurrentProcess(); + var moduleInfo = process.MainModule.FileVersionInfo; + var shortcutPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), moduleInfo.ProductName + ".lnk"); + + if (LaunchOnStartup) + { + WshShell wshShell = new WshShell(); + + // Create the shortcut + IWshShortcut shortcut = (IWshShortcut)wshShell.CreateShortcut(shortcutPath); + + shortcut.TargetPath = moduleInfo.FileName; + shortcut.WorkingDirectory = Path.GetDirectoryName(moduleInfo.FileName); + shortcut.Description = $"Launch {moduleInfo.ProductName}"; + shortcut.Save(); + } + else + { + try + { + System.IO.File.Delete(shortcutPath); + } + catch (Exception ex) + { +#if DEBUG + throw; +#endif + } + } +#endif + } + } +} diff --git a/AwqatSalaat.WinUI/Media/DesktopAcrylicSystemBackdrop.cs b/AwqatSalaat.WinUI/Media/DesktopAcrylicSystemBackdrop.cs new file mode 100644 index 0000000..ffb4b37 --- /dev/null +++ b/AwqatSalaat.WinUI/Media/DesktopAcrylicSystemBackdrop.cs @@ -0,0 +1,38 @@ +using Microsoft.UI.Composition; +using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; + +namespace AwqatSalaat.WinUI.Media +{ + internal class DesktopAcrylicSystemBackdrop : SystemBackdrop + { + private DesktopAcrylicController acrylicController; + + protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot) + { + // Call the base method to initialize the default configuration object. + base.OnTargetConnected(connectedTarget, xamlRoot); + + if (acrylicController is null) + { + acrylicController = new DesktopAcrylicController(); + // Set configuration. + SystemBackdropConfiguration defaultConfig = GetDefaultSystemBackdropConfiguration(connectedTarget, xamlRoot); + defaultConfig.IsInputActive = true; + acrylicController.SetSystemBackdropConfiguration(defaultConfig); + } + + // Add target. + acrylicController.AddSystemBackdropTarget(connectedTarget); + } + + protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget) + { + base.OnTargetDisconnected(disconnectedTarget); + + acrylicController.RemoveSystemBackdropTarget(disconnectedTarget); + acrylicController = null; + } + } +} diff --git a/AwqatSalaat.WinUI/TaskBarManager.cs b/AwqatSalaat.WinUI/TaskBarManager.cs index 330d8df..a43d4ce 100644 --- a/AwqatSalaat.WinUI/TaskBarManager.cs +++ b/AwqatSalaat.WinUI/TaskBarManager.cs @@ -16,18 +16,21 @@ internal static class TaskBarManager private static PopupMenuItem showItem; private static PopupMenuItem hideItem; private static PopupMenuItem repositionItem; + private static PopupMenuItem manualPositionItem; private static PopupMenuItem quitItem; public static IntPtr CurrentWidgetHandle => taskBarWidget?.Handle ?? throw new InvalidOperationException("The taskbar widget is missing."); public static ICommand ShowWidget { get; } public static ICommand HideWidget { get; } public static ICommand RepositionWidget { get; } + public static ICommand ManuallyPositionWidget { get; } static TaskBarManager() { ShowWidget = new RelayCommand(static o => ShowWidgetExecute()); HideWidget = new RelayCommand(static o => HideWidgetExecute()); - RepositionWidget = new RelayCommand(static o => taskBarWidget?.UpdatePosition()); + RepositionWidget = new RelayCommand(static o => taskBarWidget?.UpdatePosition(true)); + ManuallyPositionWidget = new RelayCommand(static o => taskBarWidget?.StartDragging()); App.Quitting += App_Quitting; LocaleManager.Default.CurrentChanged += (_, _) => UpdateTrayIconLocalization(); @@ -41,7 +44,8 @@ public static void Initialize(DispatcherQueue dispatcherQueue) showItem = new PopupMenuItem("Show", (_, _) => dispatcher.TryEnqueue(ShowWidgetExecute)); hideItem = new PopupMenuItem("Hide", (_, _) => dispatcher.TryEnqueue(HideWidgetExecute)); - repositionItem = new PopupMenuItem("Re-position", (_, _) => taskBarWidget?.UpdatePosition()); + repositionItem = new PopupMenuItem("Re-position", (_, _) => taskBarWidget?.UpdatePosition(true)); + manualPositionItem = new PopupMenuItem("Manual position", (_, _) => dispatcher.TryEnqueue(() => taskBarWidget?.StartDragging())); quitItem = new PopupMenuItem("Quit", (_, _) => dispatcher.TryEnqueue(() => App.Quit.Execute(null))); trayIcon = new TrayIconWithContextMenu() @@ -52,7 +56,9 @@ public static void Initialize(DispatcherQueue dispatcherQueue) { showItem, hideItem, + new PopupMenuSeparator(), repositionItem, + manualPositionItem, new PopupMenuSeparator(), quitItem, } @@ -83,11 +89,15 @@ private static void ShowWidgetExecute() { var widget = new TaskBarWidget(); + widget.Destroying += Widget_Destroying; + widget.Initialize(); widget.Show(); taskBarWidget = widget; + + UpdateTrayMenuItemsStates(true); } } @@ -104,6 +114,12 @@ private static void HideWidgetExecute() } } + private static void Widget_Destroying(object sender, EventArgs e) + { + (sender as TaskBarWidget).Destroying -= Widget_Destroying; + UpdateTrayMenuItemsStates(false); + } + private static void OnTaskbarCreated() { try @@ -122,12 +138,21 @@ private static void OnTaskbarCreated() ShowWidgetExecute(); } + private static void UpdateTrayMenuItemsStates(bool isWidgetVisible) + { + showItem.Enabled = !isWidgetVisible; + hideItem.Enabled = isWidgetVisible; + repositionItem.Enabled = isWidgetVisible; + manualPositionItem.Enabled = isWidgetVisible; + } + private static void UpdateTrayIconLocalization() { trayIcon.UpdateToolTip(LocaleManager.Default.Get("Data.AppName")); showItem.Text = LocaleManager.Default.Get("UI.ContextMenu.Show"); hideItem.Text = LocaleManager.Default.Get("UI.ContextMenu.Hide"); repositionItem.Text = LocaleManager.Default.Get("UI.ContextMenu.Reposition"); + manualPositionItem.Text = LocaleManager.Default.Get("UI.ContextMenu.ManualPosition"); quitItem.Text = LocaleManager.Default.Get("UI.ContextMenu.Quit"); trayIcon.ContextMenu.RightToLeft = LocaleManager.Default.CurrentCulture.TextInfo.IsRightToLeft; diff --git a/AwqatSalaat.WinUI/TaskBarWidget.cs b/AwqatSalaat.WinUI/TaskBarWidget.cs index 806fea4..d38aac1 100644 --- a/AwqatSalaat.WinUI/TaskBarWidget.cs +++ b/AwqatSalaat.WinUI/TaskBarWidget.cs @@ -1,9 +1,11 @@ using AwqatSalaat.Helpers; using AwqatSalaat.Interop; +using AwqatSalaat.WinUI.Controls; using AwqatSalaat.WinUI.Views; using Microsoft.UI; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Xaml.Input; using System; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -29,8 +31,7 @@ internal class TaskBarWidget : IDisposable private readonly IntPtr hwndTrayNotify; private readonly IntPtr hwndReBar; - private RECT taskbarRect; - private TaskbarStructureWatcher taskbarWatcher; + private readonly TaskbarStructureWatcher taskbarWatcher; private IntPtr hwnd; private AppWindow appWindow; @@ -40,6 +41,10 @@ internal class TaskBarWidget : IDisposable private int WidgetHostWidth; private int currentOffsetX = int.MinValue; private int currentOffsetY = 0; + private bool isDragging; + private int draggingInnerOffsetX; + private int lastCursorPositionX; + private bool initialized; private bool disposedValue; public IntPtr Handle => hwnd != IntPtr.Zero ? hwnd : throw new InvalidOperationException("The widget is not initialized."); @@ -55,6 +60,10 @@ public TaskBarWidget() var dpi = User32.GetDpiForWindow(hwndShell); dpiScale = dpi / 96d; WidgetHostWidth = (int)Math.Ceiling(dpiScale * DefaultWidgetHostWidth); + + taskbarWatcher = new TaskbarStructureWatcher(hwndShell, hwndReBar); + taskbarWatcher.TaskbarChangedNotificationStarted += TaskbarWatcher_TaskbarChangedNotificationStarted; + taskbarWatcher.TaskbarChangedNotificationCompleted += TaskbarWatcher_TaskbarChangedNotificationCompleted; } public void Initialize() @@ -69,20 +78,54 @@ public void Initialize() appWindow.IsShownInSwitchers = false; appWindow.Destroying += AppWindow_Destroying; - taskbarRect = SystemInfos.GetTaskBarBounds(); + var taskbarRect = SystemInfos.GetTaskBarBounds(); appWindow.ResizeClient(new SizeInt32(WidgetHostWidth, taskbarRect.bottom - taskbarRect.top)); host.Initialize(id); host.SiteBridge.ResizePolicy = Microsoft.UI.Content.ContentSizePolicy.ResizeContentToParentWindow; widgetSummary = new WidgetSummary() { Margin = new Microsoft.UI.Xaml.Thickness(4, 0, 4, 0), MaxHeight = 40 }; widgetSummary.DisplayModeChanged += WidgetSummary_DisplayModeChanged; - host.Content = widgetSummary; + host.Content = new GridEx + { + Children = { widgetSummary }, + Background = new Microsoft.UI.Xaml.Media.SolidColorBrush(Colors.Transparent) + }; InjectIntoTaskbar(); - taskbarWatcher = new TaskbarStructureWatcher(hwndShell, UpdatePositionImpl); + UpdatePositionImpl(TaskbarChangeReason.None); + + initialized = true; + } + + private void TaskbarWatcher_TaskbarChangedNotificationStarted(object sender, TaskbarChangedEventArgs e) + { + if (e.IsTaskbarHidden || !initialized) + { + e.Canceled = true; + return; + } + + int savedOffsetX = Properties.Settings.Default.CustomPosition; - UpdatePositionImpl(); + // -1 means the user didn't set manual position + if (savedOffsetX == -1 && e.Reason == TaskbarChangeReason.Alignment) + { + // This only to make the widget show an animation :) + widgetSummary.DispatcherQueue.TryEnqueue(() => (host.Content as GridEx).Children.Clear()); + } + else if (savedOffsetX > -1 && e.Reason != TaskbarChangeReason.TabletMode) + { + e.Canceled = true; + } + } + + private void TaskbarWatcher_TaskbarChangedNotificationCompleted(object sender, TaskbarChangedEventArgs e) + { + if (initialized) + { + UpdatePositionImpl(e.Reason, e.IsTaskbarCentered, e.IsTaskbarWidgetsEnabled); + } } private void InjectIntoTaskbar() @@ -100,7 +143,7 @@ private void InjectIntoTaskbar() System.Threading.Thread.Sleep(1000); } - + throw new WidgetNotInjectedException("Could not inject the widget into the taskbar.\nThe taskbar may be in use."); } @@ -135,110 +178,249 @@ private void AppWindow_Destroying(AppWindow sender, object args) public void Destroy() => appWindow.Destroy(); - public void UpdatePosition() => Task.Run(UpdatePositionImpl); + public void UpdatePosition(bool force = false, TaskbarChangeReason reason = TaskbarChangeReason.None) + { + if (force && Properties.Settings.Default.CustomPosition != -1) + { + Properties.Settings.Default.CustomPosition = -1; + + if (Properties.Settings.Default.IsConfigured) + { + Properties.Settings.Default.Save(); + } + } - private void UpdatePositionImpl() + Task.Run(() => UpdatePositionImpl(reason)); + } + + private void UpdatePositionImpl(TaskbarChangeReason changeReason) { bool isCentered = SystemInfos.IsTaskBarCentered(); bool isWidgetsEnabled = SystemInfos.IsTaskBarWidgetsEnabled(); - int offsetX = 0; - bool osRTL = System.Globalization.CultureInfo.InstalledUICulture.TextInfo.IsRightToLeft; + UpdatePositionImpl(changeReason, isCentered, isWidgetsEnabled); + } - User32.GetWindowRect(hwndTrayNotify, out RECT trayNotifyRect); + private void UpdatePositionImpl(TaskbarChangeReason changeReason, bool isCentered, bool isWidgetsEnabled) + { + int offsetX = Properties.Settings.Default.CustomPosition; + bool osRTL = System.Globalization.CultureInfo.InstalledUICulture.TextInfo.IsRightToLeft; - IntPtr isAutoHidePtr = User32.GetProp(hwndShell, "IsAutoHideEnabled"); - bool autoHide = isAutoHidePtr == (IntPtr)1; + User32.GetWindowRect(hwndShell, out RECT taskbarRect); - if (autoHide) + // -1 means the user didn't set manual position, so we have to find the best one + if (offsetX == -1) { - User32.GetWindowRect(hwndShell, out var newRect); - bool isHidden = newRect.top > taskbarRect.top; + var widgetsButton = isWidgetsEnabled ? taskbarWatcher.GetAutomationElement(WidgetsButtonAutomationId) : null; + User32.GetWindowRect(hwndTrayNotify, out RECT trayNotifyRect); - if (isHidden) + if (isCentered) { - return; + if (osRTL) + { + offsetX = (widgetsButton?.CurrentBoundingRectangle.left ?? taskbarRect.right) - WidgetHostWidth; + } + else + { + offsetX = widgetsButton?.CurrentBoundingRectangle.right ?? 0; + } + } + else + { + if (osRTL) + { + if (widgetsButton is not null && (widgetsButton.CurrentBoundingRectangle.left - trayNotifyRect.right) < WidgetHostWidth) + { + offsetX = widgetsButton.CurrentBoundingRectangle.right; + } + else + { + offsetX = trayNotifyRect.right; + } + } + else + { + if (widgetsButton is not null && (trayNotifyRect.left - widgetsButton.CurrentBoundingRectangle.right) < WidgetHostWidth) + { + offsetX = widgetsButton.CurrentBoundingRectangle.left - WidgetHostWidth; + } + else + { + offsetX = trayNotifyRect.left - WidgetHostWidth; + } + } } - } - if (isCentered) - { - var widgetsButton = isWidgetsEnabled ? taskbarWatcher.GetAutomationElement(WidgetsButtonAutomationId) : null; + try + { + List wnds = GetOtherInjectedWindows(); + + foreach (var wnd in wnds) + { + User32.GetWindowRect(wnd, out var bounds); + + if (bounds.right < offsetX || bounds.left > (offsetX + WidgetHostWidth)) + { + continue; + } + + if (isCentered == osRTL) + { + offsetX = bounds.left - WidgetHostWidth; + } + else + { + offsetX = bounds.right; + } + } + } + catch (Exception ex) + { +#if DEBUG + throw; +#endif + } if (osRTL) { - offsetX = (widgetsButton?.CurrentBoundingRectangle.left ?? taskbarRect.right) - WidgetHostWidth; + offsetX = Math.Clamp(offsetX, trayNotifyRect.right, taskbarRect.right - WidgetHostWidth); } else { - offsetX = widgetsButton?.CurrentBoundingRectangle.right ?? 0; + offsetX = Math.Clamp(offsetX, 0, trayNotifyRect.left - WidgetHostWidth); } } - else + + User32.GetWindowRect(hwndReBar, out RECT barRect); + int offsetY = barRect.top - taskbarRect.top; + + if (currentOffsetY != offsetY) { - if (osRTL) + appWindow.MoveAndResize(new RectInt32(offsetX, offsetY, WidgetHostWidth, barRect.bottom - barRect.top)); + currentOffsetX = offsetX; + currentOffsetY = offsetY; + } + else if (currentOffsetX != offsetX) + { + appWindow.Move(new PointInt32(offsetX, offsetY)); + currentOffsetX = offsetX; + } + + // This only to make the widget show an animation :) + widgetSummary.DispatcherQueue.TryEnqueue(() => + { + var grid = host.Content as GridEx; + + if (changeReason == TaskbarChangeReason.Alignment || grid.Children.Count == 0) { - offsetX = trayNotifyRect.right; + grid.Children.Add(widgetSummary); } - else + }); + } + + public void StartDragging() + { + if (!isDragging) + { + widgetSummary.IsHitTestVisible = false; + User32.SetCursorPos(appWindow.Position.X + appWindow.Size.Width / 2, appWindow.Position.Y + appWindow.Size.Height / 2); + host.Content.KeyUp += Content_KeyUp; + host.Content.PointerPressed += Content_PointerPressed; + host.Content.PointerReleased += Content_PointerReleased; + (host.Content as GridEx).SetCursor(Microsoft.UI.Input.InputSystemCursorShape.SizeWestEast); + isDragging = true; + + // If the command is triggered from tray menu then we need to make the window focused to receive keyboard events + if (!host.HasFocus && hwnd != User32.GetForegroundWindow()) { - offsetX = trayNotifyRect.left - WidgetHostWidth; + User32.SetForegroundWindow(hwnd); } } + } - try + public void EndDragging(bool revert) + { + if (isDragging) { - List wnds = GetOtherInjectedWindows(); - - foreach (var wnd in wnds) + isDragging = false; + host.Content.ReleasePointerCaptures(); + host.Content.KeyUp -= Content_KeyUp; + host.Content.PointerMoved -= Content_PointerMoved; + host.Content.PointerPressed -= Content_PointerPressed; + host.Content.PointerReleased -= Content_PointerReleased; + (host.Content as GridEx).ResetCursor(); + widgetSummary.IsHitTestVisible = true; + + if (revert) { - User32.GetWindowRect(wnd, out var bounds); - - if (bounds.right < offsetX || bounds.left > (offsetX + WidgetHostWidth)) - { - continue; - } + appWindow.Move(new PointInt32(currentOffsetX, currentOffsetY)); + } + else + { + currentOffsetX = appWindow.Position.X; + Properties.Settings.Default.CustomPosition = currentOffsetX; - if (isCentered == osRTL) - { - offsetX = bounds.left - WidgetHostWidth; - } - else + if (Properties.Settings.Default.IsConfigured) { - offsetX = bounds.right; + Properties.Settings.Default.Save(); } } } - catch (Exception ex) + } + + private void Content_KeyUp(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Escape && isDragging) { -#if DEBUG - throw; -#endif + EndDragging(true); } + } + + private void Content_PointerReleased(object sender, PointerRoutedEventArgs e) + { + EndDragging(false); + } + + private void Content_PointerPressed(object sender, PointerRoutedEventArgs e) + { + e.Handled = true; + host.Content.PointerMoved += Content_PointerMoved; + host.Content.CapturePointer(e.Pointer); + User32.GetCursorPos(out var lpPoint); + lastCursorPositionX = lpPoint.x; + draggingInnerOffsetX = lpPoint.x - appWindow.Position.X; + } + + private void Content_PointerMoved(object sender, PointerRoutedEventArgs e) + { + User32.GetWindowRect(hwndShell, out RECT taskbarRect); + User32.GetWindowRect(hwndTrayNotify, out RECT trayNotifyRect); + User32.GetCursorPos(out var lpPoint); + + int minCursorX, maxCursorX; + bool osRTL = System.Globalization.CultureInfo.InstalledUICulture.TextInfo.IsRightToLeft; if (osRTL) { - offsetX = Math.Clamp(offsetX, trayNotifyRect.right, taskbarRect.right - WidgetHostWidth); + minCursorX = trayNotifyRect.right + draggingInnerOffsetX; + maxCursorX = taskbarRect.right - WidgetHostWidth + draggingInnerOffsetX; } else { - offsetX = Math.Clamp(offsetX, 0, trayNotifyRect.left - WidgetHostWidth); + minCursorX = draggingInnerOffsetX; + maxCursorX = trayNotifyRect.left - WidgetHostWidth + draggingInnerOffsetX; } - User32.GetWindowRect(hwndReBar, out RECT barRect); - int offsetY = barRect.top - taskbarRect.top; + lpPoint.x = Math.Clamp(lpPoint.x, minCursorX, maxCursorX); - if (currentOffsetY != offsetY) - { - appWindow.MoveAndResize(new RectInt32(offsetX, offsetY, WidgetHostWidth, barRect.bottom - barRect.top)); - currentOffsetX = offsetX; - currentOffsetY = offsetY; - } - else if (currentOffsetX != offsetX) - { - appWindow.Move(new PointInt32(offsetX, offsetY)); - currentOffsetX = offsetX; - } + int delta = lpPoint.x - lastCursorPositionX; + int newX = delta + appWindow.Position.X; + + appWindow.Move(new PointInt32(newX, currentOffsetY)); + lastCursorPositionX = lpPoint.x; + + // This is necessary to make sure the content can raise keyboard events + host.Content.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); } private void RegisterWindowClass() @@ -264,7 +446,7 @@ private IntPtr CreateHostWindow(IntPtr parent) dwStyle: WindowStyles.WS_POPUP, x: 0, y: 0, nWidth: 0, nHeight: 0, - hWndParent: parent, + hWndParent: IntPtr.Zero,// parent, // Setting parent was causing reentrancy issue on Windows 10 hMenu: IntPtr.Zero, hInstance: IntPtr.Zero, lpParam: IntPtr.Zero); @@ -329,6 +511,7 @@ static bool IsSystemWindow(string className) private IntPtr WindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam) { + const int ENDSESSION_CLOSEAPP = 0x00000001; var msg = (WindowMessage)uMsg; if (msg == WindowMessage.WM_SETTINGCHANGE && lParam != IntPtr.Zero) @@ -337,9 +520,14 @@ private IntPtr WindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam) if (area is "UserInteractionMode" or "ConvertibleSlateMode") { - UpdatePosition(); + UpdatePosition(reason: TaskbarChangeReason.TabletMode); } } + else if (msg == WindowMessage.WM_QUERYENDSESSION && ((lParam.ToInt32() & ENDSESSION_CLOSEAPP) == ENDSESSION_CLOSEAPP)) + { + // The app is being updated so we should restart + Kernel32.RegisterApplicationRestart(null); + } return User32.DefWindowProc(hWnd, uMsg, wParam, lParam); } @@ -352,6 +540,8 @@ protected virtual void Dispose(bool disposing) { // TODO: dispose managed state (managed objects) widgetSummary.DisplayModeChanged -= WidgetSummary_DisplayModeChanged; + taskbarWatcher.TaskbarChangedNotificationStarted -= TaskbarWatcher_TaskbarChangedNotificationStarted; + taskbarWatcher.TaskbarChangedNotificationCompleted -= TaskbarWatcher_TaskbarChangedNotificationCompleted; taskbarWatcher.Dispose(); host.Dispose(); } @@ -363,12 +553,12 @@ protected virtual void Dispose(bool disposing) } } - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TaskBarWidget() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } + // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~TaskBarWidget() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } public void Dispose() { diff --git a/AwqatSalaat.WinUI/TaskbarStructureWatcher.cs b/AwqatSalaat.WinUI/TaskbarStructureWatcher.cs index 2b36e04..af7865e 100644 --- a/AwqatSalaat.WinUI/TaskbarStructureWatcher.cs +++ b/AwqatSalaat.WinUI/TaskbarStructureWatcher.cs @@ -1,4 +1,11 @@ -using System; +using AwqatSalaat.Helpers; +using AwqatSalaat.Interop; +using Microsoft.UI; +using Microsoft.UI.Windowing; +using System; +using System.Management; +using System.Security.Principal; +using System.Threading; using System.Threading.Tasks; using UIAutomationClient; @@ -6,19 +13,117 @@ namespace AwqatSalaat.WinUI { internal class TaskbarStructureWatcher : IUIAutomationStructureChangedEventHandler, IUIAutomationPropertyChangedEventHandler, IDisposable { + private class UpdateContext + { + // The values are based on clock resolution which is ~15 ms + // The delays give time to animations to play + private const int DefaultDelay = 60; + // When taskbar alignment change we need more time for animations + private const int AlignmentChangeShortDelay = 105; + // When taskbar alignment change while Widgets button enabled + // we need even more time because the final position of the button + // is only known after the end of its translation animation + private const int AlignmentChangeLongDelay = 360; + + public readonly DateTime Time = DateTime.Now; + public readonly TaskbarChangeReason Reason; + public readonly TaskbarChangedEventArgs EventArgs; + public readonly int Delay = DefaultDelay; + + public UpdateContext(TaskbarChangedEventArgs eventArgs) + { + EventArgs = eventArgs; + Reason = eventArgs.Reason; + + if (Reason == TaskbarChangeReason.Alignment) + { + Delay = eventArgs.IsTaskbarWidgetsEnabled ? AlignmentChangeLongDelay : AlignmentChangeShortDelay; + } + } + + public int EstimateDelay(TaskbarChangeReason changeReason) + { + if (Reason == changeReason) + { + // We are estimating delay for the first invocation in the serie + return Delay; + } + else if (Delay > DefaultDelay) + { + // If an alignment change triggered the serie of events, we need to respect the initial delay + var delta = DateTime.Now - Time; + int temp = Delay - (int)delta.TotalMilliseconds; + + if (temp > 15) + { + return temp; + } + } + + return DefaultDelay; + } + } + private const int UIA_BoundingRectanglePropertyId = 30001; private const int UIA_AutomationIdPropertyId = 30011; private static readonly IUIAutomation pUIAutomation = new CUIAutomation(); + private readonly IntPtr hwndTaskbar; + private readonly IntPtr hwndReBar; private readonly IUIAutomationElement taskbarElement; - private readonly Action changeCallback; + private readonly object syncRoot = new object(); + + private int rebarGap; + private bool widgetsButtonEnabled; + private bool taskbarCentered; + private bool taskbarHidden; + private CancellationTokenSource updateCancellation; + private ManagementEventWatcher watcher; + private UpdateContext updateContext; + + // Notify listeners about a change happened in the taskbar + public event EventHandler TaskbarChangedNotificationStarted; + // Notify listeners about a change happened in the taskbar after handling + // the serie of events, if any, and after waiting for necessary delays + public event EventHandler TaskbarChangedNotificationCompleted; - public TaskbarStructureWatcher(IntPtr hwndTaskbar, Action changeCallback) + public TaskbarStructureWatcher(IntPtr hwndTaskbar, IntPtr hwndReBar) { + this.hwndTaskbar = hwndTaskbar; + this.hwndReBar = hwndReBar; taskbarElement = pUIAutomation.ElementFromHandle(hwndTaskbar); - this.changeCallback = changeCallback; + widgetsButtonEnabled = SystemInfos.IsTaskBarWidgetsEnabled(); + taskbarCentered = SystemInfos.IsTaskBarCentered(); + taskbarHidden = IsTaskbarHidden(); RegisterEventHandlers(); + CreateRegistryWatcher(); + + rebarGap = GetReBarGap(); + } + + private int GetReBarGap() + { + User32.GetWindowRect(hwndTaskbar, out var taskbarRect); + User32.GetWindowRect(hwndReBar, out var rebarRect); + return rebarRect.top - taskbarRect.top; + } + + private bool IsTaskbarHidden() + { + IntPtr isAutoHidePtr = User32.GetProp(hwndTaskbar, "IsAutoHideEnabled"); + bool autoHide = isAutoHidePtr == (IntPtr)1; + + if (!autoHide) + { + return false; + } + + WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwndTaskbar); + var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Primary); + User32.GetWindowRect(hwndTaskbar, out var taskbarRect); + + return taskbarRect.bottom > displayArea.OuterBounds.Height; } public IUIAutomationElement GetAutomationElement(string automationId) @@ -36,7 +141,7 @@ void IUIAutomationStructureChangedEventHandler.HandleStructureChangedEvent(IUIAu { if (sender.CurrentName == taskbarElement.CurrentName) { - changeCallback?.Invoke(); + RaiseNotification(); } } @@ -44,7 +149,103 @@ void IUIAutomationPropertyChangedEventHandler.HandlePropertyChangedEvent(IUIAuto { if (sender.CurrentName == taskbarElement.CurrentName) { - changeCallback?.Invoke(); + RaiseNotification(); + } + } + + // Try to determine the reason of taskbar change and raise notification. + private void RaiseNotification() + { + bool isWidgetsButtonEnabled = SystemInfos.IsTaskBarWidgetsEnabled(); + bool isTaskbarCentered = SystemInfos.IsTaskBarCentered(); + bool isTaskbarHidden = IsTaskbarHidden(); + // The gap between Shell_TrayWnd and ReBarWindow32 change when Table Mode is enabled/disabled. + int gap = GetReBarGap(); + + TaskbarChangeReason reason = TaskbarChangeReason.Other; + + if (isWidgetsButtonEnabled != widgetsButtonEnabled) + { + reason = TaskbarChangeReason.WidgetsButton; + widgetsButtonEnabled = isWidgetsButtonEnabled; + } + else if (isTaskbarCentered != taskbarCentered) + { + reason = TaskbarChangeReason.Alignment; + taskbarCentered = isTaskbarCentered; + } + else if (gap != rebarGap) + { + reason = TaskbarChangeReason.TabletMode; + rebarGap = gap; + } + else if (isTaskbarHidden != taskbarHidden) + { + reason = TaskbarChangeReason.Visibility; + taskbarHidden = isTaskbarHidden; + } + + RaiseNotification(reason); + } + + // Note: In some circumstances, a change in the taskbar triggers a serie of + // events when the change is observed by the UIAutomation system. + // Thus we try to avoid handling all events since one notification is enough. + private void RaiseNotification(TaskbarChangeReason changeReason) + { + lock (syncRoot) + { + // Only first invocation in the serie is needed to start notification + // Note that it's highly unexpected to have a serie of invocations triggered for different (initial) reasons. + if (updateContext is null) + { + var args = new TaskbarChangedEventArgs + { + Reason = changeReason, + IsTaskbarHidden = taskbarHidden, + IsTaskbarCentered = taskbarCentered, + IsTaskbarWidgetsEnabled = widgetsButtonEnabled + }; + updateContext = new UpdateContext(args); + TaskbarChangedNotificationStarted?.Invoke(this, args); + + if (args.Canceled) + { + updateContext = null; + return; + } + } + + // We want to make the last invocation to be the one that do the update + // because it will be the closest one to the actual state of the taskbar. + if (updateCancellation is not null) + { + updateCancellation.Cancel(); + updateCancellation.Dispose(); + } + + // Delay is needed to give time for some animations on the taskbar to take place. + // Also the Widgets button may translate to different location which make it + // report a wrong position if we don't wait for its animation to finish. + int delay = updateContext.EstimateDelay(changeReason); + + updateCancellation = new CancellationTokenSource(); + Task.Delay(delay, updateCancellation.Token).ContinueWith(RaiseNotificationCompletedIfSuccess); + } + } + + private void RaiseNotificationCompletedIfSuccess(Task delayTask) + { + lock (syncRoot) + { + // If the task didn't succeed then it means we canceled it to replace it with a more recent one. + if (delayTask.IsCompletedSuccessfully) + { + TaskbarChangedNotificationCompleted?.Invoke(this, updateContext.EventArgs); + updateCancellation?.Dispose(); + updateCancellation = null; + updateContext = null; + } } } @@ -65,9 +266,58 @@ private void UnregisterEventHandlers() } } + private void CreateRegistryWatcher() + { + var currentUser = WindowsIdentity.GetCurrent(); + + WqlEventQuery query = new WqlEventQuery( + "SELECT * FROM RegistryKeyChangeEvent WHERE " + + "Hive = 'HKEY_USERS' " + + @"AND KeyPath = '" + currentUser.User.Value + @"\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced'"); + + query.WithinInterval = new TimeSpan(0, 0, 0, 1); + + watcher = new ManagementEventWatcher(query); + watcher.EventArrived += new EventArrivedEventHandler(RegistryKeyChanged); + watcher.Start(); + } + + private void RegistryKeyChanged(object sender, EventArrivedEventArgs e) + { + bool isWidgetsButtonEnabled = SystemInfos.IsTaskBarWidgetsEnabled(); + bool isTaskbarCentered = SystemInfos.IsTaskBarCentered(); + + // We are only interested in registry notifications for widgets button change when taskbar is left-aligned + if ((isWidgetsButtonEnabled != widgetsButtonEnabled) && !isTaskbarCentered) + { + widgetsButtonEnabled = isWidgetsButtonEnabled; + RaiseNotification(TaskbarChangeReason.WidgetsButton); + } + } + public void Dispose() { Task.Run(UnregisterEventHandlers); + watcher?.Dispose(); } } + + public class TaskbarChangedEventArgs : EventArgs + { + public bool Canceled { get; set; } + public TaskbarChangeReason Reason { get; init; } + public bool IsTaskbarHidden { get; init; } + public bool IsTaskbarCentered { get; init; } + public bool IsTaskbarWidgetsEnabled { get; init; } + } + + public enum TaskbarChangeReason + { + None, // Used for manual re-position + Alignment, + Visibility, + WidgetsButton, + TabletMode, + Other + } } diff --git a/AwqatSalaat.WinUI/Views/SettingsPanel.xaml b/AwqatSalaat.WinUI/Views/SettingsPanel.xaml index c79b9a5..f63d975 100644 --- a/AwqatSalaat.WinUI/Views/SettingsPanel.xaml +++ b/AwqatSalaat.WinUI/Views/SettingsPanel.xaml @@ -38,6 +38,7 @@ + - - + + + + @@ -348,7 +348,7 @@ - + + + + + \ No newline at end of file diff --git a/AwqatSalaat/UI/Themes/Styles.xaml b/AwqatSalaat/UI/Themes/Styles.xaml index 0a6b951..0d257ff 100644 --- a/AwqatSalaat/UI/Themes/Styles.xaml +++ b/AwqatSalaat/UI/Themes/Styles.xaml @@ -17,5 +17,6 @@ + \ No newline at end of file diff --git a/AwqatSalaat/UI/Utils.cs b/AwqatSalaat/UI/Utils.cs index 272b225..b5a133d 100644 --- a/AwqatSalaat/UI/Utils.cs +++ b/AwqatSalaat/UI/Utils.cs @@ -1,5 +1,9 @@ -using System.Windows; +using System.Collections.Generic; +using System.Linq; +using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Interop; namespace AwqatSalaat.UI { @@ -66,5 +70,16 @@ public static bool RemoveFromParent(UIElement child, DependencyObject parent, ou return false; } + + // https://stackoverflow.com/a/32462688/4644774 + public static IEnumerable GetOpenPopups() + { + return PresentationSource.CurrentSources.OfType() + .Select(h => h.RootVisual) + .OfType() + .Select(f => f.Parent) + .OfType() + .Where(p => p.IsOpen); + } } } diff --git a/AwqatSalaat/UI/Views/SettingsPanel.xaml b/AwqatSalaat/UI/Views/SettingsPanel.xaml index 1a2d560..3b6b914 100644 --- a/AwqatSalaat/UI/Views/SettingsPanel.xaml +++ b/AwqatSalaat/UI/Views/SettingsPanel.xaml @@ -50,6 +50,11 @@ + + + + + @@ -123,6 +128,31 @@ + + + + + + + + + + + + + + + + + + @@ -336,27 +366,38 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AwqatSalaat/UI/Views/SettingsPanel.xaml.cs b/AwqatSalaat/UI/Views/SettingsPanel.xaml.cs index b8318de..23cd6a6 100644 --- a/AwqatSalaat/UI/Views/SettingsPanel.xaml.cs +++ b/AwqatSalaat/UI/Views/SettingsPanel.xaml.cs @@ -1,8 +1,10 @@ -using AwqatSalaat.UI.Controls; +using AwqatSalaat.Helpers; +using AwqatSalaat.UI.Controls; using AwqatSalaat.ViewModels; using Microsoft.Win32; using System; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Windows; using System.Windows.Controls; @@ -84,5 +86,52 @@ private void BrowseSound_Click(object sender, RoutedEventArgs e) ParentPopup.IsTopMost = true; } } + + private async void CheckForUpdatesClick(object sender, RoutedEventArgs e) + { + // MessageBox will make the popup disappear so we have to force it to stay open temporarily + var popup = Utils.GetOpenPopups().First(); + bool alteredPopup = false; + + if (popup != null && !popup.StaysOpen) + { + popup.StaysOpen = true; + alteredPopup = true; + } + + try + { + var current = System.Version.Parse(Version); +#if DEBUG + current = System.Version.Parse("1.0"); +#endif + var latest = await ViewModel.CheckForNewVersion(current); + + if (latest is null) + { + MessageBoxEx.Info(Properties.Resources.Dialog_WidgetUpToDate); + } + else + { + var result = MessageBoxEx.Question(string.Format(Properties.Resources.Dialog_NewUpdateAvailableFormat, latest.Tag)); + + if (result == MessageBoxResult.Yes) + { + Process.Start(new ProcessStartInfo(latest.HtmlUrl)); + } + } + } + catch (Exception ex) + { + MessageBoxEx.Error(Properties.Resources.Dialog_CheckingUpdatesFailed + $"\nError: {ex.Message}"); + } + finally + { + if (alteredPopup) + { + popup.StaysOpen = false; + } + } + } } } diff --git a/AwqatSalaat/UI/Views/WidgetPanel.xaml b/AwqatSalaat/UI/Views/WidgetPanel.xaml index cab5890..a284552 100644 --- a/AwqatSalaat/UI/Views/WidgetPanel.xaml +++ b/AwqatSalaat/UI/Views/WidgetPanel.xaml @@ -154,16 +154,51 @@ Grid.Row="1" Grid.ColumnSpan="2" Visibility="{Binding IsRefreshing, Converter={StaticResource BoolVisConverter}}"/> - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/AwqatSalaat/UI/Views/WidgetSummary.xaml.cs b/AwqatSalaat/UI/Views/WidgetSummary.xaml.cs index c9c4974..85dfc41 100644 --- a/AwqatSalaat/UI/Views/WidgetSummary.xaml.cs +++ b/AwqatSalaat/UI/Views/WidgetSummary.xaml.cs @@ -4,6 +4,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; +using System.Windows.Interop; using System.Windows.Media; namespace AwqatSalaat.UI.Views @@ -69,6 +70,11 @@ public WidgetSummary() InitializeComponent(); mediaPlayer.MediaEnded += (_, __) => mediaPlayer.Position = TimeSpan.Zero; + popup.Opened += (_, __) => + { + var src = HwndSource.FromVisual(popup.Child) as HwndSource; + (src.RootVisual as UIElement)?.Focus(); + }; popup.Closed += (_, __) => { if (ViewModel.WidgetSettings.IsOpen && ViewModel.WidgetSettings.Settings.IsConfigured) @@ -76,6 +82,14 @@ public WidgetSummary() ViewModel.WidgetSettings.Cancel.Execute(null); } }; + popup.KeyDown += (_, e) => + { + if (e.Key == System.Windows.Input.Key.Escape) + { + toggle.IsChecked = false; + e.Handled = true; + } + }; this.Loaded += (_, __) => UpdateDisplayMode(); this.Unloaded += WidgetSummary_Unloaded; ViewModel.WidgetSettings.Settings.PropertyChanged += Settings_PropertyChanged; diff --git a/CHANGELOG.ar.md b/CHANGELOG.ar.md index 59b86ed..d41d594 100644 --- a/CHANGELOG.ar.md +++ b/CHANGELOG.ar.md @@ -1,4 +1,18 @@ -### نسخة v3.2 +### نسخة v3.3 + +- جعل الأداة متوفرة على متجر مايكروسوفت. (نسخة WinUI فقط) +- تمكين سحب الأداة إلى موقع مخصص. (نسخة WinUI فقط) +- إضافة إعداد للتحكم في التاريخ الهجري. +- إضافة التحقق من التحديثات في صفحة **حول**. +- تفعيل الخلفية الضبابية. (نسخة WinUI فقط) +- تحسين تموقع الأداة. (نسخة WinUI فقط) +- إصلاح التنقل بين العناصر باستخدام مفتاح **Tab**. (نسخة Deskband فقط) +- جعل علب الرسائل تراعي اتجاه اللغة الحالية. +- إظهار تلميح عند تعطل الخدمة الحالية من أجل اقتراح حل محتمل. +- تحسين البحث عن الموقع باستخدام خدمة Nominatim. +- إصلاح عدة مشاكل مختلفة. + +### نسخة v3.2 - إضافة **الوضع المضغوط**. - اظهار وقت الشروق. diff --git a/CHANGELOG.md b/CHANGELOG.md index dba1800..f123f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +### v3.3 + +- Make the widget available on Microsoft Store. (WinUI only) +- Allow dragging the widget to custom position. (WinUI only) +- Add a setting to control Hijri date. +- Add updates checking in About page. +- Enable Acrylic background. (WinUI only) +- Improve widget positioning. (WinUI only) +- Fix navigation using Tab key. (Deskband only) +- Make MessageBox dialogs respect current language direction. +- Show a hint when the current service is down to suggest a potential solution. +- Improve location search using Nominatim. +- Various bug-fixes. + ### v3.2 - Add **Compact mode**. diff --git a/Directory.Build.props b/Directory.Build.props index 5344f65..607764d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ https://github.com/Khiro95/Awqat-Salaat.git git https://github.com/Khiro95/Awqat-Salaat - MIT License + MIT diff --git a/PRIVACY-POLICY.md b/PRIVACY-POLICY.md new file mode 100644 index 0000000..015b3be --- /dev/null +++ b/PRIVACY-POLICY.md @@ -0,0 +1,64 @@ +### Privacy Policy + +Last updated: September 29, 2024 + +This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights. + +We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. + +### Interpretation and Definitions + +#### Interpretation +The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural. + +#### Definitions + +- **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable. +- **Developer** (referred to as either "the Developer", "We", "Us" or "Our" in this Agreement) refers to Awqat Salaat's owner. +- **Service** refers to the Awqat Salaat widget. +- **Service Provider** means any natural or legal person who processes the data on behalf of the Developer. It refers to third-party services to perform services related to the Service. +- **Personal Data** is any information that relates to an identified or identifiable individual. +- **Non-Personal Data** is any information that does not relates to an identified or identifiable individual. +- **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet. + +### Collecting and Using Your Personal Data + +#### Types of Data Collected + +##### Personal Data + +While using Our Service, We may ask You to provide Us with certain personally identifiable information that are necessary for the Service. Personally identifiable information include: + +- Address, State, Province, ZIP/Postal code, City, Country + +We use this information to provide features of Our Service. The information may be uploaded to a Service Provider's server or it may be simply stored on Your device. + +We will never retain this information outside Your device. + +#### Use of Your Personal Data + +The Service may use Personal Data for the following purposes: + +- To manage Your requests. + +We may share Your personal information in the following situations: + +- **With Service Providers:** To provide You with features of Our Service. + +### Links to Other Websites + +Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit. + +We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. + +### Changes to this Privacy Policy + +We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page. + +You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. + +### Contact Us + +If you have any questions about this Privacy Policy, You can contact us: + +- By email: khiro95.gh@gmail.com \ No newline at end of file diff --git a/README.ar.md b/README.ar.md index 078fb7a..2b2729d 100644 --- a/README.ar.md +++ b/README.ar.md @@ -19,7 +19,17 @@ ## المنصات المدعومة -أوقات الصلاة هي أداة خاصة بنظام ويندوز وهي متوفرة في شكلين: +أوقات الصلاة هي أداة خاصة بنظام ويندوز وهي متوفرة في شكلين: ***Deskband*** (Awqat Salaat) و ***WinUI*** (Awqat Salaat WinUI). +وإليكم مقارنة سريعة بين الشكلين فيما يخص التوافق: + +| أوقات الصلاة WinUI | أوقات الصلاة | نظام التشغيل | +| --: | --: | --: | +| متوافق ✔ | غير متوافق ❌ | **ويندوز 11** | +| متوافق ✔⚠ | متوافق ✔ | **ويندوز 10** | +| غير متوافق ❌ | متوافق ✔ | **ويندوز 8/8.1** | +| غير متوافق ❌ | متوافق ✔ | **ويندوز 7** | + +⚠ أداة أوقات الصلاة WinUI لا تدعم نسخ ويندوز 10 الأقدم من 1809. ### أوقات الصلاة (Deskband) @@ -52,10 +62,17 @@ - الأداة لا يمكن إظهارها وإخفاءها من شريط المهام بنفسه. لكن تتوفر قائمة سياق وأيقونة علبة النظام من أجل التحكم في الأداة. - المستكشف (Explorer) لن يدير الأداة ولذلك يجب تشغيلها خارجيا عند تشغيل نظام ويندوز. يمكن ضبط هذا الأمر في الإعدادات. - على أجهزة 2-في-1، قد تحتاج إلى عمل إعادة تموقع يدوي للأداة بعد التبديل من/إلى وضع اللوحة. -- تأثير الخلفية الضبابية لا يعمل لسبب ما رغم أنه يعمل في حال لم يتم إقحام الأداة داخل شريط المهام. ## التثبيت +### من متجر مايكروسوفت + +أداة أوقات الصلاة WinUI متوفرة على متجر مايكروسوفت. + +
+ +### من منصة GitHub + اذهب إلى صفحة [الإصدارات](https://github.com/Khiro95/Awqat-Salaat/releases) وقم بتحميل برنامج التثبيت الموافق لهندسة نظام التشغيل لديك. > [!warning] @@ -63,6 +80,8 @@ بعد تحميل برنامج التثبيت قم بالضغط عليه مرتين لتثبيت الأداة. +### الخطوات + ***إذا اخترت أداة أوقات الصلاة WinUI فقم بتشغيل التطبيق واقفز إلى الخطوة 3.*** من المتوقع أن الأداة لن تظهر بعد نهاية التثبيت لذا يتعين عليك تفعيلها يدويا. إليك الخطوات: @@ -141,7 +160,6 @@ - الأداة تقوم بتخزين مؤقت لكل أوقات الشهر الحالي التي يتم الحصول عليها من خدمة واجهة برمجة التطبيقات وهذا لكي تعمل الأداة في وضع عدم الاتصال بالانترنت. - برنامج المعاينة يستعمل لأغراض التطوير فقط. -- التاريخ الهجري الذي يعرض على الأداة هو مزود من طرف مشغل البرنامج (runtime (.NET Framework/.NET)) وهذا التاريخ مبني على تقويم *أم القرى* ولهذا فقد لا يكون مطابقا في كل مكان. ## عرفان @@ -155,6 +173,10 @@ للاستفسار أو التبليغ عن أية مشكلة، يرجى فتح [مسألة جديدة](https://github.com/Khiro95/Awqat-Salaat/issues/new) أو إرسال بريد الكتروني إلى khiro95.gh@gmail.com. +## سياسة الخصوصية + +يمكن إيجاد التفاصيل [هنا](PRIVACY-POLICY.md) (متوفرة باللغة الانجليزية فقط حاليا). + ## الترخيص هذا المشروع مرخص بموجب بنود [ترخيص MIT](LICENSE). \ No newline at end of file diff --git a/README.md b/README.md index 819f805..f847eb0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,17 @@ So **DON'T** rely on the widget to get the exact time, especially for performing ## Supported Platforms -Awqat Salaat is a *Windows* widget that is available in two forms: +Awqat Salaat is a *Windows* widget that is available in two forms: ***Deskband*** (Awqat Salaat) and ***WinUI*** (Awqat Salaat WinUI). +Here is a quick compatibility comarison between both forms: + +| Operating System | Awqat Salaat | Awqat Salaat WinUI | +| --- | --- | --- | +| **Windows 11** | Incompatible ❌ | Compatible ✔ | +| **Windows 10** | Compatible ✔ | Compatible ✔⚠ | +| **Windows 8/8.1** | Compatible ✔ | Incompatible ❌ | +| **Windows 7** | Compatible ✔ | Incompatible ❌ | + +⚠ Awqat Salaat WinUI is not supported on Windows 10 versions older than 1809. ### Awqat Salaat (Deskband) @@ -37,7 +47,7 @@ This app bring Awqat Salaat to Windows 11 which wasn't supported in earlier vers > [!note] > Although this app can run on Windows 10, it's *not recommended* due to the limitiations listed below, use deskband widget instead. -#### Requirements: +#### Requirements - [.NET Desktop Runtime 6](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) - [Windows App Runtime 1.5](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads) @@ -53,10 +63,17 @@ This app bring Awqat Salaat to Windows 11 which wasn't supported in earlier vers - The widget cannot be shown/hidden from the taskbar itself. However, a context-menu and a system tray icon are available to control the widget. - Explorer will not manage the widget so it has to be launched externally at Windows startup. This can be configured in Settings. - On 2-in-1 devices, you *may* need to manually re-position the widget after switching to/from tablet mode. -- The blurred background effect doesn't work, for some reason, even though it works when the widget is not injected into the taskbar. ## Installation +### From Microsoft Store + +Awqat Salaat WinUI is available on Microsoft Store. + +[](https://apps.microsoft.com/detail/9nhh4c81fz0n?mode=full) + +### From GitHub + Go to [Releases](https://github.com/Khiro95/Awqat-Salaat/releases) page and download the installer that matches your OS architecture. > [!warning] @@ -64,6 +81,8 @@ Go to [Releases](https://github.com/Khiro95/Awqat-Salaat/releases) page and down After downloading the installer, double-click on it to install the widget. +### Steps + ***If you choose Awqat Salaat WinUI, launch the app and jump directly to step 3.*** It's expected that the widget will not appear after the installation finish, so you need to activate it manually. Here are the steps: @@ -143,7 +162,6 @@ For a list of changes, check the changelog [here](CHANGELOG.md). - The widget cache all the times of the current month, obtained from the API, so that it can work in offline mode. - The preview app is used for development purposes only. -- The Hijri date shown in the widget is provided by the runtime (.NET Framework/.NET) and is based on *Um Al Qura* calendar, thus it may not match the exact date everywhere. ## Acknowledgement @@ -157,6 +175,10 @@ For a list of changes, check the changelog [here](CHANGELOG.md). For any question or problem reporting, please consider opening a [new issue](https://github.com/Khiro95/Awqat-Salaat/issues/new) or send an email to khiro95.gh@gmail.com. +## Privacy Policy + +You can find details [here](PRIVACY-POLICY.md). + ## License This project is licensed under the terms of [MIT License](LICENSE). \ No newline at end of file diff --git a/images/as_win11_16.png b/images/as_win11_16.png index e4cc065..7888c62 100644 Binary files a/images/as_win11_16.png and b/images/as_win11_16.png differ diff --git a/images/elapsed_win11_ar.png b/images/elapsed_win11_ar.png index a440789..f0954d1 100644 Binary files a/images/elapsed_win11_ar.png and b/images/elapsed_win11_ar.png differ diff --git a/images/elapsed_win11_en.png b/images/elapsed_win11_en.png index f448e90..2aa1742 100644 Binary files a/images/elapsed_win11_en.png and b/images/elapsed_win11_en.png differ diff --git a/images/regular_open_win11_ar.png b/images/regular_open_win11_ar.png index 67c5023..13842ac 100644 Binary files a/images/regular_open_win11_ar.png and b/images/regular_open_win11_ar.png differ diff --git a/images/regular_open_win11_en.png b/images/regular_open_win11_en.png index 3245a1f..9fd95e9 100644 Binary files a/images/regular_open_win11_en.png and b/images/regular_open_win11_en.png differ diff --git a/images/reminder_win11_ar.png b/images/reminder_win11_ar.png index d0dd8a0..a937eb0 100644 Binary files a/images/reminder_win11_ar.png and b/images/reminder_win11_ar.png differ diff --git a/images/reminder_win11_en.png b/images/reminder_win11_en.png index fc09112..039ea69 100644 Binary files a/images/reminder_win11_en.png and b/images/reminder_win11_en.png differ diff --git a/version.json b/version.json index 04ab83c..40b7e77 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.2.0", + "version": "3.3.0", "assemblyVersion": "3.0", "versionHeightOffset": -1, "pathFilters": [ "/AwqatSalaat", "/AwqatSalaat.Common", "/AwqatSalaat.WinUI" ],