diff --git a/locales/de.yml b/locales/de.yml index f8936b6d..20bfc18c 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -36,6 +36,9 @@ max_short: MAX files_location: Dateistandort moving_files: Dateien verschieben wrong_path_error: Falscher Weg angegeben +check_updates: Suchen Sie beim Start nach Updates +update_available: Update ist verfügbar! +changelog: 'Wechselbuch:' wallets: await_conf_amount: Erwarte Bestätigung await_fin_amount: Warten auf die Fertigstellung diff --git a/locales/en.yml b/locales/en.yml index 55ac5d1d..6e2e5806 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -36,6 +36,9 @@ max_short: MAX files_location: Files location moving_files: Moving files wrong_path_error: Wrong path specified +check_updates: Check for updates at startup +update_available: Update is available! +changelog: 'Changelog:' wallets: await_conf_amount: Awaiting confirmation await_fin_amount: Awaiting finalization diff --git a/locales/fr.yml b/locales/fr.yml index 185be00a..fe1e8bd8 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -36,6 +36,9 @@ max_short: MAX files_location: Emplacement du fichier moving_files: Déplacer des fichiers wrong_path_error: Chemin incorrect spécifié +check_updates: Vérifiez les mises à jour au démarrage +update_available: Mise à jour disponible! +changelog: 'Journal des modifications:' wallets: await_conf_amount: En attente de confirmation await_fin_amount: En attente de finalisation diff --git a/locales/ru.yml b/locales/ru.yml index b137aa77..4e3d0898 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -36,6 +36,9 @@ max_short: МАКС files_location: Расположение файлов moving_files: Перемещение файлов wrong_path_error: Указан неправильный путь +check_updates: Проверять обновления при запуске +update_available: Доступно обновление! +changelog: 'Журнал изменений:' wallets: await_conf_amount: Ожидает подтверждения await_fin_amount: Ожидает завершения diff --git a/locales/tr.yml b/locales/tr.yml index f769f7b0..1294f14b 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -36,6 +36,9 @@ max_short: MAKS files_location: Dosya konumu moving_files: Dosyalari Tasima wrong_path_error: Yanlis yol belirtildi +check_updates: Başlangiçta güncellemeleri kontrol edin +update_available: Güncelleme mevcut! +changelog: 'Değişiklik Günlüğü:' wallets: await_conf_amount: Onay bekleniyor await_fin_amount: Tamamlanma bekleniyor diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index e80e8231..5a7e0184 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -36,6 +36,9 @@ max_short: 最大數量 files_location: 檔案位置 moving_files: 檔案移動 wrong_path_error: 指定錯誤路徑 +check_updates: 啟動時請查看更新 +update_available: 最新消息已发布! +changelog: '更新日誌:' wallets: await_conf_amount: 等待确认中 await_fin_amount: 等待确定中 diff --git a/src/gui/views/settings/content.rs b/src/gui/views/settings/content.rs index 4f470780..c41036a4 100644 --- a/src/gui/views/settings/content.rs +++ b/src/gui/views/settings/content.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::AppConfig; use crate::gui::icons::GLOBE_SIMPLE; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::settings::{InterfaceSettingsContent, NetworkSettingsContent}; @@ -41,6 +42,13 @@ impl Default for SettingsContent { impl SettingsContent { /// Draw application settings content. pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.add_space(5.0); + View::checkbox(ui, AppConfig::check_updates(), t!("check_updates"), || { + AppConfig::toggle_check_updates(); + }); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::stroke()); + // Show interface settings. self.interface_settings.ui(ui, cb); diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 9af26cfa..d20347dd 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -14,15 +14,15 @@ use std::time::Duration; use egui::scroll_area::ScrollBarVisibility; -use egui::{Align, CornerRadius, Id, Layout, Margin, RichText, ScrollArea, StrokeKind}; +use egui::{Align, CornerRadius, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, StrokeKind}; use egui::os::OperatingSystem; - -use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; +use egui_async::Bind; +use crate::gui::icons::{ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CARET_RIGHT, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::settings::SettingsContent; use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType}; use crate::gui::views::wallets::creation::WalletCreationContent; -use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletSettingsModal, WalletListModal}; +use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletSettingsModal, WalletListModal, ChangelogContent}; use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer}; use crate::gui::views::wallets::WalletContent; use crate::gui::views::{Content, Modal, TitlePanel, View}; @@ -31,6 +31,8 @@ use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{Wallet, WalletList}; use crate::AppConfig; use crate::gui::views::wallets::wallet::RecoverySettings; +use crate::http::{retrieve_release, ReleaseInfo}; +use crate::settings::AppUpdate; /// Wallets content. pub struct WalletsContent { @@ -53,6 +55,13 @@ pub struct WalletsContent { /// Settings content. settings_content: Option, + + /// Result of update check + check_update: Bind, + /// Application update information. + update_info: (bool, Option), + /// Update changelog [`Modal`] content. + changelog_content: Option } /// Identifier for [`Modal`] to add the wallet. @@ -75,6 +84,9 @@ impl Default for WalletsContent { wallet_content: WalletContent::default(), creation_content: None, settings_content: None, + check_update: Bind::new(false), + update_info: (false, None), + changelog_content: None, } } } @@ -86,7 +98,8 @@ impl ContentContainer for WalletsContent { OPEN_WALLET_MODAL, WALLET_SETTINGS_MODAL, SELECT_WALLET_MODAL, - Self::DELETE_CONFIRMATION_MODAL + Self::DELETE_CONFIRMATION_MODAL, + ChangelogContent::MODAL_ID ] } @@ -137,6 +150,11 @@ impl ContentContainer for WalletsContent { RecoverySettings::deletion_modal_ui(ui, w); } } + ChangelogContent::MODAL_ID => { + if let Some(c) = self.changelog_content.as_mut() { + c.ui(ui); + } + } _ => {} } } @@ -514,6 +532,31 @@ impl WalletsContent { View::app_logo_name_version(ui); ui.add_space(15.0); + // Show result of update check. + if AppConfig::check_updates() { + if let Some(res) = self.check_update.read_or_request(|| async { + retrieve_release().await + }) { + let checked = self.update_info.0; + if !checked { + self.update_info.0 = true; + match res { + Ok(info) => { + if info.is_update() { + AppConfig::save_update(Some(info)); + } else { + AppConfig::save_update(None); + } + self.update_info.1 = AppConfig::app_update(); + } + Err(_) => AppConfig::save_update(None), + } + } + } + // Show update information. + self.update_info_ui(ui); + } + let list = self.wallets.list().clone(); for w in list.iter() { let id = w.get_config().id; @@ -633,9 +676,74 @@ impl WalletsContent { }); } + /// Draw update information content. + fn update_info_ui(&mut self, ui: &mut egui::Ui) { + if self.update_info.1.is_none() { + return; + } + let update = self.update_info.1.as_ref().unwrap(); + ui.add_space(-4.0); + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(78.0); + let r = View::item_rounding(0, 1, false); + ui.painter().rect(rect, r, Colors::fill(), View::item_stroke(), StrokeKind::Outside); + ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { + // Show button to download the update. + let mut link_clicked = false; + View::item_button(ui, View::item_rounding(0, 1, true), CLOUD_ARROW_DOWN, None, || { + link_clicked = true; + }); + if link_clicked { + ui.ctx().open_url(OpenUrl { + url: update.url.clone(), + new_tab: true, + }); + } + // Show button to see update information. + View::item_button(ui, CornerRadius::default(), NOTEPAD, None, || { + self.changelog_content = Some(ChangelogContent::new(update.changelog.clone())); + let title = format!("Grim {}", update.version); + Modal::new(ChangelogContent::MODAL_ID) + .position(ModalPosition::Center) + .title(title) + .show(); + }); + + let layout_size = ui.available_size(); + ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { + ui.add_space(6.0); + ui.vertical(|ui| { + ui.add_space(3.0); + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + let update_text = "Update is available!"; + View::ellipsize_text(ui, update_text, 18.0, Colors::green()); + }); + + // Show version info. + let ver_text = if let Some(size) = update.size.as_ref() { + format!("{} {} ({} MB)", BOOKMARKS, update.version, size) + } else { + format!("{} {} > {}", BOOKMARKS, crate::VERSION, update.version) + }; + View::ellipsize_text(ui, ver_text, 15.0, Colors::text(false)); + ui.add_space(1.0); + + // Show update date. + let date_text = format!("{} {}", CALENDAR_CHECK, update.date); + View::ellipsize_text(ui, date_text, 15.0, Colors::gray()); + ui.add_space(3.0); + }); + }); + }); + ui.add_space(12.0); + View::horizontal_line(ui, Colors::stroke()); + ui.add_space(12.0); + } + /// Show [`Modal`] to select and open wallet. - fn show_opening_modal(&mut self, wallet: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { - self.select_wallet(wallet, data, cb); + fn show_opening_modal(&mut self, w: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { + self.select_wallet(w, data, cb); self.open_wallet_content = OpenWalletModal::new(); Modal::new(OPEN_WALLET_MODAL) .position(ModalPosition::CenterTop) @@ -644,12 +752,12 @@ impl WalletsContent { } /// Select wallet to make some actions on it. - fn select_wallet(&mut self, wallet: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { + fn select_wallet(&mut self, w: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { self.wallet_content.account_content.close_qr_scan(cb); if let Some(data) = data { - wallet.task(WalletTask::OpenMessage(data)); + w.task(WalletTask::OpenMessage(data)); } - self.wallets.select(Some(wallet.get_config().id)); + self.wallets.select(Some(w.get_config().id)); } } diff --git a/src/gui/views/wallets/modals/changelog.rs b/src/gui/views/wallets/modals/changelog.rs new file mode 100644 index 00000000..7d324433 --- /dev/null +++ b/src/gui/views/wallets/modals/changelog.rs @@ -0,0 +1,137 @@ +// Copyright 2026 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::scroll_area::ScrollBarVisibility; +use egui::{Id, OpenUrl, RichText, ScrollArea}; + +use crate::gui::views::{Modal, View}; +use crate::gui::Colors; +use crate::gui::icons::{BRACKETS_CURLY, GITHUB_LOGO, TELEGRAM_LOGO}; + +/// Application release changelog content. +pub struct ChangelogContent { + /// Changelog text. + changelog: String, +} + +/// Endpoint for GitHub repository. +const GITHUB_URL: &'static str = "https://github.com/GetGrin/grim"; +/// Endpoint for Telegram releases channel. +const TELEGRAM_URL: &'static str = "https://t.me/grim_releases"; +/// Endpoint for git repository. +const GIT_URL: &'static str = "https://code.gri.mw/GUI/grim"; + +impl ChangelogContent { + /// Create new content instance. + pub fn new(changelog: String) -> Self { + Self { changelog } + } + + /// Identifier for [`Modal`]. + pub const MODAL_ID: &'static str = "release_changelog_modal"; + + /// Draw changelog [`Modal`] content. + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("changelog")).size(16.0).color(Colors::gray())); + }); + ui.add_space(6.0); + + // Show changelog text. + ui.vertical_centered(|ui| { + let scroll_id = Id::from("release_changelog"); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + egui::TextEdit::multiline(&mut self.changelog) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + }); + + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(3, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw button to open GitHub link. + let mut github_clicked = false; + View::button(ui, GITHUB_LOGO, Colors::white_or_black(false), || { + github_clicked = true; + }); + if github_clicked { + ui.ctx().open_url(OpenUrl { + url: GITHUB_URL.into(), + new_tab: true, + }); + } + }); + columns[1].vertical_centered_justified(|ui| { + // Draw button to open Telegram link. + let mut tg_clicked = false; + View::button(ui, TELEGRAM_LOGO, Colors::white_or_black(false), || { + tg_clicked = true; + }); + if tg_clicked { + ui.ctx().open_url(OpenUrl { + url: TELEGRAM_URL.into(), + new_tab: true, + }); + } + }); + columns[2].vertical_centered_justified(|ui| { + // Draw button to open repository link. + let mut git_clicked = false; + View::button(ui, BRACKETS_CURLY, Colors::white_or_black(false), || { + git_clicked = true; + }); + if git_clicked { + ui.ctx().open_url(OpenUrl { + url: GIT_URL.into(), + new_tab: true, + }); + } + }); + }); + + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + Modal::close(); + }); + }); + ui.add_space(6.0); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/modals/mod.rs b/src/gui/views/wallets/modals/mod.rs index c3a585f2..837fc4c9 100644 --- a/src/gui/views/wallets/modals/mod.rs +++ b/src/gui/views/wallets/modals/mod.rs @@ -22,4 +22,7 @@ mod open; pub use open::*; mod add; -pub use add::*; \ No newline at end of file +pub use add::*; + +mod changelog; +pub use changelog::*; \ No newline at end of file diff --git a/src/http/client.rs b/src/http/client.rs index efca43ad..71817e7e 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use bytes::Bytes; -use http_body_util::Full; -use hyper::body::Incoming; +use hyper::body::{Body, Incoming}; use hyper::{Request, Response}; use hyper_proxy2::{Intercept, Proxy, ProxyConnector}; use hyper_tls::HttpsConnector; use hyper_util::client::legacy::{Client, Error}; use hyper_util::rt::TokioExecutor; +use serde::de::StdError; use crate::AppConfig; @@ -29,7 +28,11 @@ pub struct HttpClient { impl HttpClient { /// Send request. - pub async fn send(req: Request>) -> Result, Error> { + pub async fn send(req: Request) -> Result, Error> + where B: Body + Send + 'static + Unpin, ::Data: Send, + B::Data: Send, + B::Error: Into>, + { if AppConfig::use_proxy() { if let Some(url) = AppConfig::socks_proxy_url() { Self::send_socks_proxy(url, req).await @@ -38,14 +41,18 @@ impl HttpClient { } } else { let client = Client::builder(TokioExecutor::new()) - .build::<_, Full>(HttpsConnector::new()); + .build::<_, B>(HttpsConnector::new()); client.request(req).await } } /// Create socks proxy client. - pub async fn send_socks_proxy(proxy_url: String, req: Request>) - -> Result, Error> { + pub async fn send_socks_proxy(proxy_url: String, req: Request) + -> Result, Error> + where B: Body + Send + 'static + Unpin, ::Data: Send, + B::Data: Send, + B::Error: Into>, + { let connector = HttpsConnector::new(); let uri = proxy_url.parse().unwrap(); let proxy = hyper_socks2::SocksConnector { @@ -54,19 +61,23 @@ impl HttpClient { connector, }.with_tls().unwrap(); let client = Client::builder(TokioExecutor::new()) - .build::<_, Full>(proxy); + .build::<_, B>(proxy); client.request(req).await } /// Create http proxy client. - pub async fn send_http_proxy(proxy_url: String, req: Request>) - -> Result, Error> { + pub async fn send_http_proxy(proxy_url: String, req: Request) + -> Result, Error> + where B: Body + Send + 'static + Unpin, ::Data: Send, + B::Data: Send, + B::Error: Into>, + { let uri = proxy_url.parse().unwrap(); let proxy = Proxy::new(Intercept::All, uri); let connector = HttpsConnector::new(); let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); let client = Client::builder(TokioExecutor::new()) - .build::<_, Full>(proxy_connector); + .build::<_, B>(proxy_connector); client.request(req).await } } \ No newline at end of file diff --git a/src/http/mod.rs b/src/http/mod.rs index f9f1e7f7..e952fc1a 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -13,4 +13,7 @@ // limitations under the License. mod client; -pub use client::*; \ No newline at end of file +pub use client::*; + +mod release; +pub use release::*; \ No newline at end of file diff --git a/src/http/release.rs b/src/http/release.rs new file mode 100644 index 00000000..ab95f5df --- /dev/null +++ b/src/http/release.rs @@ -0,0 +1,178 @@ +// Copyright 2026 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bytes::Bytes; +use chrono::NaiveDateTime; +use egui::os::OperatingSystem; +use http_body_util::{BodyExt, Empty}; +use serde_derive::Deserialize; +use crate::gui::views::View; +use crate::http::HttpClient; + +#[derive(Deserialize)] +pub struct ReleaseAsset { + pub name: String, + pub browser_download_url: String, + pub size: u64, +} + +#[derive(Deserialize)] +pub struct ReleaseInfo { + pub tag_name: String, + pub body: String, + pub published_at: String, + pub assets: Vec, +} + +#[cfg(target_arch = "x86_64")] +/// x86 CPU architecture. +const X86_ARCH: &str = "x86_64"; +#[cfg(target_arch = "x86_64")] +const ARCH: &'static str = X86_ARCH; + +/// ARM CPU architecture. +const ARM_ARCH: &str = "arm"; +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +const ARCH: &'static str = ARM_ARCH; + +/// Base endpoint to download the release. +const BASE_DOWNLOAD_URL: &'static str = "https://code.gri.mw/GUI/grim/releases/download/"; + +impl ReleaseInfo { + /// Get version number. + pub fn version(&self) -> String { + self.tag_name.replace("v", "") + } + + /// Get artifact release name based on current platform. + fn name(&self) -> Option { + let os = OperatingSystem::from_target_os(); + match os { + OperatingSystem::Unknown => None, + OperatingSystem::Android => { + let name = if ARCH == ARM_ARCH { + format!("grim-{}-android.apk", self.tag_name) + } else { + format!("grim-{}-android-x86_64.apk", self.tag_name) + }; + Some(name) + }, + OperatingSystem::IOS => None, + OperatingSystem::Nix => { + let name = if ARCH == ARM_ARCH { + format!("grim-{}-linux-arm.AppImage", self.tag_name) + } else { + format!("grim-{}-linux-x86_64.AppImage", self.tag_name) + }; + Some(name) + } + OperatingSystem::Mac => { + Some(format!("grim-{}-macos-universal.zip", self.tag_name)) + }, + OperatingSystem::Windows => { + if ARCH == ARM_ARCH { + None + } else { + Some(format!("grim-{}-win-x86_64.msi", self.tag_name)) + } + } + } + } + + /// Get link to download the release. + pub fn url(&self) -> Option { + let base_url = format!("{}{}/", BASE_DOWNLOAD_URL, self.tag_name); + if let Some(name) = self.name() { + return Some(format!("{}{}", base_url, name)); + } + None + } + + /// Get formatted release date. + pub fn date(&self) -> String { + let date = self.published_at.clone().replace("T", " ").replace("Z", ""); + let date_format = NaiveDateTime::parse_from_str(date.as_str(), "%Y-%m-%d %H:%M:%S"); + if let Ok(date) = date_format { + return View::format_time(date.and_utc().timestamp()); + } + date + } + + /// Get release size in megabytes. + pub fn size(&self) -> Option { + let name = self.name()?; + for a in &self.assets { + if a.name == name { + let size_mb = a.size as f64 / 1000000.0; + return Some(format!("{:.2}", size_mb)); + } + } + None + } + + /// Check if release is update. + pub fn is_update(&self) -> bool { + let cur = crate::VERSION; + let ver = self.version(); + if cur == ver { + return false; + } + let cur_numbers: Vec = cur + .split(".") + .filter_map(|s| s.parse::().ok()) + .collect(); + let ver_numbers: Vec = ver + .split(".") + .filter_map(|s| s.parse::().ok()) + .collect(); + if cur_numbers.len() != ver_numbers.len() { + return true; + } + for (i, num) in ver_numbers.iter().enumerate() { + if num > &cur_numbers.get(i).unwrap() { + if i == 0 { + return true; + } else if i == 1 && cur_numbers.get(0).unwrap() == ver_numbers.get(0).unwrap() { + return true; + } else if i == 2 && cur_numbers.get(1).unwrap() == ver_numbers.get(1).unwrap() { + return true; + } + } + } + false + } +} + +/// API endpoint to check last release. +const REQUEST_URL: &'static str = "https://code.gri.mw/api/v1/repos/gui/grim/releases/latest"; + +pub async fn retrieve_release() -> Result { + let req = hyper::Request::builder() + .method(hyper::Method::GET) + .uri(REQUEST_URL) + .body(Empty::::new()) + .unwrap(); + if let Ok(resp) = HttpClient::send(req).await { + let status = resp.status().as_u16(); + if status == 200 { + if let Ok(body) = resp.into_body().collect().await { + let body_bytes = body.to_bytes(); + if let Ok(update_info) = serde_json::from_slice::(&body_bytes) { + return Ok(update_info); + } + } + } + } + Err("Error checking update".to_string()) +} \ No newline at end of file diff --git a/src/settings/config.rs b/src/settings/config.rs index 3e15548f..29c51c59 100644 --- a/src/settings/config.rs +++ b/src/settings/config.rs @@ -15,11 +15,27 @@ use grin_core::global; use grin_core::global::ChainTypes; use serde_derive::{Deserialize, Serialize}; -use crate::gui::views::Content; +use crate::gui::views::Content; +use crate::http::ReleaseInfo; use crate::node::NodeConfig; -use crate::Settings; use crate::wallet::ConnectionsConfig; +use crate::Settings; + +/// Application update information. +#[derive(Serialize, Deserialize, Clone)] +pub struct AppUpdate { + /// Version of release. + pub version: String, + /// Size of release in megabytes. + pub size: Option, + /// Date of release. + pub date: String, + /// Changes in the release. + pub changelog: String, + /// Link to download the release. + pub url: String, +} /// Application configuration, stored at toml file. #[derive(Serialize, Deserialize)] @@ -61,6 +77,11 @@ pub struct AppConfig { http_proxy_url: Option, /// SOCKS5 proxy URL. socks_proxy_url: Option, + + /// Flag to check updates on startup. + check_updates: Option, + /// Application update information. + app_update: Option, } impl Default for AppConfig { @@ -82,6 +103,8 @@ impl Default for AppConfig { use_socks_proxy: None, http_proxy_url: None, socks_proxy_url: None, + check_updates: Some(true), + app_update: None, } } } @@ -329,4 +352,51 @@ impl AppConfig { w_config.save(); } + /// Check updates on startup. + pub fn check_updates() -> bool { + let r_config = Settings::app_config_to_read(); + r_config.check_updates.unwrap_or(true) + } + + /// Disable or enable updates checking. + pub fn toggle_check_updates() { + let check = Self::check_updates(); + // Clear update info on disable. + if !check { + Self::save_update(None); + } + let mut w_config = Settings::app_config_to_update(); + w_config.check_updates = Some(!check); + w_config.save(); + } + + /// Get last update information, that includes: version, date and description. + pub fn app_update() -> Option { + let r_config = Settings::app_config_to_read(); + r_config.app_update.clone() + } + + /// Save update information. + pub fn save_update(release: Option<&ReleaseInfo>) { + let mut w_config = Settings::app_config_to_update(); + match release { + None => { + w_config.app_update = None; + } + Some(release) => { + let url = release.url(); + if let Some(url) = url { + let app_update = AppUpdate { + version: release.version(), + size: release.size(), + date: release.date(), + changelog: release.body.clone(), + url, + }; + w_config.app_update = Some(app_update); + } + } + } + w_config.save(); + } } \ No newline at end of file diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 1e936977..1fb7d1a2 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -16,4 +16,4 @@ mod settings; pub use settings::Settings; mod config; -pub use config::AppConfig; \ No newline at end of file +pub use config::*; \ No newline at end of file diff --git a/src/tor/tor.rs b/src/tor/tor.rs index 9fd94170..0e6b8852 100644 --- a/src/tor/tor.rs +++ b/src/tor/tor.rs @@ -20,7 +20,7 @@ use ed25519_dalek::hazmat::ExpandedSecretKey; use fs_mistrust::Mistrust; use futures::task::SpawnExt; use grin_util::secp::SecretKey; -use http_body_util::BodyExt; +use http_body_util::{BodyExt, Full}; use lazy_static::lazy_static; use parking_lot::RwLock; use sha2::Sha512; @@ -30,6 +30,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use std::{fs, thread}; +use bytes::Bytes; use log::error; use safelog::DisplayRedacted; use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; @@ -131,10 +132,10 @@ impl Tor { /// Send post request using Tor. pub async fn post(body: String, url: String) -> Option { if let Some(proxy) = TorConfig::get_proxy() { - let req = hyper::Request::builder() + let req: hyper::Request> = hyper::Request::builder() .method(hyper::Method::POST) .uri(url) - .body(http_body_util::Full::from(body)) + .body(Full::from(body)) .unwrap(); let res = match proxy { TorProxy::SOCKS5(url) => { diff --git a/src/wallet/connections/external.rs b/src/wallet/connections/external.rs index 243ea738..e1ee5277 100644 --- a/src/wallet/connections/external.rs +++ b/src/wallet/connections/external.rs @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use bytes::Bytes; use grin_core::global::ChainTypes; use grin_util::to_base64; +use http_body_util::Full; use serde_derive::{Deserialize, Serialize}; use crate::http::HttpClient; @@ -141,7 +143,7 @@ fn check_ext_conn(conn: &ExternalConnection, ui_ctx: &egui::Context) { req_setup = req_setup .header(hyper::header::AUTHORIZATION, basic_auth.clone()); } - let req = req_setup.body(http_body_util::Full::from( + let req: hyper::Request> = req_setup.body(Full::from( r#"{"id":1,"jsonrpc":"2.0","method":"get_version","params":{} }"#) ).unwrap(); // Send request.