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 name: build
on: on:
workflow_dispatch:
push: push:
branches: [main, master] branches: [ main, master ]
paths: paths:
- ".github/**" - 'egui_client/**'
- "rusty_hub_egui/**" - 'rusty_hub/**'
- "unity_hub_lib/**"
pull_request: pull_request:
branches: [main, master] branches: [ main, master ]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@ -18,42 +16,34 @@ jobs:
build-unix: build-unix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Hack sources.list - name: Install dep
run: sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu/|http://mirror.arizona.edu/ubuntu/|g' /etc/apt/sources.list run: sudo apt-get install -y mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libatk1.0-dev librust-gdk-sys-dev
- name: Update res - run: rustup toolchain install stable --profile minimal
run: sudo apt-get update - uses: Swatinem/rust-cache@v2
- name: Install dependencies with:
run: sudo apt-get install -y mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libatk1.0-dev librust-gdk-sys-dev # An explicit cache key that is used instead of the automatic `job`-based
- run: rustup toolchain install stable --profile minimal # cache key and is thus stable across jobs.
- uses: Swatinem/rust-cache@v2 # Default: empty
with: shared-key: ""
# An explicit cache key that is used instead of the automatic `job`-based # An additional cache key that is added alongside the automatic `job`-based
# cache key and is thus stable across jobs. # cache key and can be used to further differentiate jobs.
# Default: empty # Default: empty
shared-key: "" key: ""
# An additional cache key that is added alongside the automatic `job`-based # A whitespace separated list of env-var *prefixes* who's value contributes
# cache key and can be used to further differentiate jobs. # to the environment cache key.
# Default: empty # The env-vars are matched by *prefix*, so the default `RUST` var will
key: "" # match all of `RUSTC`, `RUSTUP_*`, `RUSTFLAGS`, `RUSTDOC_*`, etc.
# A whitespace separated list of env-var *prefixes* who's value contributes # Default: "CARGO CC CFLAGS CXX CMAKE RUST"
# to the environment cache key. env-vars: ""
# The env-vars are matched by *prefix*, so the default `RUST` var will # The cargo workspaces and target directory configuration.
# match all of `RUSTC`, `RUSTUP_*`, `RUSTFLAGS`, `RUSTDOC_*`, etc. # These entries are separated by newlines and have the form
# Default: "CARGO CC CFLAGS CXX CMAKE RUST" # `$workspace -> $target`. The `$target` part is treated as a directory
env-vars: "" # relative to the `$workspace` and defaults to "target" if not explicitly given.
# The cargo workspaces and target directory configuration. # Default: ". -> target"
# These entries are separated by newlines and have the form workspaces: ""
# `$workspace -> $target`. The `$target` part is treated as a directory # Determines if the cache should be saved even when the workflow has failed.
# relative to the `$workspace` and defaults to "target" if not explicitly given. # Default: "false"
# Default: ". -> target" cache-on-failure: ""
workspaces: "" - uses: actions/checkout@v2
# Determines if the cache should be saved even when the workflow has failed. - name: Build
# Default: "false" run: cd egui_client && cargo build --verbose
cache-on-failure: ""
- uses: actions/checkout@v4
- 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

View File

