Compare commits

...

33 Commits

Author SHA1 Message Date
Piotr Siuszko aae3c5fe70 update deps 2025-08-05 20:42:45 +02:00
Piotr Siuszko 946b0191cd Create workspace and upgrade dependencies 2025-08-05 20:38:54 +02:00
Piotr Siuszko 8b611fe33e rm locks 2025-08-05 20:21:10 +02:00
Piotr Siuszko 13c99a8cd5 Update deps 2024-10-22 16:18:38 +02:00
Piotr Siuszko 5db08cb0d4 Deps update 2024-04-21 19:37:43 +02:00
Piotr Siuszko 4a66dccba3 Update deps 2024-03-05 11:00:16 +01:00
Piotr Siuszko 25881e033f UX improvements 2023-08-28 18:42:23 +02:00
Piotr Siuszko b9690bdf29 Fix paths for unix 2023-03-07 20:01:56 +01:00
Piotr Siuszko 80923b610b Rework project display 2023-03-07 19:49:45 +01:00
Piotr Siuszko dba911d2ae fix looking for git dir 2023-03-07 19:49:17 +01:00
Piotr Siuszko 815508b9e9 Cleanup install paths layout 2023-03-07 18:27:38 +01:00
Piotr Siuszko 5a0f12c57e Cleanup header and side panel layout 2023-03-07 18:01:30 +01:00
Piotr Siuszko b0bc59200b Custom fonts 2023-03-07 16:46:58 +01:00
Piotr Siuszko 664926cb6d Remove duplicated unity project entries 2023-03-02 17:18:26 +01:00
Piotr Siuszko e98924eb0c Update editors on start 2023-03-02 17:17:48 +01:00
Piotr Siuszko 089c657bac Draw central panel last, fix theme switch button 2023-02-16 00:27:26 +01:00
Piotr Siuszko 870400dce6 Update apt-get 2023-02-16 00:10:52 +01:00
Piotr Siuszko a52669eead Enable manual running job 2023-02-16 00:09:37 +01:00
Piotr Siuszko ee630ed600
Update README.md 2023-02-16 00:08:19 +01:00
Piotr Siuszko 7dbd96a961 Hack sources.list 2023-02-16 00:06:38 +01:00
Piotr Siuszko aa1030f143 Update job 2023-02-16 00:03:10 +01:00
Piotr Siuszko d5b91a50c9 Update deps 2023-02-15 23:56:17 +01:00
Piotr Siuszko 1821a17eb6 Update project Unity version on start 2023-02-15 23:56:17 +01:00
Piotr Siuszko 4ea1a272bc update paths 2023-02-15 23:56:16 +01:00
Piotr Siuszko 188d4e7168
Update README.md 2022-10-09 13:17:57 +02:00
Piotr Siuszko d108f7a644 Add ToDo 2022-09-26 21:57:56 +02:00
Piotr Siuszko 4c3916e2d1 Cleanup, project template class 2022-09-26 21:47:25 +02:00
Piotr Siuszko 365c724252 fix windows formatting 2022-09-26 18:32:25 +02:00
Piotr Siuszko eb79e560dc Fix macOS 2022-09-26 18:30:26 +02:00
Piotr Siuszko f20d3916fe Merge branch 'master' of https://github.com/Leinnan/rusty_hub 2022-09-26 17:50:19 +02:00
Piotr Siuszko c913f49e31 Init work on macos support 2022-09-26 17:50:15 +02:00
Piotr Siuszko 029b23d470
Update README.md 2022-09-26 09:02:48 +02:00
Piotr Siuszko d9365975c6
Update README.md 2022-09-26 08:39:46 +02:00
36 changed files with 985 additions and 3713 deletions

View File

@ -1,11 +1,13 @@
name: build
on:
workflow_dispatch:
push:
branches: [main, master]
paths:
- 'egui_client/**'
- 'rusty_hub/**'
- ".github/**"
- "rusty_hub_egui/**"
- "unity_hub_lib/**"
pull_request:
branches: [main, master]
@ -16,7 +18,11 @@ jobs:
build-unix:
runs-on: ubuntu-latest
steps:
- name: Install dep
- 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
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
@ -44,6 +50,10 @@ jobs:
# Determines if the cache should be saved even when the workflow has failed.
# Default: "false"
cache-on-failure: ""
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Build
run: cd egui_client && cargo build --verbose
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

View File

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

3
.gitignore vendored
View File

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

