Compare commits

..

No commits in common. "master" and "0.1.0" have entirely different histories.

36 changed files with 3712 additions and 984 deletions

View File

@ -1,15 +1,13 @@
name: build
on:
workflow_dispatch:
push:
branches: [main, master]
branches: [ main, master ]
paths:
- ".github/**"
- "rusty_hub_egui/**"
- "unity_hub_lib/**"
- 'egui_client/**'
- 'rusty_hub/**'
pull_request:
branches: [main, master]
branches: [ main, master ]
env:
CARGO_TERM_COLOR: always
@ -18,11 +16,7 @@ jobs:
build-unix:
runs-on: ubuntu-latest
steps:
- name: Hack sources.list
run: sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu/|http://mirror.arizona.edu/ubuntu/|g' /etc/apt/sources.list
- name: Update res
run: sudo apt-get update
- name: Install dependencies
- name: Install dep
run: sudo apt-get install -y mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libatk1.0-dev librust-gdk-sys-dev
- run: rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2
@ -50,10 +44,6 @@ jobs:
# Determines if the cache should be saved even when the workflow has failed.
# Default: "false"
cache-on-failure: ""
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Build
run: cd rusty_hub_egui && cargo build --verbose
- uses: actions/upload-artifact@v4
with:
name: upload executable
path: rusty_hub_egui/target/release/rusty_hub_egui
run: cd egui_client && cargo build --verbose

View File

@ -12,10 +12,10 @@ jobs:
steps:
- run: rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Build
run: cd rusty_hub_egui && cargo build --release --verbose
- uses: actions/upload-artifact@v4
run: cd egui_client && cargo build --release --verbose
- uses: actions/upload-artifact@v3
with:
name: build-exe
path: rusty_hub_egui/target/release/*exe
path: egui_client/target/release/*exe

3
.gitignore vendored
View File

@ -1,5 +1,2 @@
/target
/*/target
unity_hub_lib/Cargo.lock
Cargo.lock
/*/.idea

View File

@ -1,8 +0,0 @@
# [0.2.0]
- Update egui to 0.21 version
- Initial macOS support
- Light theme
# [0.1.0]
- Initial version

View File

@ -1,13 +0,0 @@
[workspace]
resolver = "3"
members = ["rusty_hub_egui", "unity_hub_lib"]
[profile.release]
opt-level = 'z'
panic = 'abort'
lto = true
[profile.dev.package."*"]
opt-level = 2

View File

@ -1,29 +1,24 @@
# Rusty Hub [![build](https://github.com/Leinnan/rusty_hub/actions/workflows/rust.yml/badge.svg)](https://github.com/Leinnan/rusty_hub/actions/workflows/rust.yml)
https://www.mevlyshkin.com/projects/rusty_unity_hub/
Very simple alternative for Unity Hub. Rust pet project. For now it does work properly only on Windows, but support for other platforms is on the roadmap.
Very simple alternative for Unity Hub. Rust pet project.
![rusty_hub_egui_JU3JdNtfpz](https://user-images.githubusercontent.com/13188195/192162924-2f8eaef5-fc65-47f2-834c-f8abb704451d.gif)
## Download
There is ready to download Windows version. Go to [Releases](https://github.com/Leinnan/rusty_hub/releases) and look for `windows-build.zip` in `Assets`.
## Building and using
[Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) is required in order to build it.
Building is pretty simple, just copy repo, open `rusty_hub_egui` subdirectory in `CLI` client and run these commands:
Building is pretty simple, just copy repo, open `egui_client` subdirectory in `CLI` client and run these commands:
```sh
git clone git@github.com:Leinnan/rusty_hub.git
cd rusty_hub/rusty_hub_egui
cd rusty_hub/egui_client
cargo build --release
cargo run --release
```
## Thanks
Big thanks to https://github.com/unitycoder/UnityLauncherPro

2404
egui_client/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
egui_client/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "rusty_hub_egui"
version = "0.1.0"
edition = "2021"
homepage = "https://github.com/Leinnan/rusty_hub"
build = "build.rs"
[profile.release]
opt-level = 'z'
panic = 'abort'
lto = true
[profile.dev.package."*"]
opt-level = 2
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
confy = "^0.5.0"
eframe = "0.19.0"
egui_extras = "0.19.0"
rusty_hub = { path="../rusty_hub" }
image = { version = "0.24.0", default-features = false, features = ["png"] }
rfd = "0.10.0"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"

View File

@ -9,4 +9,5 @@ fn main() {
}
#[cfg(unix)]
fn main() {}
fn main() {
}

View File

@ -2,6 +2,3 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
pub const APP_NAME: &str = "Rusty Unity Hub";
pub const VERTICAL_SPACING: f32 = 8.0;
pub const TOP_BUTTON_WIDTH: f32 = 150.0;
pub const TOP_SIDE_MARGIN: f32 = 10.0;
pub const HEADER_HEIGHT: f32 = 45.0;

View File

@ -0,0 +1,448 @@
use crate::{
consts::HOMEPAGE,
consts::{APP_NAME, VERSION, VERTICAL_SPACING},
window_tab::WindowTab,
};
use eframe::{
egui::{self, Layout, Ui},
epaint::Color32,
};
use egui_extras::{Size, TableBuilder};
use rfd::FileDialog;
use rusty_hub::hub::Hub;
pub struct HubClient {
hub: Hub,
current_tab: WindowTab,
}
impl HubClient {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let hub_option = confy::load("lwa_unity_hub", "config");
let hub = if hub_option.is_ok() {
hub_option.unwrap()
} else {
Hub::default()
};
let mut client = Self {
hub,
current_tab: WindowTab::Projects,
};
client.save_config(true);
client
}
fn save_config(&mut self, rebuild: bool) {
if rebuild {
self.hub.update_info();
}
let _ = confy::store("lwa_unity_hub", "config", &self.hub);
}
pub fn draw_central_panel(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
match self.current_tab {
WindowTab::Projects => self.draw_project(&ctx, ui),
WindowTab::Editors => self.draw_editors(&ctx, ui),
};
});
});
}
fn draw_editors(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
ui.label(egui::RichText::new("Editor search paths").heading());
ui.add_space(VERTICAL_SPACING);
let text_height = egui::TextStyle::Body.resolve(&ui.style()).size * 2.0;
ui.scope(|ui| {
let table = TableBuilder::new(ui)
.striped(false)
.scroll(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Size::initial(150.0).at_least(150.0))
.column(Size::remainder().at_least(260.0))
.resizable(false);
let paths = self.hub.config.unity_search_paths.clone();
table.body(|body| {
body.rows(text_height, paths.len(), |row_index, mut row| {
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING - 2.0);
if ui.button("Remove").clicked() {
self.hub.config.unity_search_paths.remove(row_index);
self.save_config(true);
return;
}
});
});
row.col(|ui| {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Max),
|ui| {
ui.add_space(VERTICAL_SPACING);
ui.label(&paths[row_index]);
},
);
});
});
});
});
ui.add_space(VERTICAL_SPACING * 2.0);
ui.label(egui::RichText::new("Installed editor versions").heading());
ui.add_space(VERTICAL_SPACING);
let table2 = TableBuilder::new(ui)
.striped(true)
.scroll(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Size::initial(100.0).at_least(100.0).at_most(120.0))
.column(Size::initial(150.0).at_least(150.0).at_most(400.0))
.column(Size::remainder().at_least(260.0))
.resizable(false);
table2.body(|body| {
body.rows(
text_height,
self.hub.config.editors_configurations.len(),
|row_index, mut row| {
let editor = &self.hub.config.editors_configurations[row_index];
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING);
ui.label(&editor.version);
});
});
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING);
ui.label(egui::RichText::new(editor.platforms.join(",")).small());
});
});
row.col(|ui| {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Max),
|ui| {
ui.add_space(VERTICAL_SPACING);
ui.label(&editor.base_path);
},
);
});
},
);
});
}
fn draw_project(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
let text_height = egui::TextStyle::Body.resolve(ui.style()).size * 2.0;
let table = TableBuilder::new(ui)
.striped(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Size::initial(150.0).at_least(150.0))
.column(Size::initial(90.0).at_least(40.0))
.column(Size::initial(90.0).at_least(90.0))
.column(Size::remainder().at_least(260.0))
.resizable(false);
table
.header(25.0, |mut header| {
header.col(|ui| {
ui.heading("Name");
ui.add_space(VERTICAL_SPACING);
});
header.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.heading("Version");
ui.add_space(VERTICAL_SPACING);
});
});
header.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.heading("Branch");
ui.add_space(VERTICAL_SPACING);
});
});
header.col(|ui| {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Max),
|ui| {
ui.heading("Directory");
ui.add_space(VERTICAL_SPACING);
},
);
});
})
.body(|body| {
body.rows(
text_height,
self.hub.projects.len(),
|row_index, mut row| {
let project = &self.hub.projects[row_index];
let editor_for_project_exists =
self.hub.editor_for_project(project).is_some();
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING - 2.0);
if ui
.add_enabled(
editor_for_project_exists,
egui::Button::new(format!("{}", &project.title)),
)
.on_disabled_hover_text(format!(
"Select different Unity version"
))
.clicked()
{
self.hub.run_project_nr(row_index);
}
ui.add_space(VERTICAL_SPACING);
});
});
row.col(|ui| {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Center),
|ui| {
ui.add_space(VERTICAL_SPACING);
let mut text = egui::RichText::new(&project.version);
if !editor_for_project_exists {
text = text.color(Color32::RED);
}
let version_response =
ui.add(egui::Label::new(text).sense(egui::Sense::click()));
version_response.context_menu(|ui| {
for editor in &self.hub.config.editors_configurations {
let mut text = egui::RichText::new(format!(
"Open in {}",
&editor.version
));
if editor.version.contains(&project.version) {
text = text.strong().color(Color32::GREEN);
}
if ui.button(text).clicked() {
Hub::run_project(&editor, &project);
ui.close_menu();
}
}
});
},
);
});
row.col(|ui| {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Max),
|ui| {
if project.branch.len() < 1 {
return;
}
ui.add_space(VERTICAL_SPACING);
const MAX_BRANCH_LEN: usize = 15;
let is_long = project.branch.len() > MAX_BRANCH_LEN;
let short = if !is_long {
project.branch.clone()
} else {
let mut result =
String::from(&project.branch[0..MAX_BRANCH_LEN]);
result.push_str("...");
result
};
let label = ui.label(egui::RichText::new(short).small());
if is_long {
label.on_hover_text(&project.branch);
}
},
);
});
row.col(|ui| {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Max),
|ui| {
ui.add_space(VERTICAL_SPACING);
let path_response = ui.add(
egui::Label::new(&project.path).sense(egui::Sense::click()),
);
path_response.context_menu(|ui| {
if ui.button("Open directory").clicked() {
use std::process::Command;
Command::new("explorer")
.arg(&project.path)
.spawn()
.unwrap();
ui.close_menu();
}
});
},
);
});
},
);
});
}
fn draw_editors_header(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
let text_height = egui::TextStyle::Body.resolve(ui.style()).size * 2.0;
let table = build_header_table(ui);
table.body(|body| {
body.rows(text_height, 1, |_, mut row| {
row.col(|ui| {
add_header(ui);
});
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING);
if ui
.button("Add new path")
.on_hover_text("Add new editor search path")
.clicked()
{
let directory = FileDialog::new().pick_folder();
if let Some(dir) = directory {
self.hub
.config
.unity_search_paths
.push(dir.into_os_string().into_string().unwrap());
self.save_config(true);
}
}
});
});
row.col(|_ui| {});
});
});
}
fn draw_project_header(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
let text_height = egui::TextStyle::Body.resolve(ui.style()).size * 2.0;
let table = build_header_table(ui);
table.body(|body| {
body.rows(text_height, 1, |_, mut row| {
row.col(|ui| {
add_header(ui);
});
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING);
if ui
.button("Scan")
.on_hover_text("Scan selected folder for projects")
.clicked()
{
let directory = FileDialog::new().pick_folder();
if let Some(dir) = directory {
let amount = self.hub.search_for_projects_at_path(&dir);
let mut message =
rfd::MessageDialog::new().set_title("Search ended");
match amount {
0 => {
message = message
.set_description("No new projects found.")
.set_level(rfd::MessageLevel::Warning)
}
1 => message = message.set_description("Project founded!"),
_ => {
message = message.set_description(&format!(
"Founded {} projects.",
amount
))
}
}
message.show();
self.save_config(true);
}
}
});
});
row.col(|_ui| {});
});
});
}
fn draw_side_panel(&mut self, ui: &mut Ui) {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Min),
|ui| {
ui.add_space(VERTICAL_SPACING);
let button = egui::Button::new(egui::RichText::new("📦 Projects").heading())
.frame(&self.current_tab == &WindowTab::Projects);
if ui
.add_enabled(&self.current_tab != &WindowTab::Projects, button)
.clicked()
{
self.current_tab = WindowTab::Projects;
}
ui.add_space(VERTICAL_SPACING);
if ui
.add_enabled(
&self.current_tab != &WindowTab::Editors,
egui::Button::new(egui::RichText::new("🛠 Editors").heading())
.frame(&self.current_tab == &WindowTab::Editors),
)
.clicked()
{
self.current_tab = WindowTab::Editors;
}
},
);
}
}
fn build_header_table(ui: &mut Ui) -> TableBuilder {
let table = TableBuilder::new(ui)
.striped(false)
.scroll(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Size::remainder().at_least(150.0))
.column(Size::initial(100.0).at_most(100.0))
.column(Size::initial(5.0).at_most(5.0))
.resizable(false);
table
}
fn add_header(ui: &mut Ui) {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Min),
|ui| {
ui.add_space(5.0);
let text = egui::RichText::new(APP_NAME).heading().strong();
ui.add(egui::Label::new(text));
},
);
}
impl eframe::App for HubClient {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::TopBottomPanel::top("topPanel")
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.add_space(14.0);
match self.current_tab {
WindowTab::Projects => self.draw_project_header(&ctx, ui),
WindowTab::Editors => self.draw_editors_header(&ctx, ui),
};
ui.add_space(14.0);
});
});
egui::SidePanel::left("dsadsa")
.resizable(false)
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
self.draw_side_panel(ui);
});
self.draw_central_panel(&ctx);
egui::TopBottomPanel::bottom("bottomPanel").show(ctx, |ui| {
ui.with_layout(Layout::right_to_left(eframe::emath::Align::Center), |ui| {
egui::widgets::global_dark_light_mode_switch(ui);
ui.hyperlink_to(
format!("{} v {}", egui::special_emojis::GITHUB, VERSION),
HOMEPAGE,
);
});
});
}
}

38
egui_client/src/main.rs Normal file
View File

@ -0,0 +1,38 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
extern crate confy;
use consts::{APP_NAME, VERSION};
use eframe::{egui, IconData, NativeOptions};
use std::io::Cursor;
mod consts;
mod hub_client;
mod window_tab;
fn main() {
let img = image::io::Reader::new(Cursor::new(include_bytes!("../static/hub.png")))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
let icon = IconData {
width: 32,
height: 32,
rgba: img.into_rgba8().into_raw(),
};
let options = NativeOptions {
always_on_top: false,
maximized: false,
decorated: true,
fullscreen: false,
drag_and_drop_support: false,
initial_window_size: Some(egui::vec2(850.0, 400.0)),
min_window_size: Some(egui::vec2(850.0, 400.0)),
icon_data: Some(icon),
..NativeOptions::default()
};
eframe::run_native(
&format!("{} v {}", APP_NAME, VERSION),
options,
Box::new(|cc| Box::new(crate::hub_client::HubClient::new(cc))),
);
}

View File

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

491
rusty_hub/Cargo.lock generated Normal file
View File

@ -0,0 +1,491 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
dependencies = [
"libc",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]]
name = "exe"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2327a5b94310f33654812ff74ec7cd226a89eb430c46d4e1308c7e895da12f19"
dependencies = [
"bitflags",
"byteorder",
"chrono",
"hex",
"md-5",
"num-traits",
"pkbuffer",
"sha-1",
"sha2",
"widestring",
]
[[package]]
name = "generic-array"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "iana-time-zone"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "js-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "md-5"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
dependencies = [
"block-buffer",
"digest",
"opaque-debug",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "pkbuffer"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c63758cdd196779b1d782c6c5f568cbd6b412eb3c51eab711f08e83cd57edef"
dependencies = [
"pkbuffer_derive",
]
[[package]]
name = "pkbuffer_derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c86ad26b9715c9c1664b79f6e5c44baf38fb87a5133bdd7ec90baff7b71d155"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "registry"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e4b158bf49b0d000013487636c92268de4cfd26cdbb629f020a612749f12c4"
dependencies = [
"bitflags",
"log",
"thiserror",
"utfx",
"winapi",
]
[[package]]
name = "rusty_hub"
version = "0.1.0"
dependencies = [
"exe",
"registry",
"serde",
"serde_derive",
"walkdir",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
[[package]]
name = "serde_derive"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha-1"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
dependencies = [
"block-buffer",
"cfg-if",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer",
"cfg-if",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]]
name = "syn"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-ident"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "utfx"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133bf74f01486773317ddfcde8e2e20d2933cc3b68ab797e5d718bef996a81de"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "widestring"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

21
rusty_hub/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "rusty_hub"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 'z'
panic = 'abort'
lto = true
[profile.dev.package."*"]
opt-level = 2
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "^1.0"
serde_derive = "^1.0"
walkdir = "^2.3.2"
exe = "^0.5.4"
registry = "1.2.2"

77
rusty_hub/src/config.rs Normal file
View File

@ -0,0 +1,77 @@
use walkdir::{DirEntry, WalkDir};
use crate::unity_editor::UnityEditor;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Configuration {
pub unity_search_paths: Vec<String>,
pub editors_configurations: Vec<UnityEditor>,
}
impl Configuration {
pub fn rebuild(&mut self) {
self.editors_configurations = Vec::new();
let paths = self.get_unity_paths();
for path in &paths {
let editor = UnityEditor::new(&path);
if editor.is_none() {
continue;
}
let editor = editor.unwrap();
if !self.editors_configurations.contains(&editor) {
self.editors_configurations.push(editor);
}
}
}
pub fn get_unity_paths(&self) -> Vec<String> {
let mut paths = Vec::new();
for path in &self.unity_search_paths {
paths.extend(Configuration::search_for_editor(path.as_str()));
}
paths
}
fn is_unity_dir(entry: &DirEntry) -> bool {
let uninstall_exists = entry.path().clone().join("Uninstall.exe").exists();
let unity_exe_exists = entry.path().clone().join("Unity.exe").exists();
uninstall_exists && unity_exe_exists
}
fn search_for_editor(path: &str) -> Vec<String> {
let path_exists = std::fs::metadata(path).is_ok();
if !path_exists {
return Vec::new();
}
let mut result_paths: Vec<String> = Vec::new();
for entry in WalkDir::new(path)
.max_depth(5)
.into_iter()
.filter_entry(|_| true)
{
if entry.is_ok() {
let entry_unwraped = entry.unwrap();
let success = Configuration::is_unity_dir(&entry_unwraped);
if success {
result_paths.push(entry_unwraped.path().to_string_lossy().into());
}
}
}
result_paths
}
}
impl Default for Configuration {
fn default() -> Self {
let mut default = Self {
unity_search_paths: vec!["C:\\Program Files\\Unity\\Hub".to_string()],
editors_configurations: Vec::new(),
};
default.rebuild();
default
}
}

View File

@ -1,10 +1,9 @@
use crate::{config::Configuration, unity_editor::UnityEditor, unity_project::UnityProject};
use dpc_pariter::IteratorExt;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::{path::PathBuf, process::Command};
use walkdir::WalkDir;
use crate::{config::Configuration, unity_editor::UnityEditor, unity_project::UnityProject};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Hub {
pub config: Configuration,
@ -16,28 +15,11 @@ impl Hub {
Self { config, projects }
}
pub fn update_data(&mut self) {
pub fn update_info(&mut self) {
self.config.rebuild();
self.update_projects_info();
}
pub fn update_projects_info(&mut self) {
let mut registry = UnityProject::get_projects_from_registry()
.into_iter()
.filter(|p| !self.projects.contains(p))
.collect();
self.projects.append(&mut registry);
self.projects = self
.projects
.iter()
.cloned()
.collect::<HashSet<UnityProject>>()
.into_iter()
.collect();
self.projects.iter_mut().for_each(|project| {
for project in self.projects.iter_mut() {
project.update_info();
});
self.projects.sort_by(|a, b| b.edit_time.cmp(&a.edit_time));
}
}
pub fn run_project_nr(&self, nr: usize) {
@ -70,28 +52,30 @@ impl Hub {
pub fn search_for_projects_at_path(&mut self, path: &PathBuf) -> usize {
let path_exists = std::fs::metadata(path).is_ok();
let mut result = 0;
if !path_exists {
return 0;
return result;
}
let projects = self.projects.clone();
let new_projects: Vec<UnityProject> = WalkDir::new(path)
for entry in WalkDir::new(path)
.max_depth(3)
.into_iter()
.parallel_filter(|entry| entry.is_ok())
.parallel_map(|entry| {
UnityProject::try_get_project_at_path(
&entry.unwrap().path().as_os_str().to_str().unwrap(),
)
})
.parallel_filter(|project| project.is_some())
.parallel_map(|project| project.unwrap())
.parallel_filter(move |p| !projects.contains(p))
.collect();
.filter_entry(|_| true)
{
let projects = self.projects.clone();
if entry.is_err() {
continue;
}
let len = new_projects.len();
self.projects.extend(new_projects);
len
let entry_unwraped = entry.unwrap();
let path_string = entry_unwraped.path().as_os_str().to_str();
if let Some(project) = UnityProject::try_get_project_at_path(&path_string.unwrap()) {
if !projects.contains(&project) {
self.projects.push(project);
result = result + 1;
}
}
}
result
}
}
impl Default for Hub {

View File

@ -1,6 +1,7 @@
#[macro_use]
extern crate serde_derive;
pub mod config;
pub mod consts;
pub mod hub;
pub mod project_template;
pub mod unity_editor;
pub mod unity_project;

View File

@ -0,0 +1,99 @@
use exe::pe::VecPE;
use exe::VSVersionInfo;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnityEditor {
pub version: String,
pub exe_path: String,
pub base_path: String,
pub platforms: Vec<String>,
}
impl PartialEq for UnityEditor {
fn eq(&self, other: &Self) -> bool {
self.exe_path == other.exe_path
}
}
impl UnityEditor {
pub fn new(path: &str) -> Option<Self> {
let base_path = Path::new(path);
let exe_path = base_path.join("Unity.exe");
if !std::fs::metadata(&exe_path).is_ok() {
return None;
}
let image = VecPE::from_disk_file(base_path.join("Unity.exe")).unwrap();
let vs_version_check = VSVersionInfo::parse(&image);
if vs_version_check.is_err() {
return None;
}
let mut version = None;
if let Some(string_file_info) = vs_version_check.unwrap().string_file_info {
let hashmap = string_file_info.children[0].string_map();
if let Some(result_version) = hashmap.get("ProductVersion") {
version = Some(result_version.clone());
if let Some(short) = result_version.clone().split("_").take(1).next() {
version = Some(short.to_string());
}
}
}
if version.is_none() {
None
} else {
Some(Self {
version: version.unwrap().clone(),
exe_path: exe_path.into_os_string().into_string().unwrap(),
base_path: String::from(path),
platforms: UnityEditor::get_platforms(path),
})
}
}
fn get_platforms(unity_folder: &str) -> Vec<String> {
let platform_names = HashMap::from([
("androidplayer", "Android"),
("windowsstandalonesupport", "Windows"),
("linuxstandalonesupport", "Linux"),
("LinuxStandalone", "Linux"),
("OSXStandalone", "OSX"),
("webglsupport", "WebGL"),
("metrosupport", "UWP"),
("iossupport", "iOS"),
]);
let mut platforms = Vec::new();
let base_path = Path::new(unity_folder).join("Data").join("PlaybackEngines");
if !std::fs::metadata(&base_path).is_ok() {
return platforms;
}
let dir = std::fs::read_dir(base_path);
if dir.is_err() {
return platforms;
}
for path in dir.unwrap() {
if path.is_err() {
continue;
}
let f = path.unwrap();
if let Ok(result_string) = f.file_name().into_string() {
if let Some(value) =
platform_names.get(&result_string.clone().to_lowercase().borrow())
{
platforms.push(value.to_string());
} else {
platforms.push(result_string);
}
}
}
platforms
}
}

View File

@ -1,16 +1,12 @@
use serde::{Deserialize, Serialize};
use std::{ops::Sub, path::Path, str};
use std::{path::Path, str};
use crate::consts;
#[derive(Debug, Serialize, Deserialize, Clone, Hash, Eq)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnityProject {
pub path: String,
pub title: String,
pub version: String,
pub branch: String,
pub is_valid: bool,
pub edit_time: std::time::SystemTime,
}
impl PartialEq for UnityProject {
@ -35,6 +31,7 @@ impl UnityProject {
Security::Read,
)
.unwrap();
println!("{}", key.to_string());
for value in key.values() {
if value.is_err() {
@ -51,6 +48,7 @@ impl UnityProject {
if let Some(result) = UnityProject::try_get_project_at_path(&project_path) {
projects.push(result);
}
println!("\t{}: {}", unwraped_name, project_path);
}
}
projects
@ -63,48 +61,33 @@ impl UnityProject {
std::fs::metadata(&one).is_ok() && std::fs::metadata(&two).is_ok()
}
pub fn get_version_at_path(path: &str) -> Option<String> {
pub fn try_get_project_at_path(path: &str) -> Option<UnityProject> {
let path = path.trim_matches(char::from(0)).replace("/", "\\");
if !UnityProject::is_project_at_path(&path) {
return None;
}
let project_version_file = std::fs::read_to_string(
Path::new(&path)
.join("ProjectSettings")
.join("ProjectVersion.txt"),
);
if project_version_file.is_err() {
return None;
}
let binding = project_version_file.unwrap();
let mut iter = binding.split_whitespace();
let project_version_file = project_version_file.unwrap();
let mut iter = project_version_file.split_whitespace();
iter.next();
let project_version = iter.next().unwrap().to_string();
Some(project_version)
}
pub fn try_get_project_at_path(path: &str) -> Option<UnityProject> {
#[cfg(windows)]
let path = path.trim_matches(char::from(0)).replace("/", "\\");
#[cfg(not(windows))]
let path = path.trim_matches(char::from(0)).to_string();
if !UnityProject::is_project_at_path(&path) {
return None;
}
let mut project = UnityProject {
path: path.clone(),
title: path.split(consts::SLASH).last().unwrap().to_string(),
Some(UnityProject {
path: path.to_string(),
title: path.split("\\").last().unwrap().to_string(),
version: project_version,
branch: String::new(),
version: String::new(),
is_valid: true,
edit_time: std::time::SystemTime::now()
.sub(std::time::Duration::new(60 * 60 * 24 * 365 * 30, 0)),
};
project.update_info();
Some(project)
})
}
pub fn update_info(&mut self) {
const HEAD_PREFIX: &str = "ref: refs/heads/";
let is_project = UnityProject::is_project_at_path(&self.path);
self.is_valid = is_project;
@ -113,43 +96,18 @@ impl UnityProject {
}
let mut base_path = Path::new(&self.path);
self.version = Self::get_version_at_path(&self.path).unwrap();
match self.try_read_from_path(base_path) {
None => {
while let Some(path) = base_path.parent() {
base_path = path;
let new_branch = self.try_read_from_path(base_path);
if new_branch.is_some() {
self.branch = new_branch.unwrap();
break;
}
}
}
Some(value) => {
self.branch = value;
}
}
if let Ok(meta) = std::fs::metadata(&self.path) {
if let Ok(data) = meta.modified() {
self.edit_time = data;
}
}
}
fn try_read_from_path(&self, path: &std::path::Path) -> Option<String> {
const HEAD_PREFIX: &str = "ref: refs/heads/";
let head_path = Path::new(&path).join(".git").join("HEAD");
if !head_path.exists() {
return None;
continue;
}
let head_content = std::fs::read_to_string(&head_path).expect("Could not read HEAD file");
let head_content =
std::fs::read_to_string(&head_path).expect("Could not read HEAD file");
if head_content.contains(HEAD_PREFIX) {
Some(head_content.replace(HEAD_PREFIX, "").trim().to_string())
} else {
None
self.branch = head_content.replace(HEAD_PREFIX, "").trim().to_string();
}
}
}
}

View File

@ -1,28 +0,0 @@
[package]
name = "rusty_hub_egui"
version = "0.2.0"
edition = "2021"
homepage = "https://github.com/Leinnan/rusty_hub"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
confy = "0.6"
eframe = { version = "0.32", default-features = false, features = [
#"accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
"wayland", # To support Linux (and CI)
] }
egui = "0.32"
egui_extras = "0.32"
unity_hub_lib = { path="../unity_hub_lib" }
rfd = "0.15"
inline_tweak = "1"
anyhow = "1"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"

View File

@ -1,449 +0,0 @@
use crate::{
consts::HOMEPAGE,
consts::{
APP_NAME, HEADER_HEIGHT, TOP_BUTTON_WIDTH, TOP_SIDE_MARGIN, VERSION, VERTICAL_SPACING,
},
window_tab::WindowTab,
};
use eframe::{
egui::{self, CursorIcon, Layout, Ui},
epaint::{Color32, FontFamily, FontId},
};
use egui_extras::{Column, TableBuilder};
use inline_tweak::*;
use rfd::FileDialog;
use unity_hub_lib::{consts::FILE_MANAGER, hub::Hub};
pub struct HubClient {
hub: Hub,
current_tab: WindowTab,
}
fn setup_custom_fonts(ctx: &egui::Context) {
// Start with the default fonts (we will be adding to them rather than replacing them).
let mut fonts = egui::FontDefinitions::default();
if let Ok((regular, semibold)) = get_fonts() {
fonts.font_data.insert(
"regular".to_owned(),
egui::FontData::from_owned(regular).into(),
);
fonts.font_data.insert(
"semibold".to_owned(),
egui::FontData::from_owned(semibold).into(),
);
// Put my font first (highest priority) for proportional text:
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "regular".to_owned());
fonts
.families
.entry(egui::FontFamily::Name("semibold".into()))
.or_default()
.insert(0, "semibold".to_owned());
// Put my font as last fallback for monospace:
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.push("regular".to_owned());
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
ctx.style_mut(|style| {
for font_id in style.text_styles.values_mut() {
font_id.size *= 1.4;
}
});
}
#[cfg(not(windows))]
fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
use std::fs;
let font_path = std::path::Path::new("/System/Library/Fonts");
let regular = fs::read(font_path.join("SFNSRounded.ttf"))?;
let semibold = fs::read(font_path.join("SFCompact.ttf"))?;
Ok((regular, semibold))
}
#[cfg(windows)]
fn get_fonts() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
use std::fs;
let app_data = std::env::var("APPDATA")?;
let font_path = std::path::Path::new(&app_data);
let regular = fs::read(font_path.join("../Local/Microsoft/Windows/Fonts/aptos.ttf"))?;
let semibold = fs::read(font_path.join("../Local/Microsoft/Windows/Fonts/aptos-semibold.ttf"))?;
Ok((regular, semibold))
}
impl HubClient {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
setup_custom_fonts(&cc.egui_ctx);
let hub_option = confy::load("rusty_hub_egui", "config");
let hub = if hub_option.is_ok() {
let mut h: Hub = hub_option.unwrap();
h.update_data();
h
} else {
Hub::default()
};
Self {
hub,
current_tab: WindowTab::Projects,
}
}
fn save_config(&mut self, rebuild: bool) {
if rebuild {
self.hub.update_data();
}
let _ = confy::store("rusty_hub_egui", "config", &self.hub);
}
pub fn draw_central_panel(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
match self.current_tab {
WindowTab::Projects => self.draw_project(ctx, ui),
WindowTab::Editors => self.draw_editors(ctx, ui),
};
});
});
}
fn draw_editors(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
ui.label(egui::RichText::new("Editor search paths").heading());
ui.add_space(VERTICAL_SPACING);
let text_height = egui::TextStyle::Body.resolve(ui.style()).size * 2.0;
let paths = self.hub.config.unity_search_paths.clone();
for (i, path) in paths.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(path);
let height = tweak!(30.0);
let button_width = tweak!(100.0);
ui.allocate_space(egui::vec2(
ui.available_width() - button_width - TOP_SIDE_MARGIN,
height,
));
if ui
.add_sized([button_width, height], egui::Button::new("🚮 Remove"))
.clicked()
{
self.hub.config.unity_search_paths.remove(i);
self.save_config(true);
}
});
}
ui.add_space(VERTICAL_SPACING * 2.0);
ui.label(egui::RichText::new("Installed editor versions").heading());
ui.add_space(VERTICAL_SPACING);
let table2 = TableBuilder::new(ui)
.striped(true)
.vscroll(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::initial(100.0).at_least(100.0).at_most(120.0))
.column(Column::initial(150.0).at_least(150.0).at_most(400.0))
.column(Column::remainder().at_least(260.0))
.resizable(false);
table2.body(|body| {
body.rows(
text_height,
self.hub.config.editors_configurations.len(),
|mut row| {
let row_index = row.index();
let editor = &self.hub.config.editors_configurations[row_index];
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING);
ui.label(&editor.version);
});
});
row.col(|ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(VERTICAL_SPACING);
ui.label(egui::RichText::new(editor.platforms.join(",")).small());
});
});
row.col(|ui| {
ui.with_layout(
Layout::top_down_justified(eframe::emath::Align::Max),
|ui| {
ui.add_space(VERTICAL_SPACING);
let version_response = ui.add(
egui::Label::new(&editor.base_path).sense(egui::Sense::click()),
);
version_response.context_menu(|ui| {
let text = egui::RichText::new("🗁 Open directory");
if ui.button(text).clicked() {
use std::process::Command;
Command::new(FILE_MANAGER)
.arg(&editor.base_path)
.spawn()
.unwrap();
ui.close();
}
});
},
);
});
},
);
});
}
fn draw_project(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
let text_height = egui::TextStyle::Body.resolve(ui.style()).size * tweak!(3.0);
let projects = self.hub.projects.clone();
for (i, project) in projects.iter().enumerate() {
let editor_for_project_exists = self.hub.editor_for_project(project).is_some();
ui.horizontal(|ui| {
let color = if i % 2 == 0 {
Color32::from_rgba_premultiplied(0, 0, 0, 30)
} else {
egui::Color32::TRANSPARENT
};
egui::Frame::NONE.fill(color).show(ui, |ui| {
ui.add_sized(
[text_height, text_height],
egui::Button::new("").frame(false),
)
.context_menu(|ui| {
ui.menu_button("Open in", |ui| {
if !editor_for_project_exists {
ui.add_enabled(
false,
egui::Button::new(
egui::RichText::new(format!(
"Missing: {}",
&project.version
))
.strong(),
),
);
}
for editor in &self.hub.config.editors_configurations {
let mut text = egui::RichText::new(editor.version.to_string());
if editor.version.contains(&project.version) {
text = text.strong().color(Color32::GREEN);
}
if ui.button(text).clicked() {
Hub::run_project(editor, project);
ui.close();
}
}
});
if ui.button("Open directory").clicked() {
use std::process::Command;
Command::new(FILE_MANAGER)
.arg(&project.path)
.spawn()
.unwrap();
ui.close();
}
});
ui.label(egui::RichText::new(project.title.to_string()).heading())
.on_hover_text(&project.path);
if !project.branch.is_empty() {
ui.add_space(TOP_SIDE_MARGIN);
const MAX_BRANCH_LEN: usize = 15;
let is_long = project.branch.len() > MAX_BRANCH_LEN;
let short = if !is_long {
project.branch.clone()
} else {
let mut result = String::from(&project.branch[0..MAX_BRANCH_LEN]);
result.push_str("...");
result
};
let label = ui.label(egui::RichText::new(short).small().weak());
if is_long {
label.on_hover_text(format!("{}", &project.branch));
}
}
let btn_width = tweak!(100.0);
ui.allocate_space(egui::vec2(
ui.available_width() - btn_width - TOP_SIDE_MARGIN,
text_height,
));
let text = if editor_for_project_exists {
egui::RichText::new("Open".to_string())
} else {
egui::RichText::new("Missing").weak()
};
let button = egui::Button::new(text);
let added_button = ui.add_sized([btn_width, text_height], button);
if !editor_for_project_exists {
added_button.on_hover_text_at_pointer(format!(
"Missing {} Unity",
&project.version
));
} else if added_button.clicked() {
self.hub.run_project_nr(i);
}
});
});
}
}
fn draw_editors_header(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
add_header(ui);
let available_width =
ui.available_width() - TOP_BUTTON_WIDTH - TOP_SIDE_MARGIN - TOP_SIDE_MARGIN;
ui.allocate_space(egui::vec2(available_width, HEADER_HEIGHT));
if ui
.add_sized(
[TOP_BUTTON_WIDTH, 30.0],
egui::Button::new("🖴 Add new path"),
)
.on_hover_text("Add new editor search path")
.clicked()
{
let directory = FileDialog::new().pick_folder();
if let Some(dir) = directory {
self.hub
.config
.unity_search_paths
.push(dir.into_os_string().into_string().unwrap());
self.save_config(true);
}
}
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, 10.0));
}
fn draw_project_header(&mut self, _ctx: &egui::Context, ui: &mut Ui) {
add_header(ui);
let available_width =
ui.available_width() - TOP_BUTTON_WIDTH - TOP_SIDE_MARGIN - TOP_SIDE_MARGIN;
ui.allocate_space(egui::vec2(available_width, HEADER_HEIGHT));
if ui
.add_sized(
[TOP_BUTTON_WIDTH, 30.0],
egui::Button::new("🔭 Scan for projects"),
)
.on_hover_text("Scan selected folder for projects")
.clicked()
{
let directory = FileDialog::new().pick_folder();
if let Some(dir) = directory {
let amount = self.hub.search_for_projects_at_path(&dir);
let mut message = rfd::MessageDialog::new().set_title("Search ended");
match amount {
0 => {
message = message
.set_description("No new projects found.")
.set_level(rfd::MessageLevel::Warning)
}
1 => message = message.set_description("Project founded!"),
_ => message = message.set_description(format!("Founded {} projects.", amount)),
}
message.show();
self.save_config(true);
}
}
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, 10.0));
}
fn tab_button(&self, ui: &mut Ui, tab: &WindowTab, text: &str) -> bool {
let button_size = tweak!(36.0);
let font_size = tweak!(16.0);
let button_size = egui::vec2(ui.available_width(), button_size);
let rich_text = if &self.current_tab == tab {
egui::RichText::new(text).strong()
} else {
egui::RichText::new(text).weak()
}
.font(FontId::new(font_size, FontFamily::Proportional));
let response = ui.add_sized(
button_size,
egui::Label::new(rich_text)
.selectable(false)
.sense(egui::Sense::click()),
);
if response.hovered() {
ui.output_mut(|o| o.cursor_icon = CursorIcon::PointingHand);
}
response.clicked()
}
fn draw_side_panel(&mut self, ui: &mut Ui) {
ui.with_layout(Layout::top_down(eframe::emath::Align::Min), |ui| {
if self.tab_button(ui, &WindowTab::Projects, tweak!("📦 Projects")) {
self.current_tab = WindowTab::Projects;
}
if self.tab_button(ui, &WindowTab::Editors, tweak!("🛠 Editors")) {
self.current_tab = WindowTab::Editors;
}
});
}
}
fn add_header(ui: &mut Ui) {
let text = egui::RichText::new(APP_NAME)
.font(FontId::new(26.0, FontFamily::Name("semibold".into())))
.strong();
ui.allocate_space(egui::vec2(TOP_SIDE_MARGIN, HEADER_HEIGHT));
ui.add(egui::Label::new(text).selectable(false));
}
impl eframe::App for HubClient {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::TopBottomPanel::top("topPanel")
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
ui.with_layout(
egui::Layout::left_to_right(egui::Align::Center)
.with_cross_align(eframe::emath::Align::Center),
|ui| {
match self.current_tab {
WindowTab::Projects => self.draw_project_header(ctx, ui),
WindowTab::Editors => self.draw_editors_header(ctx, ui),
};
},
);
});
egui::SidePanel::left("dsadsa")
.resizable(false)
.frame(egui::Frame::canvas(&ctx.style()))
.show(ctx, |ui| {
self.draw_side_panel(ui);
});
egui::TopBottomPanel::bottom("bottomPanel").show(ctx, |ui| {
ui.with_layout(Layout::right_to_left(eframe::emath::Align::Center), |ui| {
egui::widgets::global_theme_preference_switch(ui);
ui.hyperlink_to(
format!("{} v {}", egui::special_emojis::GITHUB, VERSION),
HOMEPAGE,
);
});
});
self.draw_central_panel(ctx);
}
}

View File

@ -1,30 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use consts::{APP_NAME, VERSION};
use eframe::egui;
mod consts;
mod hub_client;
mod window_tab;
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([850.0, 400.0])
.with_min_inner_size([850.0, 400.0])
.with_icon(
// NOTE: Adding an icon is optional
eframe::icon_data::from_png_bytes(&include_bytes!("../static/hub.png")[..])
.expect("Failed to load icon"),
),
..Default::default()
};
eframe::run_native(
&format!("{} v {}", APP_NAME, VERSION),
options,
Box::new(|cc| {
egui_extras::install_image_loaders(&cc.egui_ctx);
Ok(Box::new(crate::hub_client::HubClient::new(cc)))
}),
)
}

View File

@ -1,11 +0,0 @@
[package]
name = "unity_hub_lib"
version = "0.2.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
walkdir = "2"
exe = "0.5"
registry = "1.3"
dpc-pariter = "0.5.1"

View File

@ -1,79 +0,0 @@
use crate::{consts, unity_editor::UnityEditor};
use dpc_pariter::IteratorExt;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use walkdir::{DirEntry, WalkDir};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Configuration {
pub unity_search_paths: Vec<String>,
pub editors_configurations: Vec<UnityEditor>,
}
impl Configuration {
pub fn rebuild(&mut self) {
let paths = self.get_unity_paths();
self.editors_configurations = paths
.into_iter()
.parallel_map(|path| UnityEditor::new(&path))
.parallel_filter(|editor| editor.is_some())
.parallel_map(|editor| editor.unwrap())
.collect();
}
pub fn get_unity_paths(&self) -> Vec<String> {
let mut paths = Vec::new();
for path in &self.unity_search_paths {
paths.extend(Configuration::search_for_editor(path.as_str()));
}
paths
}
fn is_unity_dir(entry: &DirEntry) -> bool {
#[cfg(windows)]
let uninstall_exists = entry.path().join("Uninstall.exe").exists();
#[cfg(unix)]
let uninstall_exists = true; // just check that on windows only
let unity_exe_exists = entry.path().join(consts::UNITY_EXE_NAME).exists();
uninstall_exists && unity_exe_exists
}
fn search_for_editor(path: &str) -> Vec<String> {
let path_exists = std::fs::metadata(path).is_ok();
if !path_exists {
return Vec::new();
}
let hashset: HashSet<String> = WalkDir::new(path)
.max_depth(2)
.into_iter()
.parallel_filter(|entry| entry.is_ok())
.parallel_map(|entry| entry.unwrap())
.parallel_filter(|entry| Configuration::is_unity_dir(&entry))
.parallel_map(|entry| entry.path().to_string_lossy().into())
.collect();
Vec::from_iter(hashset)
}
}
impl Default for Configuration {
fn default() -> Self {
let mut default = Self {
#[cfg(windows)]
unity_search_paths: vec!["C:\\Program Files\\Unity\\Hub\\Editor".to_string()],
#[cfg(target_os = "macos")]
unity_search_paths: vec![
"/Applications/Unity/Hub/Editor".to_string(),
"/Applications/Unity/".to_string(),
],
#[cfg(target_os = "linux")]
unity_search_paths: vec!["~/Unity/Hub/Editor".to_string()],
editors_configurations: Vec::new(),
};
default.rebuild();
default
}
}

View File

@ -1,24 +0,0 @@
#[cfg(windows)]
pub const UNITY_EXE_NAME: &str = "Unity.exe";
#[cfg(target_os = "macos")]
pub const UNITY_EXE_NAME: &str = "Unity.app/Contents/MacOS/Unity";
#[cfg(target_os = "linux")]
pub const UNITY_EXE_NAME: &str = "Unity";
#[cfg(windows)]
pub const SLASH: &str = "\\";
#[cfg(unix)]
pub const SLASH: &str = "/";
#[cfg(windows)]
pub const FILE_MANAGER: &str = "explorer";
#[cfg(target_os = "macos")]
pub const FILE_MANAGER: &str = "open";
#[cfg(target_os = "linux")]
pub const FILE_MANAGER: &str = "xdg-open";
#[cfg(windows)]
pub const TEMPLATES_DIR: &str = "Data\\Resources\\PackageManager\\ProjectTemplates";
#[cfg(target_os = "macos")]
pub const TEMPLATES_DIR: &str = "Contents/Resources/PackageManager/ProjectTemplates";
#[cfg(target_os = "linux")]
pub const TEMPLATES_DIR: &str = "Data/Resources/PackageManager/ProjectTemplates";

View File

@ -1,36 +0,0 @@
use dpc_pariter::IteratorExt;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash)]
pub struct ProjectTemplate {
pub path: String,
pub title: String,
}
impl PartialEq for ProjectTemplate {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl ProjectTemplate {
pub fn find_templates(path: &str) -> Vec<ProjectTemplate> {
let dir = std::fs::read_dir(Path::new(&path).join(crate::consts::TEMPLATES_DIR));
if dir.is_err() {
return Vec::new();
}
dir.unwrap()
.into_iter()
.parallel_filter(|path| path.is_ok())
.parallel_map(|path| path.unwrap())
.parallel_filter(|path| path.file_name().into_string().unwrap().contains(".tgz"))
.parallel_map(|path| Self {
path: path.path().to_str().unwrap().to_string(),
title: path.file_name().into_string().unwrap().replace(".tgz", ""),
})
.collect()
}
}

View File

@ -1,122 +0,0 @@
use crate::consts;
use crate::project_template::ProjectTemplate;
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::Hash;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash)]
pub struct UnityEditor {
pub version: String,
pub exe_path: String,
pub base_path: String,
pub platforms: Vec<String>,
pub templates: Vec<ProjectTemplate>,
}
impl PartialEq for UnityEditor {
fn eq(&self, other: &Self) -> bool {
self.exe_path == other.exe_path
}
}
impl UnityEditor {
pub fn new(path: &str) -> Option<Self> {
let base_path = Path::new(path);
let exe_path = base_path.join(consts::UNITY_EXE_NAME);
let meta = std::fs::metadata(&exe_path);
if !meta.is_ok_and(|meta| meta.is_file()) {
return None;
}
let mut version: Option<String> = None;
#[cfg(windows)]
{
use exe::pe::VecPE;
use exe::VSVersionInfo;
let image = VecPE::from_disk_file(&exe_path).unwrap();
let vs_version_check = VSVersionInfo::parse(&image);
if let Some(string_file_info) = vs_version_check.unwrap().string_file_info {
let hashmap_result = string_file_info.children[0].string_map();
let hash_map = match hashmap_result {
Ok(r) => r,
Err(_) => HashMap::new(),
};
if let Some(result_version) = hash_map.get("ProductVersion") {
version = Some(result_version.clone());
if let Some(short) = result_version.clone().split("_").take(1).next() {
version = Some(short.to_string());
}
}
}
}
if version.is_none() {
let folder = base_path
.to_str()
.expect("Fail")
.split(consts::SLASH)
.last()
.unwrap();
version = Some(folder.to_string());
}
if version.is_none() {
return None;
}
Some(Self {
version: version.unwrap().clone(),
exe_path: exe_path.into_os_string().into_string().unwrap(),
base_path: String::from(path),
platforms: UnityEditor::get_platforms(&path),
templates: ProjectTemplate::find_templates(&path),
})
}
fn get_platforms(unity_folder: &str) -> Vec<String> {
let platform_names = HashMap::from([
("androidplayer", "Android"),
("windowsstandalonesupport", "Windows"),
("linuxstandalonesupport", "Linux"),
("LinuxStandalone", "Linux"),
("OSXStandalone", "OSX"),
("webglsupport", "WebGL"),
("metrosupport", "UWP"),
("iossupport", "iOS"),
]);
let mut platforms = Vec::new();
let base_path = Path::new(unity_folder).join("Data").join("PlaybackEngines");
if std::fs::metadata(&base_path).is_err() {
return platforms;
}
let dir = std::fs::read_dir(base_path);
if dir.is_err() {
return platforms;
}
for path in dir.unwrap() {
if path.is_err() {
continue;
}
let f = path.unwrap();
if let Ok(result_string) = f.file_name().into_string() {
if let Some(value) =
platform_names.get(&result_string.clone().to_lowercase().borrow())
{
platforms.push(value.to_string());
} else {
platforms.push(result_string);
}
}
}
platforms
}
}