initial commit
This commit is contained in:
commit
9d64be5148
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/target
|
||||||
|
/um-react.zip
|
||||||
|
/um-react.exe
|
||||||
|
/um-react@*.zip
|
||||||
|
/um-react@*.exe
|
3709
Cargo.lock
generated
Normal file
3709
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
Normal file
29
Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "um-react-wry"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wry = "0.35.1"
|
||||||
|
tao = { version = "0.24", default-features = false, features = ["rwh_05"] }
|
||||||
|
zip = "0.6.6"
|
||||||
|
http = "0.2"
|
||||||
|
bytes = "1.5.0"
|
||||||
|
clap = { version = "4.4.12", features = ["derive"] }
|
||||||
|
image = { version = "0.24", default-features = true, features = ["webp"] }
|
||||||
|
dirs = "5.0.1"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "stub"
|
||||||
|
path = "src/stub.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "builder"
|
||||||
|
path = "src/builder.rs"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
debug = false
|
||||||
|
lto = true
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Unlock-Music Team<https://git.unlock-music.dev/um/>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
15
README.MD
Normal file
15
README.MD
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# um-react-wry
|
||||||
|
|
||||||
|
一个利用 [wry](https://github.com/tauri-apps/wry) 框架库制作的 um-react 单文件打包工具。
|
||||||
|
|
||||||
|
## 构建单文件
|
||||||
|
|
||||||
|
1. 确保 rust 已经安装。[参考说明](https://www.rust-lang.org/tools/install)。
|
||||||
|
2. [下载 `um-react.zip`](https://git.unlock-music.dev/um/um-react/releases/latest) 并放入当前目录。
|
||||||
|
3. 执行 `cargo build --bin stub --release` 构建模板可执行文件。
|
||||||
|
4. 执行 `cargo run --bin builder` 构建最终的单文件 `um-react.exe`。
|
||||||
|
|
||||||
|
## 更新资源文件 `um-react.zip`
|
||||||
|
|
||||||
|
1. [下载 `um-react.zip`](https://git.unlock-music.dev/um/um-react/releases/latest) 并放入当前目录。
|
||||||
|
2. 执行 `cargo run --bin builder` 生产更新的 `um-react.exe`。
|
52
src/builder.rs
Normal file
52
src/builder.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// Search for a pattern in a file and display the lines that contain it.
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct BuildArgs {
|
||||||
|
/// The path to um-react.zip.
|
||||||
|
#[arg(short, long, default_value = "um-react.zip")]
|
||||||
|
resource: std::path::PathBuf,
|
||||||
|
|
||||||
|
/// The path to stub.
|
||||||
|
#[arg(short = 't', long, default_value = "target/release/stub.exe")]
|
||||||
|
stub: std::path::PathBuf,
|
||||||
|
|
||||||
|
// The path to final executable
|
||||||
|
#[arg(short, long, default_value = "um-react.exe")]
|
||||||
|
output: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = BuildArgs::parse();
|
||||||
|
|
||||||
|
let mut file_output = File::create(args.output).unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut vec_stub = vec![0u8; 0];
|
||||||
|
File::open(args.stub)
|
||||||
|
.unwrap()
|
||||||
|
.read_to_end(&mut vec_stub)
|
||||||
|
.unwrap();
|
||||||
|
file_output.write_all(&vec_stub).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut vec_res = vec![0u8; 0];
|
||||||
|
File::open(args.resource)
|
||||||
|
.unwrap()
|
||||||
|
.read_to_end(&mut vec_res)
|
||||||
|
.unwrap();
|
||||||
|
file_output.write_all(&mut vec_res).unwrap();
|
||||||
|
|
||||||
|
let tail_payload_len = (vec_res.len() as u32).to_le_bytes();
|
||||||
|
file_output.write_all(&tail_payload_len).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("done!");
|
||||||
|
}
|
179
src/stub.rs
Normal file
179
src/stub.rs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Read};
|
||||||
|
use std::io::{Cursor, Seek};
|
||||||
|
use std::path::Path;
|
||||||
|
use tao::platform::windows::WindowBuilderExtWindows;
|
||||||
|
use tao::window::Icon;
|
||||||
|
use tao::{
|
||||||
|
event::{Event, WindowEvent},
|
||||||
|
event_loop::{ControlFlow, EventLoop},
|
||||||
|
window::WindowBuilder,
|
||||||
|
};
|
||||||
|
use wry::WebContext;
|
||||||
|
use wry::{http::header::CONTENT_TYPE, WebViewBuilder};
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
static APP_UUID: &str = "39abaebb-57b1-4c12-ab88-321fd7a93354";
|
||||||
|
|
||||||
|
fn parse_zip<R: Read + Seek>(reader: R) -> HashMap<String, Vec<u8>> {
|
||||||
|
let mut archive = ZipArchive::new(reader).expect("Failed to open ZIP archive");
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive
|
||||||
|
.by_index(i)
|
||||||
|
.expect("Failed to get file from ZIP archive");
|
||||||
|
let mut content = Vec::new();
|
||||||
|
file.read_to_end(&mut content)
|
||||||
|
.expect("Failed to read file content");
|
||||||
|
result.insert(file.name().to_string(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_external_zip(path: &Path) -> std::io::Result<HashMap<String, Vec<u8>>> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
Ok(parse_zip(buf_reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tail_zip(path: &Path) -> std::io::Result<HashMap<String, Vec<u8>>> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
file.seek(std::io::SeekFrom::End(-4))?;
|
||||||
|
let mut buffer = [0u8; 4];
|
||||||
|
file.read_exact(&mut buffer)?;
|
||||||
|
let zip_len: i64 = u32::from_le_bytes(buffer).into();
|
||||||
|
file.seek(std::io::SeekFrom::End(-4 - zip_len))?;
|
||||||
|
let mut zip_buffer = vec![0u8; 0];
|
||||||
|
file.take(zip_len as u64).read_to_end(&mut zip_buffer)?;
|
||||||
|
Ok(parse_zip(Cursor::new(zip_buffer)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon(bytes: &[u8]) -> Icon {
|
||||||
|
let (icon_rgba, icon_width, icon_height) = {
|
||||||
|
let image = image::load_from_memory(bytes).unwrap();
|
||||||
|
let image = image.into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
(rgba, width, height)
|
||||||
|
};
|
||||||
|
Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> wry::Result<()> {
|
||||||
|
let tmp_data_dir = dirs::cache_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join(format!("um-react!{}", APP_UUID));
|
||||||
|
let exe_path = std::env::current_exe().unwrap();
|
||||||
|
let um_react_external = exe_path.parent().unwrap().join("um-react.zip");
|
||||||
|
let zip_content = if um_react_external.exists() {
|
||||||
|
// debug/prod: override by reading from external zip archive
|
||||||
|
parse_external_zip(&um_react_external).unwrap()
|
||||||
|
} else {
|
||||||
|
parse_tail_zip(&exe_path).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_suffix = if let Some(version_txt) = zip_content.get("version.txt") {
|
||||||
|
format!("v{}", String::from_utf8_lossy(version_txt).trim())
|
||||||
|
} else {
|
||||||
|
"未知版本".into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let event_loop = EventLoop::new();
|
||||||
|
let window = WindowBuilder::new()
|
||||||
|
.with_title(format!("um-react-wry 桌面客户端 - {}", version_suffix))
|
||||||
|
.with_maximized(true)
|
||||||
|
.with_window_icon(Some(load_icon(include_bytes!("um-react@16.webp"))))
|
||||||
|
.with_taskbar_icon(Some(load_icon(include_bytes!("um-react@192.webp"))))
|
||||||
|
.build(&event_loop)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "ios",
|
||||||
|
target_os = "android"
|
||||||
|
))]
|
||||||
|
let builder = WebViewBuilder::new(&window);
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "ios",
|
||||||
|
target_os = "android"
|
||||||
|
)))]
|
||||||
|
let builder = {
|
||||||
|
use tao::platform::unix::WindowExtUnix;
|
||||||
|
use wry::WebViewBuilderExtUnix;
|
||||||
|
let vbox = window.default_vbox().unwrap();
|
||||||
|
WebViewBuilder::new_gtk(vbox)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut web_ctx = WebContext::new(Some(tmp_data_dir));
|
||||||
|
let _webview = builder
|
||||||
|
.with_web_context(&mut web_ctx)
|
||||||
|
.with_url("umr://app/index.html")?
|
||||||
|
.with_custom_protocol("umr".into(), move |request| {
|
||||||
|
match get_umr_resource(request, &zip_content) {
|
||||||
|
Ok(r) => r.map(Into::into),
|
||||||
|
Err(e) => http::Response::builder()
|
||||||
|
.header(CONTENT_TYPE, "text/plain")
|
||||||
|
.status(500)
|
||||||
|
.body(e.to_string().as_bytes().to_vec())
|
||||||
|
.unwrap()
|
||||||
|
.map(Into::into),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
event_loop.run(move |event, _, control_flow| {
|
||||||
|
*control_flow = ControlFlow::Wait;
|
||||||
|
|
||||||
|
if let Event::WindowEvent {
|
||||||
|
event: WindowEvent::CloseRequested,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
*control_flow = ControlFlow::Exit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_umr_resource(
|
||||||
|
request: wry::http::Request<Vec<u8>>,
|
||||||
|
zip_content: &HashMap<String, Vec<u8>>,
|
||||||
|
) -> Result<http::Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||||
|
let path = request.uri().path();
|
||||||
|
let path = if path == "/" {
|
||||||
|
"index.html"
|
||||||
|
} else {
|
||||||
|
&path[1..]
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("GET {}", path);
|
||||||
|
let file_body = zip_content
|
||||||
|
.get(path)
|
||||||
|
.or_else(|| zip_content.get("index.html"))
|
||||||
|
.ok_or("file not found in zip")?;
|
||||||
|
|
||||||
|
let mimetype = if path.ends_with(".html") || path.ends_with("/") {
|
||||||
|
"text/html"
|
||||||
|
} else if path.ends_with(".js") {
|
||||||
|
"application/javascript"
|
||||||
|
} else if path.ends_with(".wasm") {
|
||||||
|
"application/wasm"
|
||||||
|
} else if path.ends_with(".ico") {
|
||||||
|
"image/vnd.microsoft.icon"
|
||||||
|
} else {
|
||||||
|
"application/octet-stream"
|
||||||
|
};
|
||||||
|
|
||||||
|
http::Response::builder()
|
||||||
|
.header(CONTENT_TYPE, mimetype)
|
||||||
|
.body(file_body.clone())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
BIN
src/um-react@16.webp
Normal file
BIN
src/um-react@16.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 316 B |
BIN
src/um-react@192.webp
Normal file
BIN
src/um-react@192.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Loading…
Reference in New Issue
Block a user