Compare commits
33 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
aae3c5fe70 | |
|
|
946b0191cd | |
|
|
8b611fe33e | |
|
|
13c99a8cd5 | |
|
|
5db08cb0d4 | |
|
|
4a66dccba3 | |
|
|
25881e033f | |
|
|
b9690bdf29 | |
|
|
80923b610b | |
|
|
dba911d2ae | |
|
|
815508b9e9 | |
|
|
5a0f12c57e | |
|
|
b0bc59200b | |
|
|
664926cb6d | |
|
|
e98924eb0c | |
|
|
089c657bac | |
|
|
870400dce6 | |
|
|
a52669eead | |
|
|
ee630ed600 | |
|
|
7dbd96a961 | |
|
|
aa1030f143 | |
|
|
d5b91a50c9 | |
|
|
1821a17eb6 | |
|
|
4ea1a272bc | |
|
|
188d4e7168 | |
|
|
d108f7a644 | |
|
|
4c3916e2d1 | |
|
|
365c724252 | |
|
|
eb79e560dc | |
|
|
f20d3916fe | |
|
|
c913f49e31 | |
|
|
029b23d470 | |
|
|
d9365975c6 |
|
|
@ -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,7 +18,11 @@ jobs:
|
||||||
build-unix:
|
build-unix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install dep
|
- name: Hack sources.list
|
||||||
|
run: sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu/|http://mirror.arizona.edu/ubuntu/|g' /etc/apt/sources.list
|
||||||
|
- name: Update res
|
||||||
|
run: sudo apt-get update
|
||||||
|
- name: Install dependencies
|
||||||
run: sudo apt-get install -y mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libatk1.0-dev librust-gdk-sys-dev
|
run: sudo apt-get install -y mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libatk1.0-dev librust-gdk-sys-dev
|
||||||
- run: rustup toolchain install stable --profile minimal
|
- run: rustup toolchain install stable --profile minimal
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
@ -44,6 +50,10 @@ jobs:
|
||||||
# Determines if the cache should be saved even when the workflow has failed.
|
# Determines if the cache should be saved even when the workflow has failed.
|
||||||
# Default: "false"
|
# Default: "false"
|
||||||
cache-on-failure: ""
|
cache-on-failure: ""
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cd egui_client && cargo build --verbose
|
run: cd rusty_hub_egui && cargo build --verbose
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: upload executable
|
||||||
|
path: rusty_hub_egui/target/release/rusty_hub_egui
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ jobs:
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
/target
|
/target
|
||||||
/*/target
|
/*/target
|
||||||
|
unity_hub_lib/Cargo.lock
|
||||||
|
Cargo.lock
|
||||||
|
/*/.idea
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# [0.2.0]
|
||||||
|
- Update egui to 0.21 version
|
||||||
|
- Initial macOS support
|
||||||
|
- Light theme
|
||||||
|
|
||||||
|
# [0.1.0]
|
||||||
|
|
||||||
|
- Initial version
|
||||||
|
|
@ -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
|
||||||
13
README.md
13
README.md
|
|
@ -1,24 +1,29 @@
|
||||||
# Rusty Hub [](https://github.com/Leinnan/rusty_hub/actions/workflows/rust.yml)
|
# Rusty Hub [](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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -9,5 +9,4 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn main() {
|
fn main() {}
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -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"
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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();
|
||||||
project.update_info();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_projects_info(&mut self) {
|
||||||
|
let mut registry = UnityProject::get_projects_from_registry()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| !self.projects.contains(p))
|
||||||
|
.collect();
|
||||||
|
self.projects.append(&mut registry);
|
||||||
|
self.projects = self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect::<HashSet<UnityProject>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
self.projects.iter_mut().for_each(|project| {
|
||||||
|
project.update_info();
|
||||||
|
});
|
||||||
|
self.projects.sort_by(|a, b| b.edit_time.cmp(&a.edit_time));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_project_nr(&self, nr: usize) {
|
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 {
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
match self.try_read_from_path(base_path) {
|
||||||
|
None => {
|
||||||
while let Some(path) = base_path.parent() {
|
while let Some(path) = base_path.parent() {
|
||||||
base_path = path;
|
base_path = path;
|
||||||
|
let new_branch = self.try_read_from_path(base_path);
|
||||||
|
if new_branch.is_some() {
|
||||||
|
self.branch = new_branch.unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(value) => {
|
||||||
|
self.branch = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(meta) = std::fs::metadata(&self.path) {
|
||||||
|
if let Ok(data) = meta.modified() {
|
||||||
|
self.edit_time = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_from_path(&self, path: &std::path::Path) -> Option<String> {
|
||||||
|
const HEAD_PREFIX: &str = "ref: refs/heads/";
|
||||||
|
|
||||||
let head_path = Path::new(&path).join(".git").join("HEAD");
|
let head_path = Path::new(&path).join(".git").join("HEAD");
|
||||||
if !head_path.exists() {
|
if !head_path.exists() {
|
||||||
continue;
|
return None;
|
||||||
}
|
}
|
||||||
let head_content =
|
let head_content = std::fs::read_to_string(&head_path).expect("Could not read HEAD file");
|
||||||
std::fs::read_to_string(&head_path).expect("Could not read HEAD file");
|
|
||||||
if head_content.contains(HEAD_PREFIX) {
|
if head_content.contains(HEAD_PREFIX) {
|
||||||
self.branch = head_content.replace(HEAD_PREFIX, "").trim().to_string();
|
Some(head_content.replace(HEAD_PREFIX, "").trim().to_string())
|
||||||
}
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue