initial commit

This commit is contained in:
Jixun Wu 2023-12-28 20:43:00 +00:00
commit 964eae3e0b
9 changed files with 4010 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/target
/um-react.zip
/um-react.exe
/um-react@*.zip
/um-react@*.exe

3709
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

BIN
src/um-react@192.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB