chore: add svelte component layer with monaco editor
This commit is contained in:
1 parent
8071085e94
commit
54f7f81160
55 files changed
+20563
-2043
No files matched your search
+96
-14
@@ -15,6 +15,7 @@ use tauri::Manager;
|
||||
use tauri::State;
|
||||
use tauri::WebviewUrl;
|
||||
use tauri::WebviewWindowBuilder;
|
||||
use tauri::WindowEvent;
|
||||
use tauri_plugin_fs::FsExt;
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -1599,12 +1600,14 @@ pub fn theme_preview_url(path: String) -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a hardware-decodable copy of a large VP9/AV1 theme video.
|
||||
/// Return a hardware-decodable copy of a VP9/AV1 theme video.
|
||||
///
|
||||
/// WebKit on macOS decodes VP9 in software. A 1080p 10 Mbps WebM can use
|
||||
/// enough CPU to starve the audio decoder, while H.264 is decoded by the
|
||||
/// media engine. The first playback of a large theme video is transcoded to a
|
||||
/// cached 720p H.264/AAC file; later loads return the cached URL directly.
|
||||
/// WebKit on macOS decodes VP9/AV1 in software. A 1080p WebM can use enough
|
||||
/// CPU to starve the audio decoder, while H.264 is decoded by the media
|
||||
/// engine. The first playback of an installed theme video or a video picked
|
||||
/// in the editor is transcoded to a cached 720p H.264/AAC file; later loads
|
||||
/// return the cached URL directly. Videos already encoded as H.264/HEVC, and
|
||||
/// very small clips, are left untouched.
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ThemeMediaProgress {
|
||||
@@ -1615,6 +1618,12 @@ pub struct ThemeMediaProgress {
|
||||
#[cfg(target_os = "macos")]
|
||||
const FFMPEG_VERSION: &str = "9.0.2";
|
||||
|
||||
/// Small clips are usually short stickers; transcoding them would cost more
|
||||
/// time and disk than it saves. Anything larger may be a 1080p VP9 stream that
|
||||
/// WebKit cannot decode smoothly in software.
|
||||
#[cfg(target_os = "macos")]
|
||||
const THEME_VIDEO_OPTIMIZE_MIN_BYTES: u64 = 2 * 1024 * 1024;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct FfmpegBuild {
|
||||
url: &'static str,
|
||||
@@ -1659,7 +1668,13 @@ pub async fn optimize_theme_media(app: AppHandle, source_url: String) -> Result<
|
||||
return Ok(source_url);
|
||||
}
|
||||
};
|
||||
if metadata.len() < 20 * 1024 * 1024 {
|
||||
if metadata.len() < THEME_VIDEO_OPTIMIZE_MIN_BYTES {
|
||||
log::debug!(
|
||||
target: "launcher",
|
||||
"Skipping theme video optimization for {} ({} bytes)",
|
||||
source.display(),
|
||||
metadata.len()
|
||||
);
|
||||
return Ok(source_url);
|
||||
}
|
||||
|
||||
@@ -2155,17 +2170,34 @@ pub async fn open_theme_editor(app: AppHandle, theme_folder: Option<String>) ->
|
||||
let _ = window.set_focus();
|
||||
app.emit_to("theme-editor", "theme-editor-context", ())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let _ = app.emit_to("main", "theme-editor-state", true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
WebviewWindowBuilder::new(&app, "theme-editor", WebviewUrl::App("index.html".into()))
|
||||
.title("EZPPLauncher Theme Editor")
|
||||
.inner_size(1440.0, 900.0)
|
||||
.min_inner_size(1100.0, 720.0)
|
||||
.resizable(true)
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|error| format!("Could not open the theme editor: {error}"))?;
|
||||
let window =
|
||||
WebviewWindowBuilder::new(&app, "theme-editor", WebviewUrl::App("index.html".into()))
|
||||
.title("EZPPLauncher Theme Editor")
|
||||
.inner_size(1440.0, 900.0)
|
||||
.min_inner_size(1100.0, 720.0)
|
||||
.resizable(true)
|
||||
// Tauri's native file-drop handler intercepts HTML5 drag events on
|
||||
// macOS/WebKit. The editor only needs HTML5 drag and drop for its layer
|
||||
// tree, while the main launcher keeps native theme-file dropping.
|
||||
.disable_drag_drop_handler()
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|error| format!("Could not open the theme editor: {error}"))?;
|
||||
|
||||
// Pause the launcher's own theme media while the editor window exists. The
|
||||
// editor is a separate WebView, so the launcher needs an explicit signal
|
||||
// rather than relying on focus/blur events.
|
||||
let main_window = app.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if matches!(event, WindowEvent::Destroyed) {
|
||||
let _ = main_window.emit_to("main", "theme-editor-state", false);
|
||||
}
|
||||
});
|
||||
let _ = app.emit_to("main", "theme-editor-state", true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2531,6 +2563,49 @@ fn collect_theme_asset_refs(value: &Value, assets: &mut HashSet<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_svelte_sources(value: &Value, sources: &mut Vec<String>) {
|
||||
let Some(elements) = value.get("elements").and_then(Value::as_array) else {
|
||||
return;
|
||||
};
|
||||
for element in elements {
|
||||
if element.get("type").and_then(Value::as_str) != Some("svelte") {
|
||||
continue;
|
||||
}
|
||||
if let Some(source) = element.get("source").and_then(Value::as_str) {
|
||||
sources.push(source.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom Svelte layers load assets through `asset("path")`, which is not a
|
||||
/// `src` field in theme.json. Keep any asset whose relative path appears in a
|
||||
/// component source so exports do not drop files used by custom layers.
|
||||
fn collect_svelte_asset_refs(assets_root: &Path, sources: &[String], assets: &mut HashSet<String>) {
|
||||
if sources.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Ok(entries) = std::fs::read_dir(assets_root) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect_svelte_asset_refs(&path, sources, assets);
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let Ok(relative) = path.strip_prefix(assets_root) else {
|
||||
continue;
|
||||
};
|
||||
let key = relative.to_string_lossy().replace('\\', "/");
|
||||
if sources.iter().any(|source| source.contains(&key)) {
|
||||
assets.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For API 2.0 packages, return the assets referenced by `theme.json` so an
|
||||
/// export can omit orphaned files left behind by older edits. API 1.0 themes
|
||||
/// can load assets dynamically from their bundle, so they are exported whole.
|
||||
@@ -2547,6 +2622,13 @@ fn referenced_theme_assets(theme_json: &Path) -> Option<HashSet<String>> {
|
||||
// `preview` is omitted from theme.json.
|
||||
assets.insert("preview.png".to_string());
|
||||
collect_theme_asset_refs(&parsed, &mut assets);
|
||||
|
||||
if let Some(folder) = theme_json.parent() {
|
||||
let assets_root = folder.join("assets");
|
||||
let mut sources = Vec::new();
|
||||
collect_svelte_sources(&parsed, &mut sources);
|
||||
collect_svelte_asset_refs(&assets_root, &sources, &mut assets);
|
||||
}
|
||||
Some(assets)
|
||||
}
|
||||
|
||||
|
||||
@@ -134,8 +134,9 @@ pub fn media_root() -> Result<PathBuf, String> {
|
||||
.map_err(|error| format!("Failed to resolve the theme media cache: {error}"))
|
||||
}
|
||||
|
||||
/// Map a loopback theme URL back to the file it serves. Only paths inside the
|
||||
/// configured themes folder are accepted.
|
||||
/// Map a loopback theme URL back to the file it serves. Installed theme files
|
||||
/// are restricted to the themes folder; editor preview URLs must match a token
|
||||
/// registered through `preview_url`.
|
||||
pub fn resolve_theme_url(value: &str) -> Option<PathBuf> {
|
||||
let server = SERVER.get()?;
|
||||
let url = url::Url::parse(value).ok()?;
|
||||
@@ -149,8 +150,16 @@ pub fn resolve_theme_url(value: &str) -> Option<PathBuf> {
|
||||
let path = percent_encoding::percent_decode_str(url.path())
|
||||
.decode_utf8_lossy()
|
||||
.to_string();
|
||||
let relative = path.strip_prefix("/themes/")?;
|
||||
safe_path(&server.root, relative)
|
||||
if let Some(relative) = path.strip_prefix("/themes/") {
|
||||
return safe_path(&server.root, relative);
|
||||
}
|
||||
|
||||
let token = path.strip_prefix("/preview/")?;
|
||||
server
|
||||
.previews
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|previews| previews.get(token).cloned())
|
||||
}
|
||||
|
||||
/// Public URL for a file in the optimized media cache.
|
||||
|
||||
Reference in new issue
Block a user