Migration
This commit is contained in:
14
LauncherSource/.gitignore
vendored
Normal file
14
LauncherSource/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
# Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
10
LauncherSource/Cargo.toml
Normal file
10
LauncherSource/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"launcher_shared",
|
||||
"launcher_client_egui",
|
||||
"launcher_client_iced",
|
||||
"launcher_server",
|
||||
]
|
||||
|
||||
resolver = "2"
|
||||
16
LauncherSource/launcher_client_egui/Cargo.toml
Normal file
16
LauncherSource/launcher_client_egui/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "launcher_client_egui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
launcher_shared = { path = "../launcher_shared" }
|
||||
|
||||
eframe = "0.21"
|
||||
egui-winit = "0.21"
|
||||
reqwest = { version = "0.11", features = ["stream"] }
|
||||
ehttp = "0.2"
|
||||
poll-promise = "0.2"
|
||||
directories = "4.0"
|
||||
120
LauncherSource/launcher_client_egui/src/custom_window_frame.rs
Normal file
120
LauncherSource/launcher_client_egui/src/custom_window_frame.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use eframe::egui;
|
||||
|
||||
pub fn custom_window_frame(
|
||||
ctx: &egui::Context,
|
||||
frame: &mut eframe::Frame,
|
||||
title: &str,
|
||||
add_contents: impl FnOnce(&mut egui::Ui),
|
||||
) {
|
||||
use egui::*;
|
||||
|
||||
let panel_frame = egui::Frame {
|
||||
fill: ctx.style().visuals.window_fill(),
|
||||
rounding: 10.0.into(),
|
||||
stroke: ctx.style().visuals.widgets.noninteractive.fg_stroke,
|
||||
outer_margin: 0.5.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
CentralPanel::default().frame(panel_frame).show(ctx, |ui| {
|
||||
let app_rect = ui.max_rect();
|
||||
|
||||
let title_bar_height = 32.0;
|
||||
let title_bar_rect = {
|
||||
let mut rect = app_rect;
|
||||
rect.max.y = rect.min.y + title_bar_height;
|
||||
rect
|
||||
};
|
||||
title_bar_ui(ui, frame, title_bar_rect, title);
|
||||
|
||||
let content_rect = {
|
||||
let mut rect = app_rect;
|
||||
rect.min.y = title_bar_rect.max.y;
|
||||
rect
|
||||
}
|
||||
.shrink(4.0);
|
||||
let mut content_ui = ui.child_ui(content_rect, *ui.layout());
|
||||
add_contents(&mut content_ui);
|
||||
});
|
||||
}
|
||||
|
||||
fn title_bar_ui(
|
||||
ui: &mut egui::Ui,
|
||||
frame: &mut eframe::Frame,
|
||||
title_bar_rect: eframe::epaint::Rect,
|
||||
title: &str,
|
||||
) {
|
||||
use egui::*;
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
let title_bar_response = ui.interact(title_bar_rect, Id::new("title_bar"), Sense::click());
|
||||
|
||||
painter.text(
|
||||
title_bar_rect.center(),
|
||||
Align2::CENTER_CENTER,
|
||||
title,
|
||||
FontId::proportional(20.0),
|
||||
ui.style().visuals.text_color(),
|
||||
);
|
||||
|
||||
painter.line_segment(
|
||||
[
|
||||
title_bar_rect.left_bottom() + vec2(1.0, 0.0),
|
||||
title_bar_rect.right_bottom() + vec2(-1.0, 0.0),
|
||||
],
|
||||
ui.visuals().widgets.noninteractive.bg_stroke,
|
||||
);
|
||||
|
||||
if title_bar_response.double_clicked() {
|
||||
frame.set_maximized(!frame.info().window_info.maximized);
|
||||
} else if title_bar_response.is_pointer_button_down_on() {
|
||||
frame.drag_window();
|
||||
}
|
||||
|
||||
ui.allocate_ui_at_rect(title_bar_rect, |ui| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.visuals_mut().button_frame = false;
|
||||
ui.add_space(8.0);
|
||||
close_maximise_minimize(ui, frame);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn close_maximise_minimize(ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
use egui::{Button, RichText};
|
||||
|
||||
let button_height = 12.0;
|
||||
|
||||
let close_response = ui
|
||||
.add(Button::new(RichText::new("X").size(button_height)))
|
||||
.on_hover_text("Close the window");
|
||||
if close_response.clicked() {
|
||||
frame.close();
|
||||
}
|
||||
|
||||
if frame.info().window_info.maximized {
|
||||
let maximized_response = ui
|
||||
.add(Button::new(RichText::new("-").size(button_height)))
|
||||
.on_hover_text("Restore window");
|
||||
|
||||
if maximized_response.clicked() {
|
||||
frame.set_maximized(false);
|
||||
}
|
||||
} else {
|
||||
let maximized_response = ui
|
||||
.add(Button::new(RichText::new("-").size(button_height)))
|
||||
.on_hover_text("Maximize window");
|
||||
if maximized_response.clicked() {
|
||||
frame.set_maximized(true);
|
||||
}
|
||||
}
|
||||
|
||||
let minimized_response = ui
|
||||
.add(Button::new(RichText::new("_").size(button_height)))
|
||||
.on_hover_text("Minimize the window");
|
||||
if minimized_response.clicked() {
|
||||
frame.set_minimized(true);
|
||||
}
|
||||
}
|
||||
54
LauncherSource/launcher_client_egui/src/download_file.rs
Normal file
54
LauncherSource/launcher_client_egui/src/download_file.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::{fs::File, io::Cursor};
|
||||
|
||||
type FetchResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn fetch_url(url: String, file_name: String) -> FetchResult<()> {
|
||||
let response = reqwest::get(url).await?;
|
||||
let mut file = File::create(file_name)?;
|
||||
let mut content = Cursor::new(response.bytes().await?);
|
||||
std::io::copy(&mut content, &mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// use reqwest::{header::CONTENT_LENGTH, Client};
|
||||
// use std::{path::Path, io::Write};
|
||||
|
||||
// #[allow(dead_code)]
|
||||
// async fn download_file_with_progress(url: &str, file_path: &Path) -> Result<(), reqwest::Error> {
|
||||
// let client = Client::new();
|
||||
|
||||
// let response = client.head(url).send().await?;
|
||||
// let content_length = response.headers()
|
||||
// .get(CONTENT_LENGTH)
|
||||
// .and_then(|v| v.to_str().ok()).and_then(|v| v.parse::<u64>().ok());
|
||||
|
||||
// if let Some(content_length) = content_length {
|
||||
// println!("Downloading {} bytes from {}", content_length, url);
|
||||
// } else {
|
||||
// println!("Downloading from {}", url);
|
||||
// }
|
||||
|
||||
// let mut response = client.get(url).send().await?;
|
||||
|
||||
// let mut file = File::create(file_path)?;
|
||||
|
||||
// let mut bytes_written = 0;
|
||||
// while let Some(chunk) = response.chunk().await? {
|
||||
// file.write_all(&chunk)?;
|
||||
// bytes_written += chunk.len();
|
||||
|
||||
// if let Some(content_length) = content_length {
|
||||
// let progress = (bytes_written as f64 / content_length as f64) * 100.0;
|
||||
// print!("\rDownloading {:.2}% ({} of {} bytes)", progress, bytes_written, content_length);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if let Some(content_length) = content_length {
|
||||
// println!("\rDownload complete ({} bytes)", content_length);
|
||||
// } else {
|
||||
// println!("\rDownload complete");
|
||||
// }
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
119
LauncherSource/launcher_client_egui/src/launcher.rs
Normal file
119
LauncherSource/launcher_client_egui/src/launcher.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use eframe::{
|
||||
egui::{widgets, CentralPanel, Context},
|
||||
App,
|
||||
};
|
||||
use poll_promise::Promise;
|
||||
|
||||
use crate::custom_window_frame;
|
||||
|
||||
pub struct Launcher {
|
||||
latest_version: String,
|
||||
update_available: bool,
|
||||
download_promise: Option<Promise<Result<String, String>>>,
|
||||
server_url: String,
|
||||
server_root_uri: String,
|
||||
dirtreecontent: Option<String>,
|
||||
}
|
||||
|
||||
// {
|
||||
// "version": "0.1.0",
|
||||
// "files": [
|
||||
// {
|
||||
// "path": "/MyFile",
|
||||
// "hash": "bighashofdoom"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
impl Launcher {
|
||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
Launcher {
|
||||
latest_version: "0.1.0".to_string(),
|
||||
update_available: true,
|
||||
download_promise: None,
|
||||
server_url: "https://ginger.amasson.eu:6886".into(),
|
||||
server_root_uri: format!("/Howling/{}", std::env::consts::OS),
|
||||
dirtreecontent: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dirtree_path(&self) -> String {
|
||||
format!("{}{}/dirtree.json", self.server_url, self.server_root_uri)
|
||||
}
|
||||
}
|
||||
|
||||
impl App for Launcher {
|
||||
fn clear_color(&self, _visuals: &eframe::egui::Visuals) -> [f32; 4] {
|
||||
eframe::egui::Rgba::TRANSPARENT.to_array()
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) {
|
||||
let dirtreepath = self.dirtree_path();
|
||||
|
||||
custom_window_frame::custom_window_frame(ctx, frame, "egui with custom frame", |ui| {
|
||||
let promise = self.download_promise.get_or_insert_with(|| {
|
||||
let ctx = ctx.clone();
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get(&dirtreepath);
|
||||
|
||||
println!("Downloading from {}", &dirtreepath);
|
||||
ehttp::fetch(request, move |response| {
|
||||
let dirtreefile = response.and_then(|res| {
|
||||
let bytes = res.bytes;
|
||||
Ok(String::from_utf8(bytes).unwrap())
|
||||
});
|
||||
sender.send(dirtreefile);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
promise
|
||||
});
|
||||
|
||||
ui.heading("Howling Launcher");
|
||||
|
||||
if self.update_available {
|
||||
if ui.button("Download Update").clicked() {
|
||||
println!("download update clicked !");
|
||||
|
||||
CentralPanel::default().show(ctx, |ui| match promise.ready() {
|
||||
None => {
|
||||
ui.spinner();
|
||||
println!("Downloading...");
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
println!("Error downloading {}", err);
|
||||
ui.colored_label(ui.visuals().error_fg_color, err);
|
||||
}
|
||||
Some(Ok(text)) => {
|
||||
println!("Finished downloading");
|
||||
self.dirtreecontent = Some(text.clone());
|
||||
}
|
||||
});
|
||||
|
||||
self.update_available = false;
|
||||
}
|
||||
} else {
|
||||
if let Some(text) = self.dirtreecontent.clone() {
|
||||
ui.label(format!("dirtree:\n{}\n", text));
|
||||
}
|
||||
|
||||
ui.label(format!(
|
||||
"version: {} - You're up to date !",
|
||||
self.latest_version
|
||||
));
|
||||
}
|
||||
|
||||
if ui.button("Play").clicked() {
|
||||
println!("play clicked !");
|
||||
}
|
||||
|
||||
ui.add_space(ui.available_size_before_wrap().y / 2.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("egui theme:");
|
||||
widgets::global_dark_light_mode_buttons(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
20
LauncherSource/launcher_client_egui/src/main.rs
Executable file
20
LauncherSource/launcher_client_egui/src/main.rs
Executable file
@@ -0,0 +1,20 @@
|
||||
mod custom_window_frame;
|
||||
mod download_file;
|
||||
mod launcher;
|
||||
|
||||
use eframe::{epaint::Vec2, run_native, NativeOptions};
|
||||
use launcher::Launcher;
|
||||
|
||||
fn main() {
|
||||
_ = run_native(
|
||||
"Howling Launcher",
|
||||
NativeOptions {
|
||||
decorated: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
initial_window_size: Some(Vec2 { x: 500.0, y: 350.0 }),
|
||||
..Default::default()
|
||||
},
|
||||
Box::new(|cc| Box::new(Launcher::new(cc))),
|
||||
);
|
||||
}
|
||||
13
LauncherSource/launcher_client_iced/Cargo.toml
Normal file
13
LauncherSource/launcher_client_iced/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "launcher_client_iced"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
iced = { version = "0.8", features = ["tokio"] }
|
||||
iced_futures = "0.6"
|
||||
iced_native = "0.9"
|
||||
reqwest = { version = "0.11", features = ["rustls-tls"] }
|
||||
directories = "4.0"
|
||||
94
LauncherSource/launcher_client_iced/src/download.rs
Normal file
94
LauncherSource/launcher_client_iced/src/download.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use iced_native::subscription;
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
// Just a little utility function
|
||||
pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
|
||||
id: I,
|
||||
url: T,
|
||||
) -> iced::Subscription<(I, Progress)> {
|
||||
subscription::unfold(id, State::Ready(url.to_string()), move |state| {
|
||||
download(id, state)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Clone)]
|
||||
pub struct Download<I> {
|
||||
id: I,
|
||||
url: String,
|
||||
}
|
||||
|
||||
async fn download<I: Copy>(
|
||||
id: I,
|
||||
state: State,
|
||||
) -> (Option<(I, Progress)>, State) {
|
||||
match state {
|
||||
State::Ready(url) => {
|
||||
let response = reqwest::get(&url).await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
if let Some(total) = response.content_length() {
|
||||
(
|
||||
Some((id, Progress::Started)),
|
||||
State::Downloading {
|
||||
response,
|
||||
total,
|
||||
downloaded: 0,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(Some((id, Progress::Errored)), State::Finished)
|
||||
}
|
||||
}
|
||||
Err(_) => (Some((id, Progress::Errored)), State::Finished),
|
||||
}
|
||||
}
|
||||
State::Downloading {
|
||||
mut response,
|
||||
total,
|
||||
downloaded,
|
||||
} => match response.chunk().await {
|
||||
Ok(Some(chunk)) => {
|
||||
let downloaded = downloaded + chunk.len() as u64;
|
||||
|
||||
let percentage = (downloaded as f32 / total as f32) * 100.0;
|
||||
|
||||
(
|
||||
Some((id, Progress::Advanced(percentage))),
|
||||
State::Downloading {
|
||||
response,
|
||||
total,
|
||||
downloaded,
|
||||
},
|
||||
)
|
||||
}
|
||||
Ok(None) => (Some((id, Progress::Finished)), State::Finished),
|
||||
Err(_) => (Some((id, Progress::Errored)), State::Finished),
|
||||
},
|
||||
State::Finished => {
|
||||
// We do not let the stream die, as it would start a
|
||||
// new download repeatedly if the user is not careful
|
||||
// in case of errors.
|
||||
iced::futures::future::pending().await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Progress {
|
||||
Started,
|
||||
Advanced(f32),
|
||||
Finished,
|
||||
Errored,
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
Ready(String),
|
||||
Downloading {
|
||||
response: reqwest::Response,
|
||||
total: u64,
|
||||
downloaded: u64,
|
||||
},
|
||||
Finished,
|
||||
}
|
||||
199
LauncherSource/launcher_client_iced/src/main.rs
Normal file
199
LauncherSource/launcher_client_iced/src/main.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use iced::executor;
|
||||
use iced::widget::{button, column, container, progress_bar, text, Column};
|
||||
use iced::{
|
||||
Alignment, Application, Command, Element, Length, Settings, Subscription,
|
||||
Theme,
|
||||
};
|
||||
|
||||
mod download;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
Example::run(Settings::default())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Example {
|
||||
downloads: Vec<Download>,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Add,
|
||||
Download(usize),
|
||||
DownloadProgressed((usize, download::Progress)),
|
||||
}
|
||||
|
||||
impl Application for Example {
|
||||
type Message = Message;
|
||||
type Theme = Theme;
|
||||
type Executor = executor::Default;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Example, Command<Message>) {
|
||||
(
|
||||
Example {
|
||||
downloads: vec![Download::new(0)],
|
||||
last_id: 0,
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Download progress - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Add => {
|
||||
self.last_id += 1;
|
||||
|
||||
self.downloads.push(Download::new(self.last_id));
|
||||
}
|
||||
Message::Download(index) => {
|
||||
if let Some(download) = self.downloads.get_mut(index) {
|
||||
download.start();
|
||||
}
|
||||
}
|
||||
Message::DownloadProgressed((id, progress)) => {
|
||||
if let Some(download) =
|
||||
self.downloads.iter_mut().find(|download| download.id == id)
|
||||
{
|
||||
download.progress(progress);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(self.downloads.iter().map(Download::subscription))
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let downloads = Column::with_children(
|
||||
self.downloads.iter().map(Download::view).collect(),
|
||||
)
|
||||
.push(
|
||||
button("Add another download")
|
||||
.on_press(Message::Add)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::End);
|
||||
|
||||
container(downloads)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.padding(20)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Download {
|
||||
id: usize,
|
||||
state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Idle,
|
||||
Downloading { progress: f32 },
|
||||
Finished,
|
||||
Errored,
|
||||
}
|
||||
|
||||
impl Download {
|
||||
pub fn new(id: usize) -> Self {
|
||||
Download {
|
||||
id,
|
||||
state: State::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
match self.state {
|
||||
State::Idle { .. }
|
||||
| State::Finished { .. }
|
||||
| State::Errored { .. } => {
|
||||
self.state = State::Downloading { progress: 0.0 };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&mut self, new_progress: download::Progress) {
|
||||
if let State::Downloading { progress } = &mut self.state {
|
||||
match new_progress {
|
||||
download::Progress::Started => {
|
||||
*progress = 0.0;
|
||||
}
|
||||
download::Progress::Advanced(percentage) => {
|
||||
*progress = percentage;
|
||||
}
|
||||
download::Progress::Finished => {
|
||||
self.state = State::Finished;
|
||||
}
|
||||
download::Progress::Errored => {
|
||||
self.state = State::Errored;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
match self.state {
|
||||
State::Downloading { .. } => {
|
||||
download::file(self.id, "https://speed.hetzner.de/100MB.bin?")
|
||||
.map(Message::DownloadProgressed)
|
||||
}
|
||||
_ => Subscription::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let current_progress = match &self.state {
|
||||
State::Idle { .. } => 0.0,
|
||||
State::Downloading { progress } => *progress,
|
||||
State::Finished { .. } => 100.0,
|
||||
State::Errored { .. } => 0.0,
|
||||
};
|
||||
|
||||
let progress_bar = progress_bar(0.0..=100.0, current_progress);
|
||||
|
||||
let control: Element<_> = match &self.state {
|
||||
State::Idle => button("Start the download!")
|
||||
.on_press(Message::Download(self.id))
|
||||
.into(),
|
||||
State::Finished => {
|
||||
column!["Download finished!", button("Start again")]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
State::Downloading { .. } => {
|
||||
text(format!("Downloading... {current_progress:.2}%")).into()
|
||||
}
|
||||
State::Errored => column![
|
||||
"Something went wrong :(",
|
||||
button("Try again").on_press(Message::Download(self.id)),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
};
|
||||
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.padding(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(progress_bar)
|
||||
.push(control)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
8
LauncherSource/launcher_server/Cargo.toml
Normal file
8
LauncherSource/launcher_server/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "launcher_server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
3
LauncherSource/launcher_server/src/main.rs
Normal file
3
LauncherSource/launcher_server/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
9
LauncherSource/launcher_shared/Cargo.toml
Normal file
9
LauncherSource/launcher_shared/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "launcher_shared"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sha2 = "0.10"
|
||||
23
LauncherSource/launcher_shared/src/lib.rs
Normal file
23
LauncherSource/launcher_shared/src/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs::File, io};
|
||||
|
||||
pub fn file_hash(filename: &str) -> Result<String, std::io::Error> {
|
||||
let mut file = File::open(filename)?;
|
||||
let mut hasher = Sha256::new();
|
||||
io::copy(&mut file, &mut hasher)?;
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let hash = file_hash("Cargo.toml").unwrap();
|
||||
assert_eq!(
|
||||
hash,
|
||||
"02272c4c973d0bf266b571bf596797f46602ae6796d33df4b4057a170483d8f8"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user