mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-04 05:57:29 +00:00
feat: check app updates
Check update using API endpoint: https://code.gri.mw/api/v1/repos/gui/grim/releases/latest Reviewed-on: https://code.gri.mw/GUI/grim/pulls/54
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: Ожидает завершения
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: 等待确定中
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<SettingsContent>,
|
||||
|
||||
/// Result of update check
|
||||
check_update: Bind<ReleaseInfo, String>,
|
||||
/// Application update information.
|
||||
update_info: (bool, Option<AppUpdate>),
|
||||
/// Update changelog [`Modal`] content.
|
||||
changelog_content: Option<ChangelogContent>
|
||||
}
|
||||
|
||||
/// 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<String>, cb: &dyn PlatformCallbacks) {
|
||||
self.select_wallet(wallet, data, cb);
|
||||
fn show_opening_modal(&mut self, w: &Wallet, data: Option<String>, 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<String>, cb: &dyn PlatformCallbacks) {
|
||||
fn select_wallet(&mut self, w: &Wallet, data: Option<String>, 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,7 @@ mod open;
|
||||
pub use open::*;
|
||||
|
||||
mod add;
|
||||
pub use add::*;
|
||||
pub use add::*;
|
||||
|
||||
mod changelog;
|
||||
pub use changelog::*;
|
||||
+22
-11
@@ -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<Full<Bytes>>) -> Result<Response<Incoming>, Error> {
|
||||
pub async fn send<B>(req: Request<B>) -> Result<Response<Incoming>, Error>
|
||||
where B: Body + Send + 'static + Unpin, <B as Body>::Data: Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<Box<dyn StdError + Send + Sync>>,
|
||||
{
|
||||
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<Bytes>>(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<Full<Bytes>>)
|
||||
-> Result<Response<Incoming>, Error> {
|
||||
pub async fn send_socks_proxy<B>(proxy_url: String, req: Request<B>)
|
||||
-> Result<Response<Incoming>, Error>
|
||||
where B: Body + Send + 'static + Unpin, <B as Body>::Data: Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<Box<dyn StdError + Send + Sync>>,
|
||||
{
|
||||
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<Bytes>>(proxy);
|
||||
.build::<_, B>(proxy);
|
||||
client.request(req).await
|
||||
}
|
||||
|
||||
/// Create http proxy client.
|
||||
pub async fn send_http_proxy(proxy_url: String, req: Request<Full<Bytes>>)
|
||||
-> Result<Response<Incoming>, Error> {
|
||||
pub async fn send_http_proxy<B>(proxy_url: String, req: Request<B>)
|
||||
-> Result<Response<Incoming>, Error>
|
||||
where B: Body + Send + 'static + Unpin, <B as Body>::Data: Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<Box<dyn StdError + Send + Sync>>,
|
||||
{
|
||||
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<Bytes>>(proxy_connector);
|
||||
.build::<_, B>(proxy_connector);
|
||||
client.request(req).await
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -13,4 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
mod client;
|
||||
pub use client::*;
|
||||
pub use client::*;
|
||||
|
||||
mod release;
|
||||
pub use release::*;
|
||||
@@ -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<ReleaseAsset>,
|
||||
}
|
||||
|
||||
#[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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<i32> = cur
|
||||
.split(".")
|
||||
.filter_map(|s| s.parse::<i32>().ok())
|
||||
.collect();
|
||||
let ver_numbers: Vec<i32> = ver
|
||||
.split(".")
|
||||
.filter_map(|s| s.parse::<i32>().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<ReleaseInfo, String> {
|
||||
let req = hyper::Request::builder()
|
||||
.method(hyper::Method::GET)
|
||||
.uri(REQUEST_URL)
|
||||
.body(Empty::<Bytes>::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::<ReleaseInfo>(&body_bytes) {
|
||||
return Ok(update_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Error checking update".to_string())
|
||||
}
|
||||
+72
-2
@@ -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<String>,
|
||||
/// 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<String>,
|
||||
/// SOCKS5 proxy URL.
|
||||
socks_proxy_url: Option<String>,
|
||||
|
||||
/// Flag to check updates on startup.
|
||||
check_updates: Option<bool>,
|
||||
/// Application update information.
|
||||
app_update: Option<AppUpdate>,
|
||||
}
|
||||
|
||||
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<AppUpdate> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,4 +16,4 @@ mod settings;
|
||||
pub use settings::Settings;
|
||||
|
||||
mod config;
|
||||
pub use config::AppConfig;
|
||||
pub use config::*;
|
||||
+4
-3
@@ -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<String> {
|
||||
if let Some(proxy) = TorConfig::get_proxy() {
|
||||
let req = hyper::Request::builder()
|
||||
let req: hyper::Request<Full<Bytes>> = 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) => {
|
||||
|
||||
@@ -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<Full<Bytes>> = req_setup.body(Full::from(
|
||||
r#"{"id":1,"jsonrpc":"2.0","method":"get_version","params":{} }"#)
|
||||
).unwrap();
|
||||
// Send request.
|
||||
|
||||
Reference in New Issue
Block a user