diff --git a/GUI/AboutBox.xaml b/GUI/AboutBox.xaml index d3231729..2ad63b3d 100644 --- a/GUI/AboutBox.xaml +++ b/GUI/AboutBox.xaml @@ -33,7 +33,6 @@ IN THE SOFTWARE. Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" ShowInTaskbar="False" WindowStartupLocation="CenterOwner"> - https://github.com/morkt/GARbro#readme diff --git a/GUI/AboutBox.xaml.cs b/GUI/AboutBox.xaml.cs index c301dd2c..ba009b91 100644 --- a/GUI/AboutBox.xaml.cs +++ b/GUI/AboutBox.xaml.cs @@ -189,20 +189,8 @@ namespace GARbro.GUI private void Hyperlink_RequestNavigate (object sender, RequestNavigateEventArgs e) { - try - { - if (e.Uri.IsAbsoluteUri) - { - Process.Start (new ProcessStartInfo (e.Uri.AbsoluteUri)); - e.Handled = true; - } - else - throw new ApplicationException ("URI is not absolute"); - } - catch (Exception X) - { - Trace.WriteLine ("Link navigation failed: "+X.Message, e.Uri.ToString()); - } + if (App.NavigateUri (e.Uri)) + e.Handled = true; } } diff --git a/GUI/App.xaml b/GUI/App.xaml index 378f6ff0..c2c55f24 100644 --- a/GUI/App.xaml +++ b/GUI/App.xaml @@ -1,9 +1,12 @@  + https://github.com/morkt/GARbro#readme + https://morkt.github.io/version.xml diff --git a/GUI/App.xaml.cs b/GUI/App.xaml.cs index 632a3abf..c6183920 100644 --- a/GUI/App.xaml.cs +++ b/GUI/App.xaml.cs @@ -29,6 +29,7 @@ using System.Diagnostics; using GARbro.GUI.Properties; using GameRes; using GameRes.Compression; +using System.Reflection; namespace GARbro.GUI { @@ -37,9 +38,8 @@ namespace GARbro.GUI /// public partial class App : Application { - const StringComparison StringIgnoreCase = StringComparison.OrdinalIgnoreCase; - - public static string Name { get { return "GARbro"; } } + public static string Name { get { return "GARbro"; } } + public static string FormatsDat { get { return "Formats.dat"; } } /// /// Initial browsing directory. @@ -79,9 +79,24 @@ namespace GARbro.GUI if (string.IsNullOrEmpty (InitPath)) InitPath = Directory.GetCurrentDirectory(); - string scheme_file = Path.Combine (FormatCatalog.Instance.DataDirectory, "Formats.dat"); + DeserializeScheme (Path.Combine (FormatCatalog.Instance.DataDirectory, FormatsDat)); + DeserializeScheme (Path.Combine (GetLocalAppDataFolder(), FormatsDat)); + } + + public string GetLocalAppDataFolder () + { + string local_app_data = Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData); + var attribs = Assembly.GetExecutingAssembly().GetCustomAttributes (typeof(AssemblyCompanyAttribute), false); + string company = attribs.Length > 0 ? ((AssemblyCompanyAttribute)attribs[0]).Company : ""; + return Path.Combine (local_app_data, company, Name); + } + + public void DeserializeScheme (string scheme_file) + { try { + if (!File.Exists (scheme_file)) + return; using (var file = File.OpenRead (scheme_file)) FormatCatalog.Instance.DeserializeScheme (file); } @@ -116,5 +131,24 @@ namespace GARbro.GUI if (Settings.Default.winState == System.Windows.WindowState.Minimized) Settings.Default.winState = System.Windows.WindowState.Normal; } + + public static bool NavigateUri (Uri uri) + { + try + { + if (uri.IsAbsoluteUri) + { + Process.Start (new ProcessStartInfo (uri.AbsoluteUri)); + return true; + } + else + throw new ApplicationException ("URI is not absolute"); + } + catch (Exception X) + { + Trace.WriteLine ("Link navigation failed: "+X.Message, uri.ToString()); + } + return false; + } } } diff --git a/GUI/GARbro.GUI.csproj b/GUI/GARbro.GUI.csproj index aff20539..cb5b7fc2 100644 --- a/GUI/GARbro.GUI.csproj +++ b/GUI/GARbro.GUI.csproj @@ -154,6 +154,7 @@ + @@ -173,6 +174,9 @@ TextViewer.xaml + + UpdateDialog.xaml + @@ -227,6 +231,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + diff --git a/GUI/GarUpdate.cs b/GUI/GarUpdate.cs new file mode 100644 index 00000000..978d1330 --- /dev/null +++ b/GUI/GarUpdate.cs @@ -0,0 +1,308 @@ +//! \file GarUpdate.cs +//! \date Tue Feb 14 00:02:14 2017 +//! \brief Application update routines. +// +// Copyright (C) 2017 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Net; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Input; +using System.Xml; +using GameRes; +using GARbro.GUI.Strings; +using System.IO; + +namespace GARbro.GUI +{ + public partial class MainWindow : Window + { + GarUpdate m_updater; + + private void InitUpdatesChecker () + { + var update_url = App.Resources["UpdateUrl"] as Uri; + m_updater = new GarUpdate (this, update_url); + m_updater.CanExecuteChanged += (s, e) => CommandManager.InvalidateRequerySuggested(); + } + + public void CanExecuteUpdate (object sender, CanExecuteRoutedEventArgs e) + { + e.CanExecute = m_updater.CanExecute (e.Parameter); + } + + /// + /// Handle "Check for updates" command. + /// + private void CheckUpdatesExec (object sender, ExecutedRoutedEventArgs e) + { + m_updater.Execute (e.Parameter); + } + } + + public class GarUpdateInfo + { + public Version ReleaseVersion { get; set; } + public Uri ReleaseUrl { get; set; } + public string ReleaseNotes { get; set; } + public int FormatsVersion { get; set; } + public Uri FormatsUrl { get; set; } + public IDictionary Assemblies { get; set; } + + public static GarUpdateInfo Parse (XmlDocument xml) + { + var root = xml.DocumentElement.SelectSingleNode ("/GARbro"); + if (null == root) + return null; + var info = new GarUpdateInfo + { + ReleaseVersion = Version.Parse (GetInnerText (root.SelectSingleNode ("Release/Version"))), + ReleaseUrl = new Uri (GetInnerText (root.SelectSingleNode ("Release/Url"))), + ReleaseNotes = GetInnerText (root.SelectSingleNode ("Release/Notes")), + + FormatsVersion = Int32.Parse (GetInnerText (root.SelectSingleNode ("FormatsData/FileVersion"))), + FormatsUrl = new Uri (GetInnerText (root.SelectSingleNode ("FormatsData/Url"))), + Assemblies = ParseAssemblies (root.SelectNodes ("FormatsData/Requires/Assembly")), + }; + return info; + } + + static string GetInnerText (XmlNode node) + { + // XXX node?.InnerText ?? "" + return node != null ? node.InnerText : ""; + } + + static IDictionary ParseAssemblies (XmlNodeList nodes) + { + var dict = new Dictionary(); + foreach (XmlNode node in nodes) + { + var attr = node.Attributes; + var name = attr["Name"]; + var version = attr["Version"]; + if (name != null && version != null) + dict[name.Value] = Version.Parse (version.Value); + } + return dict; + } + } + + internal sealed class GarUpdate : ICommand, IDisposable + { + private readonly MainWindow m_main; + private readonly BackgroundWorker m_update_checker = new BackgroundWorker(); + private readonly Uri m_url; + + const int RequestTimeout = 20000; // milliseconds + + public GarUpdate (MainWindow main, Uri url) + { + m_main = main; + m_url = url; + m_update_checker.DoWork += StartUpdatesCheck; + m_update_checker.RunWorkerCompleted += UpdatesCheckComplete; + } + + public void Execute (object parameter) + { + if (!m_update_checker.IsBusy) + m_update_checker.RunWorkerAsync(); + } + + public bool CanExecute (object parameter) + { + return !m_update_checker.IsBusy; + } + + public event EventHandler CanExecuteChanged; + + void OnCanExecuteChanged () + { + var handler = CanExecuteChanged; + if (handler != null) + handler (this, EventArgs.Empty); + } + + private void StartUpdatesCheck (object sender, DoWorkEventArgs e) + { + OnCanExecuteChanged(); + if (m_url != null) + e.Result = Check (m_url); + } + + private void UpdatesCheckComplete (object sender, RunWorkerCompletedEventArgs e) + { + try + { + if (e.Error != null) + { + m_main.SetStatusText (string.Format ("{0} {1}", guiStrings.MsgUpdateFailed, e.Error.Message)); + return; + } + else if (e.Cancelled) + return; + var result = e.Result as GarUpdateInfo; + if (null == result) + { + m_main.SetStatusText (guiStrings.MsgNoUpdates); + return; + } + ShowUpdateResult (result); + } + finally + { + OnCanExecuteChanged(); + } + } + + UpdateDialog m_dialog; + Uri m_formats_url; + + private void ShowUpdateResult (GarUpdateInfo result) + { + var app_version = Assembly.GetExecutingAssembly().GetName().Version; + var db_version = FormatCatalog.Instance.CurrentSchemeVersion; + bool has_app_update = app_version < result.ReleaseVersion; + bool has_db_update = db_version < result.FormatsVersion && CheckAssemblies (result.Assemblies); + if (!has_app_update && !has_db_update) + { + m_main.SetStatusText (guiStrings.MsgUpToDate); + return; + } + m_formats_url = result.FormatsUrl; + m_dialog = new UpdateDialog (result, has_app_update, has_db_update); + m_dialog.Owner = m_main; + m_dialog.FormatsDownload.Click += StartFormatsDownload; + m_dialog.ShowDialog(); + } + + private async void StartFormatsDownload (object control, RoutedEventArgs e) + { + var dialog = m_dialog; + try + { + dialog.FormatsDownload.IsEnabled = false; + var app_data_folder = m_main.App.GetLocalAppDataFolder(); + Directory.CreateDirectory (app_data_folder); + using (var client = new WebClientEx()) + using (var tmp_file = new GARbro.Shell.TemporaryFile (app_data_folder, Path.GetRandomFileName())) + { + client.Timeout = RequestTimeout; + await client.DownloadFileTaskAsync (m_formats_url, tmp_file.Name); + + m_main.App.DeserializeScheme (tmp_file.Name); + var local_formats_dat = Path.Combine (app_data_folder, App.FormatsDat); + if (!GARbro.Shell.File.Rename (tmp_file.Name, local_formats_dat)) + throw new Win32Exception (GARbro.Shell.File.GetLastError()); + } + SetFormatsUpdateStatus (dialog, guiStrings.MsgUpdateComplete); + } + catch (Exception X) + { + SetFormatsUpdateStatus (dialog, guiStrings.MsgDownloadFailed, X.Message); + } + finally + { + dialog.FormatsDownload.Visibility = Visibility.Hidden; + } + } + + void SetFormatsUpdateStatus (UpdateDialog dialog, string text1, string text2 = null) + { + if (dialog.IsClosed) + m_main.SetStatusText (text1); + else if (null == text2) + dialog.FormatsUpdateText.Text = text1; + else + dialog.FormatsUpdateText.Text = string.Format ("{0}\n{1}", text1, text2); + } + + /// + /// Check if loaded assemblies match required versions. + /// + bool CheckAssemblies (IDictionary assemblies) + { + var loaded = AppDomain.CurrentDomain.GetAssemblies().Select (a => a.GetName()) + .ToDictionary (a => a.Name, a => a.Version); + foreach (var item in assemblies) + { + if (!loaded.ContainsKey (item.Key)) + return false; + if (loaded[item.Key] < item.Value) + return false; + } + return true; + } + + GarUpdateInfo Check (Uri version_url) + { + var request = WebRequest.Create (version_url); + request.Timeout = RequestTimeout; + var response = (HttpWebResponse)request.GetResponse(); + using (var input = response.GetResponseStream()) + { + var xml = new XmlDocument(); + xml.Load (input); + return GarUpdateInfo.Parse (xml); + } + } + + bool m_disposed = false; + public void Dispose () + { + if (!m_disposed) + { + m_update_checker.Dispose(); + m_disposed = true; + } + GC.SuppressFinalize (this); + } + } + + /// + /// WebClient with timeout setting. + /// + internal class WebClientEx : WebClient + { + /// + /// Request timeout, in milliseconds. + /// + public int Timeout { get; set; } + + public WebClientEx () + { + Timeout = 60000; + } + + protected override WebRequest GetWebRequest (Uri uri) + { + var request = base.GetWebRequest (uri); + request.Timeout = Timeout; + return request; + } + } +} diff --git a/GUI/MainWindow.xaml b/GUI/MainWindow.xaml index d900d3a9..daa8abcf 100644 --- a/GUI/MainWindow.xaml +++ b/GUI/MainWindow.xaml @@ -151,6 +151,7 @@ + @@ -402,6 +403,7 @@ + diff --git a/GUI/MainWindow.xaml.cs b/GUI/MainWindow.xaml.cs index 3d45960e..4bca3861 100644 --- a/GUI/MainWindow.xaml.cs +++ b/GUI/MainWindow.xaml.cs @@ -54,6 +54,8 @@ namespace GARbro.GUI { private App m_app; + public App App { get { return m_app; } } + const StringComparison StringIgnoreCase = StringComparison.CurrentCultureIgnoreCase; public MainWindow() @@ -64,6 +66,7 @@ namespace GARbro.GUI if (this.Left < 0) this.Left = 0; InitDirectoryChangesWatcher(); InitPreviewPane(); + InitUpdatesChecker(); if (null == Settings.Default.appRecentFiles) Settings.Default.appRecentFiles = new StringCollection(); @@ -1478,6 +1481,7 @@ namespace GARbro.GUI public static readonly RoutedCommand SortBy = new RoutedCommand(); public static readonly RoutedCommand Exit = new RoutedCommand(); public static readonly RoutedCommand About = new RoutedCommand(); + public static readonly RoutedCommand CheckUpdates = new RoutedCommand(); public static readonly RoutedCommand GoBack = new RoutedCommand(); public static readonly RoutedCommand GoForward = new RoutedCommand(); public static readonly RoutedCommand DeleteItem = new RoutedCommand(); diff --git a/GUI/Strings/guiStrings.Designer.cs b/GUI/Strings/guiStrings.Designer.cs index 6610230a..67aab972 100644 --- a/GUI/Strings/guiStrings.Designer.cs +++ b/GUI/Strings/guiStrings.Designer.cs @@ -96,6 +96,15 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to _Download. + /// + public static string ButtonDownload { + get { + return ResourceManager.GetString("ButtonDownload", resourceCulture); + } + } + /// /// Looks up a localized string similar to Extract. /// @@ -456,6 +465,15 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to Release notes. + /// + public static string LabelReleaseNotes { + get { + return ResourceManager.GetString("LabelReleaseNotes", resourceCulture); + } + } + /// /// Looks up a localized string similar to Skip incovertible files.. /// @@ -474,6 +492,15 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to _Check for updates.... + /// + public static string MenuCheckUpdates { + get { + return ResourceManager.GetString("MenuCheckUpdates", resourceCulture); + } + } + /// /// Looks up a localized string similar to E_xit. /// @@ -636,6 +663,15 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to Update download failed.. + /// + public static string MsgDownloadFailed { + get { + return ResourceManager.GetString("MsgDownloadFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to archive is empty. /// @@ -771,6 +807,15 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to No updates currently available.. + /// + public static string MsgNoUpdates { + get { + return ResourceManager.GetString("MsgNoUpdates", resourceCulture); + } + } + /// /// Looks up a localized string similar to File {0} ///already exists. @@ -837,6 +882,42 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to Formats database update available.. + /// + public static string MsgUpdateAvailable { + get { + return ResourceManager.GetString("MsgUpdateAvailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Formats database updated.. + /// + public static string MsgUpdateComplete { + get { + return ResourceManager.GetString("MsgUpdateComplete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update check failed.. + /// + public static string MsgUpdateFailed { + get { + return ResourceManager.GetString("MsgUpdateFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GARbro version is up to date.. + /// + public static string MsgUpToDate { + get { + return ResourceManager.GetString("MsgUpToDate", resourceCulture); + } + } + /// /// Looks up a localized string similar to Version {0}. /// @@ -1093,6 +1174,15 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to New version available:. + /// + public static string TextNewVersion { + get { + return ResourceManager.GetString("TextNewVersion", resourceCulture); + } + } + /// /// Looks up a localized string similar to Archive parameters. /// @@ -1138,6 +1228,24 @@ namespace GARbro.GUI.Strings { } } + /// + /// Looks up a localized string similar to Application update. + /// + public static string TextUpdateTitle { + get { + return ResourceManager.GetString("TextUpdateTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Visit download page. + /// + public static string TextVisitPage { + get { + return ResourceManager.GetString("TextVisitPage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Either WAV, MP3 or OGG. /// diff --git a/GUI/Strings/guiStrings.ko-KR.resx b/GUI/Strings/guiStrings.ko-KR.resx index 8deaee9a..52c5d398 100644 --- a/GUI/Strings/guiStrings.ko-KR.resx +++ b/GUI/Strings/guiStrings.ko-KR.resx @@ -493,4 +493,52 @@ 대상폴더에 파일 {0} 이(가) 이미 존재합니다. - + + Download + translation pending + + + Release notes + translation pending + + + Check for updates... + translation pending + + + Update download failed. + translation pending + + + No updates currently available. + translation pending + + + Formats database update available. + translation pending + + + Formats database updated. + translation pending + + + Update check failed. + translation pending + + + GARbro version is up to date. + translation pending + + + New version available: + translation pending + + + Application update + translation pending + + + Visit download page + translation pending + + \ No newline at end of file diff --git a/GUI/Strings/guiStrings.resx b/GUI/Strings/guiStrings.resx index 8c89f810..a4c3aebf 100644 --- a/GUI/Strings/guiStrings.resx +++ b/GUI/Strings/guiStrings.resx @@ -497,4 +497,40 @@ Overwrite? File {0} already exists in the destination folder. + + _Download + + + Release notes + + + _Check for updates... + + + No updates currently available. + + + Formats database update available. + + + Formats database updated. + + + Update check failed. + + + GARbro version is up to date. + + + New version available: + + + Application update + + + Visit download page + + + Update download failed. + \ No newline at end of file diff --git a/GUI/Strings/guiStrings.ru-RU.resx b/GUI/Strings/guiStrings.ru-RU.resx index cb36b5fe..48e15388 100644 --- a/GUI/Strings/guiStrings.ru-RU.resx +++ b/GUI/Strings/guiStrings.ru-RU.resx @@ -518,4 +518,40 @@ Файл с именем {0} уже существует. + + Обновить + + + Примечания к выпуску + + + Проверить обновления... + + + Не удалось обновить базу форматов. + + + Обновления недоступны. + + + Доступна обновлённая база форматов. + + + База форматов успешно обновлена. + + + Сбой проверки обновлений. + + + Установлена актуальная версия GARbro. + + + Доступна новая версия: + + + Обновление программы + + + Перейти на страницу загрузки + \ No newline at end of file diff --git a/GUI/Strings/guiStrings.zh-Hans.resx b/GUI/Strings/guiStrings.zh-Hans.resx index a602e8fc..dbb2c1c5 100644 --- a/GUI/Strings/guiStrings.zh-Hans.resx +++ b/GUI/Strings/guiStrings.zh-Hans.resx @@ -505,4 +505,52 @@ 文件{0}已经存在。 + + Download + translation pending + + + Release notes + translation pending + + + Check for updates... + translation pending + + + Update download failed. + translation pending + + + No updates currently available. + translation pending + + + Formats database update available. + translation pending + + + Formats database updated. + translation pending + + + Update check failed. + translation pending + + + GARbro version is up to date. + translation pending + + + New version available: + translation pending + + + Application update + translation pending + + + Visit download page + translation pending + \ No newline at end of file diff --git a/GUI/UpdateDialog.xaml b/GUI/UpdateDialog.xaml new file mode 100644 index 00000000..db2914d9 --- /dev/null +++ b/GUI/UpdateDialog.xaml @@ -0,0 +1,61 @@ + + + + + +