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