8
CHANGELOG.md Normal file
View File

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

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[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,24 +1,29 @@
# 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)
Very simple alternative for Unity Hub. Rust pet project.
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.
![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 `egui_client` subdirectory in `CLI` client and run these commands:
Building is pretty simple, just copy repo, open `rusty_hub_egui` subdirectory in `CLI` client and run these commands:
```sh
git clone git@github.com:Leinnan/rusty_hub.git
cd rusty_hub/egui_client
cd rusty_hub/rusty_hub_egui
cargo build --release
cargo run --release
```
## Thanks
Big thanks to https://github.com/unitycoder/UnityLauncherPro

2404
egui_client/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
[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

@ -1,448 +0,0 @@
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,
);
});
});
}
}

View File

@ -1,38 +0,0 @@
#![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))),
);
}

491
rusty_hub/Cargo.lock generated
View File

@ -1,491 +0,0 @@
# 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"

View File

@ -1,21 +0,0 @@
[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"

View File

@ -1,77 +0,0 @@
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,99 +0,0 @@
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
}
}

28
rusty_hub_egui/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[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

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

View File

View File

@ -2,3 +2,6 @@ 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,449 @@
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

@ -0,0 +1,30 @@
#![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)))
}),
)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

11
unity_hub_lib/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[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

@ -0,0 +1,79 @@
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

@ -0,0 +1,24 @@
#[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,8 +1,9 @@
use std::{path::PathBuf, process::Command};
use walkdir::WalkDir;
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;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Hub {
@ -15,11 +16,28 @@ impl Hub {
Self { config, projects }
}
pub fn update_info(&mut self) {
pub fn update_data(&mut self) {
self.config.rebuild();
for project in self.projects.iter_mut() {
project.update_info();
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| {
project.update_info();
});
self.projects.sort_by(|a, b| b.edit_time.cmp(&a.edit_time));
}
pub fn run_project_nr(&self, nr: usize) {
@ -52,30 +70,28 @@ 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 result;
return 0;
}
for entry in WalkDir::new(path)
let projects = self.projects.clone();
let new_projects: Vec<UnityProject> = WalkDir::new(path)
.max_depth(3)
.into_iter()
.filter_entry(|_| true)
{
let projects = self.projects.clone();
if entry.is_err() {
continue;
}
.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();
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
let len = new_projects.len();
self.projects.extend(new_projects);
len
}
}
impl Default for Hub {

View File

@ -1,7 +1,6 @@
#[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,36 @@
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

@ -0,0 +1,122 @@
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
}
}

View File

@ -1,12 +1,16 @@
use std::{path::Path, str};
use serde::{Deserialize, Serialize};
use std::{ops::Sub, path::Path, str};
#[derive(Debug, Serialize, Deserialize, Clone)]
use crate::consts;
#[derive(Debug, Serialize, Deserialize, Clone, Hash, Eq)]
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 {
@ -31,7 +35,6 @@ impl UnityProject {
Security::Read,
)
.unwrap();
println!("{}", key.to_string());
for value in key.values() {
if value.is_err() {
@ -48,7 +51,6 @@ impl UnityProject {
if let Some(result) = UnityProject::try_get_project_at_path(&project_path) {
projects.push(result);
}
println!("\t{}: {}", unwraped_name, project_path);
}
}
projects
@ -61,33 +63,48 @@ impl UnityProject {
std::fs::metadata(&one).is_ok() && std::fs::metadata(&two).is_ok()
}
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;
}
pub fn get_version_at_path(path: &str) -> Option<String> {
let project_version_file = std::fs::read_to_string(
Path::new(&path)
.join("ProjectSettings")
.join("ProjectVersion.txt"),
);
let project_version_file = project_version_file.unwrap();
let mut iter = project_version_file.split_whitespace();
if project_version_file.is_err() {
return None;
}
let binding = project_version_file.unwrap();
let mut iter = binding.split_whitespace();
iter.next();
let project_version = iter.next().unwrap().to_string();
Some(UnityProject {
path: path.to_string(),
title: path.split("\\").last().unwrap().to_string(),
version: project_version,
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(),
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;
@ -96,18 +113,43 @@ 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() {
continue;
return None;
}
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) {
self.branch = head_content.replace(HEAD_PREFIX, "").trim().to_string();
}
Some(head_content.replace(HEAD_PREFIX, "").trim().to_string())
} else {
None
}
}
}