mirror of
https://github.com/G2-Games/lbee-utils.git
synced 2025-04-19 07:12:55 -05:00
211 lines
8 KiB
Rust
211 lines
8 KiB
Rust
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
|
|
|
use std::fs;
|
|
use colog;
|
|
use eframe::egui::{self, ColorImage, Image, TextureFilter, TextureHandle, TextureOptions};
|
|
use log::error;
|
|
use luca_pak::{entry::EntryType, Pak};
|
|
|
|
fn main() -> eframe::Result {
|
|
colog::default_builder()
|
|
.filter(None, log::LevelFilter::Warn)
|
|
.init();
|
|
|
|
let options = eframe::NativeOptions {
|
|
viewport: egui::ViewportBuilder::default().with_inner_size([1024.0, 800.0]),
|
|
follow_system_theme: true,
|
|
..Default::default()
|
|
};
|
|
|
|
let mut fonts = egui::FontDefinitions::default();
|
|
fonts.font_data.insert(
|
|
"Noto Sans".to_owned(),
|
|
egui::FontData::from_static(include_bytes!("/home/g2/Downloads/Noto_Sans/static/NotoSans-Regular.ttf")),
|
|
);
|
|
fonts.font_data.insert(
|
|
"Noto Sans Japanese".to_owned(),
|
|
egui::FontData::from_static(include_bytes!("/home/g2/Downloads/Noto_Sans_JP/static/NotoSansJP-Regular.ttf")),
|
|
);
|
|
fonts
|
|
.families
|
|
.entry(egui::FontFamily::Proportional)
|
|
.or_default()
|
|
.insert(0, "Noto Sans".to_owned());
|
|
fonts
|
|
.families
|
|
.entry(egui::FontFamily::Proportional)
|
|
.or_default()
|
|
.insert(1, "Noto Sans Japanese".to_owned());
|
|
|
|
eframe::run_native(
|
|
"LUCA PAK Explorer",
|
|
options,
|
|
Box::new(|cc| {
|
|
cc.egui_ctx.set_fonts(fonts);
|
|
|
|
Ok(Box::<PakExplorer>::default())
|
|
}),
|
|
)
|
|
}
|
|
|
|
struct PakExplorer {
|
|
open_file: Option<Pak>,
|
|
selected_entry: Option<luca_pak::entry::Entry>,
|
|
image_texture: Option<egui::TextureHandle>,
|
|
hex_string: Option<Vec<String>>,
|
|
}
|
|
|
|
impl Default for PakExplorer {
|
|
fn default() -> Self {
|
|
Self {
|
|
open_file: None,
|
|
selected_entry: None,
|
|
image_texture: None,
|
|
hex_string: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl eframe::App for PakExplorer {
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
ctx.set_pixels_per_point(1.5);
|
|
ui.heading("PAK File Explorer");
|
|
|
|
ui.horizontal(|ui| {
|
|
if ui.button("Open file").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new().pick_file() {
|
|
let pak = match Pak::open(&path) {
|
|
Ok(pak) => Some(pak),
|
|
Err(e) => {
|
|
error!("Unable to read selected file as PAK: {}", e);
|
|
None
|
|
},
|
|
};
|
|
self.open_file = pak;
|
|
self.selected_entry = None;
|
|
self.image_texture = None;
|
|
self.hex_string = None;
|
|
}
|
|
}
|
|
if let Some(pak) = &self.open_file {
|
|
if ui.button("Save PAK").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new()
|
|
.set_file_name(pak.path().file_name().unwrap().to_string_lossy())
|
|
.save_file()
|
|
{
|
|
pak.save(&path).unwrap();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.separator();
|
|
|
|
if let Some(pak) = &self.open_file {
|
|
ui.label(format!("Opened {}", pak.path().file_name().unwrap().to_string_lossy()));
|
|
ui.label(format!("Contains {} Entries", pak.entries().len()));
|
|
|
|
let selection = if let Some(entry) = &self.selected_entry {
|
|
entry.display_name()
|
|
} else {
|
|
"None".to_string()
|
|
};
|
|
|
|
egui::ComboBox::from_id_source("my-combobox")
|
|
.selected_text(selection)
|
|
.truncate()
|
|
.show_ui(ui, |ui|
|
|
{
|
|
ui.selectable_value(&mut self.selected_entry, None, "");
|
|
for entry in pak.entries() {
|
|
if ui.selectable_value(
|
|
&mut self.selected_entry,
|
|
Some(entry.clone()),
|
|
entry.display_name(),
|
|
).clicked() {
|
|
self.image_texture = None;
|
|
};
|
|
}
|
|
});
|
|
} else {
|
|
ui.centered_and_justified(|ui|
|
|
ui.label("No File Opened")
|
|
);
|
|
}
|
|
|
|
if let Some(entry) = &self.selected_entry {
|
|
ui.horizontal(|ui| {
|
|
if ui.button("Save entry").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new()
|
|
.set_file_name(entry.display_name())
|
|
.save_file()
|
|
{
|
|
entry.save(&path).unwrap();
|
|
}
|
|
}
|
|
|
|
if let Some(pak) = &mut self.open_file.as_mut() {
|
|
if ui.button("Replace entry").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new().pick_file() {
|
|
let file_bytes = fs::read(path).unwrap();
|
|
pak.replace(entry.index(), &file_bytes).unwrap();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
match entry.file_type() {
|
|
EntryType::CZ0 | EntryType::CZ1
|
|
| EntryType::CZ2 | EntryType::CZ3
|
|
| EntryType::CZ4 | EntryType::CZ5 =>
|
|
{
|
|
if ui.button("Save as PNG").clicked() {
|
|
let mut display_name = entry.display_name();
|
|
display_name.push_str(".png");
|
|
if let Some(path) = rfd::FileDialog::new()
|
|
.set_file_name(display_name)
|
|
.save_file()
|
|
{
|
|
let cz = cz::DynamicCz::decode(&mut std::io::Cursor::new(entry.as_bytes())).unwrap();
|
|
cz.save_as_png(&path).unwrap();
|
|
}
|
|
}
|
|
|
|
ui.separator();
|
|
|
|
let texture: &TextureHandle = self.image_texture.get_or_insert_with(|| {
|
|
let cz = cz::DynamicCz::decode(&mut std::io::Cursor::new(entry.as_bytes())).unwrap();
|
|
let image = ColorImage::from_rgba_unmultiplied(
|
|
[cz.header().width() as usize, cz.header().height() as usize],
|
|
cz.as_raw()
|
|
);
|
|
ui.ctx().load_texture("eventframe", image, TextureOptions {
|
|
magnification: TextureFilter::Nearest,
|
|
minification: TextureFilter::Linear,
|
|
..Default::default()
|
|
})
|
|
});
|
|
|
|
ui.centered_and_justified(|ui|
|
|
ui.add(
|
|
Image::from_texture(texture)
|
|
.show_loading_spinner(true)
|
|
.shrink_to_fit()
|
|
.rounding(2.0)
|
|
)
|
|
);
|
|
}
|
|
_ => {
|
|
ui.centered_and_justified(|ui|
|
|
ui.label("No Preview Available")
|
|
);
|
|
},
|
|
}
|
|
} else if self.open_file.is_some() {
|
|
ui.centered_and_justified(|ui|
|
|
ui.label("Select an Entry")
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|