@ -10,12 +10,12 @@ jobs:
build: build:
runs-on: windows-2022 runs-on: windows-2022
steps: steps:
- run: rustup toolchain install stable --profile minimal - run: rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Build - name: Build
run: cd rusty_hub_egui && cargo build --release --verbose run: cd egui_client && cargo build --release --verbose
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
with: with:
name: build-exe 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
/*/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) # 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.
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) ![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 ## Building and using
[Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) is required in order to build it. [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 ```sh
git clone git@github.com:Leinnan/rusty_hub.git git clone git@github.com:Leinnan/rusty_hub.git
cd rusty_hub/rusty_hub_egui cd rusty_hub/egui_client
cargo build --release cargo build --release
cargo run --release cargo run --release
``` ```
## Thanks ## Thanks
Big thanks to https://github.com/unitycoder/UnityLauncherPro 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)] #[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 HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
pub const APP_NAME: &str = "Rusty Unity Hub"; pub const APP_NAME: &str = "Rusty Unity Hub";
pub const VERTICAL_SPACING: f32 = 8.0; 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 std::{path::PathBuf, process::Command};
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::{config::Configuration, unity_editor::UnityEditor, unity_project::UnityProject};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Hub { pub struct Hub {
pub config: Configuration, pub config: Configuration,
@ -16,28 +15,11 @@ impl Hub {
Self { config, projects } Self { config, projects }
} }
pub fn update_data(&mut self) { pub fn update_info(&mut self) {
self.config.rebuild(); self.config.rebuild();
self.update_projects_info(); for project in self.projects.iter_mut() {
}
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(); project.update_info();
}); }
self.projects.sort_by(|a, b| b.edit_time.cmp(&a.edit_time));
} }
pub fn run_project_nr(&self, nr: usize) { 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 { pub fn search_for_projects_at_path(&mut self, path: &PathBuf) -> usize {
let path_exists = std::fs::metadata(path).is_ok(); let path_exists = std::fs::metadata(path).is_ok();
let mut result = 0;
if !path_exists { if !path_exists {
return 0; return result;
} }
let projects = self.projects.clone(); for entry in WalkDir::new(path)
let new_projects: Vec<UnityProject> = WalkDir::new(path)
.max_depth(3) .max_depth(3)
.into_iter() .into_iter()
.parallel_filter(|entry| entry.is_ok()) .filter_entry(|_| true)
.parallel_map(|entry| { {
UnityProject::try_get_project_at_path( let projects = self.projects.clone();
&entry.unwrap().path().as_os_str().to_str().unwrap(), if entry.is_err() {
) continue;
}) }
.parallel_filter(|project| project.is_some())
.parallel_map(|project| project.unwrap())
.parallel_filter(move |p| !projects.contains(p))
.collect();
let len = new_projects.len(); let entry_unwraped = entry.unwrap();
self.projects.extend(new_projects); let path_string = entry_unwraped.path().as_os_str().to_str();
if let Some(project) = UnityProject::try_get_project_at_path(&path_string.unwrap()) {
len if !projects.contains(&project) {
self.projects.push(project);
result = result + 1;
}
}
}
result
} }
} }
impl Default for Hub { impl Default for Hub {

View File

@ -1,6 +1,7 @@
#[macro_use]
extern crate serde_derive;
pub mod config; pub mod config;
pub mod consts;
pub mod hub; pub mod hub;
pub mod project_template;
pub mod unity_editor; pub mod unity_editor;
pub mod unity_project; 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::{path::Path, str};
use std::{ops::Sub, path::Path, str};
use crate::consts; #[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Hash, Eq)]
pub struct UnityProject { pub struct UnityProject {
pub path: String, pub path: String,
pub title: String, pub title: String,
pub version: String, pub version: String,
pub branch: String, pub branch: String,
pub is_valid: bool, pub is_valid: bool,
pub edit_time: std::time::SystemTime,
} }
impl PartialEq for UnityProject { impl PartialEq for UnityProject {
@ -35,6 +31,7 @@ impl UnityProject {
Security::Read, Security::Read,
) )
.unwrap(); .unwrap();
println!("{}", key.to_string());
for value in key.values() { for value in key.values() {
if value.is_err() { if value.is_err() {
@ -51,6 +48,7 @@ impl UnityProject {
if let Some(result) = UnityProject::try_get_project_at_path(&project_path) { if let Some(result) = UnityProject::try_get_project_at_path(&project_path) {
projects.push(result); projects.push(result);
} }
println!("\t{}: {}", unwraped_name, project_path);
} }
} }
projects projects
@ -63,48 +61,33 @@ impl UnityProject {
std::fs::metadata(&one).is_ok() && std::fs::metadata(&two).is_ok() 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( let project_version_file = std::fs::read_to_string(
Path::new(&path) Path::new(&path)
.join("ProjectSettings") .join("ProjectSettings")
.join("ProjectVersion.txt"), .join("ProjectVersion.txt"),
); );
if project_version_file.is_err() { let project_version_file = project_version_file.unwrap();
return None; let mut iter = project_version_file.split_whitespace();
}
let binding = project_version_file.unwrap();
let mut iter = binding.split_whitespace();
iter.next(); iter.next();
let project_version = iter.next().unwrap().to_string(); let project_version = iter.next().unwrap().to_string();
Some(project_version) Some(UnityProject {
} path: path.to_string(),
title: path.split("\\").last().unwrap().to_string(),
pub fn try_get_project_at_path(path: &str) -> Option<UnityProject> { version: project_version,
#[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(), branch: String::new(),
version: String::new(),
is_valid: true, 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) { pub fn update_info(&mut self) {
const HEAD_PREFIX: &str = "ref: refs/heads/";
let is_project = UnityProject::is_project_at_path(&self.path); let is_project = UnityProject::is_project_at_path(&self.path);
self.is_valid = is_project; self.is_valid = is_project;
@ -113,43 +96,18 @@ impl UnityProject {
} }
let mut base_path = Path::new(&self.path); 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) { while let Some(path) = base_path.parent() {
None => { base_path = path;
while let Some(path) = base_path.parent() { let head_path = Path::new(&path).join(".git").join("HEAD");
base_path = path; if !head_path.exists() {
let new_branch = self.try_read_from_path(base_path); continue;
if new_branch.is_some() {
self.branch = new_branch.unwrap();
break;
}
}
} }
Some(value) => { let head_content =
self.branch = value; 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();
} }
} }
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;
}
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
}
} }
} }

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