Migration

This commit is contained in:
2024-03-21 09:35:29 +01:00
commit ffc6304f6e
8206 changed files with 26300 additions and 0 deletions

14
LauncherSource/.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,10 @@
[workspace]
members = [
"launcher_shared",
"launcher_client_egui",
"launcher_client_iced",
"launcher_server",
]
resolver = "2"

View 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"

View 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);
}
}

View 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(())
// }

View 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);
});
});
}
}

View 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))),
);
}

View 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"

View 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,
}

View 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()
}
}

View 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]

View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View 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"

View 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"
);
}
}