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

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
/target /target
/*/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,28 +1,33 @@
# 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)
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) ![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 `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 ```sh
git clone git@github.com:Leinnan/rusty_hub.git git clone git@github.com:Leinnan/rusty_hub.git
cd rusty_hub/egui_client cd rusty_hub/rusty_hub_egui
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
Most of the required information about how data is stored by Unity HUB is taken from there. Most of the required information about how data is stored by Unity HUB is taken from there.
Thanks for the icon to the [Papirus Development Team](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/) Thanks for the icon to the [Papirus Development Team](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/)

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)] #[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 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,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 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Hub { pub struct Hub {
@ -15,11 +16,28 @@ impl Hub {
Self { config, projects } Self { config, projects }
} }
pub fn update_info(&mut self) { pub fn update_data(&mut self) {
self.config.rebuild(); self.config.rebuild();
for project in self.projects.iter_mut() { 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(); 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) {
@ -52,30 +70,28 @@ 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 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) .max_depth(3)
.into_iter() .into_iter()
.filter_entry(|_| true) .parallel_filter(|entry| entry.is_ok())
{ .parallel_map(|entry| {
let projects = self.projects.clone(); UnityProject::try_get_project_at_path(
if entry.is_err() { &entry.unwrap().path().as_os_str().to_str().unwrap(),
continue; )
} })
.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 len = new_projects.len();
let path_string = entry_unwraped.path().as_os_str().to_str(); self.projects.extend(new_projects);
if let Some(project) = UnityProject::try_get_project_at_path(&path_string.unwrap()) {
if !projects.contains(&project) { len
self.projects.push(project);
result = result + 1;
}
}
}
result
} }
} }
impl Default for Hub { impl Default for Hub {

View File

@ -1,7 +1,6 @@
#[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,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 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 {
@ -31,7 +35,6 @@ 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() {
@ -48,7 +51,6 @@ 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
@ -61,33 +63,48 @@ 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 try_get_project_at_path(path: &str) -> Option<UnityProject> { pub fn get_version_at_path(path: &str) -> Option<String> {
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"),
); );
let project_version_file = project_version_file.unwrap(); if project_version_file.is_err() {
let mut iter = project_version_file.split_whitespace(); return None;
}
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(UnityProject { Some(project_version)
path: path.to_string(), }
title: path.split("\\").last().unwrap().to_string(),
version: 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(), 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;
@ -96,18 +113,43 @@ 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();
while let Some(path) = base_path.parent() { match self.try_read_from_path(base_path) {
base_path = path; None => {
let head_path = Path::new(&path).join(".git").join("HEAD"); while let Some(path) = base_path.parent() {
if !head_path.exists() { base_path = path;
continue; let new_branch = self.try_read_from_path(base_path);
if new_branch.is_some() {
self.branch = new_branch.unwrap();
break;
}
}
} }
let head_content = Some(value) => {
std::fs::read_to_string(&head_path).expect("Could not read HEAD file"); self.branch = value;
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
}
}
} }