feat(macOS): add traffic light button spacing and optimize iOS build support

add adjustment function for macOS window traffic light button spacing, support custom spacing value
add iOS build support module to optimize build flow and link configuration
add eruda debugging tool to support mobile development environment
Adjust left layout width to fit macOS 26+
skip update checking logic in development environments
This commit is contained in:
Dawn
2025-12-22 00:54:27 +08:00
parent a7f9718981
commit ad72331c16
16 changed files with 256 additions and 35 deletions

View File

@@ -10,37 +10,53 @@ index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
[env]
CFLAGS_aarch64_apple_darwin = "-fno-modules"
CFLAGS_x86_64_apple_darwin = "-fno-modules"
# Android NDK 链接配置
[target.aarch64-linux-android]
linker = "aarch64-linux-android26-clang"
rustflags = [
"-C", "link-arg=-lc++_shared",
"-C", "link-arg=-llog",
"-C", "link-arg=-landroid",
"-C",
"link-arg=-lc++_shared",
"-C",
"link-arg=-llog",
"-C",
"link-arg=-landroid",
]
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi26-clang"
rustflags = [
"-C", "link-arg=-lc++_shared",
"-C", "link-arg=-llog",
"-C", "link-arg=-landroid",
"-C",
"link-arg=-lc++_shared",
"-C",
"link-arg=-llog",
"-C",
"link-arg=-landroid",
]
[target.i686-linux-android]
linker = "i686-linux-android26-clang"
rustflags = [
"-C", "link-arg=-lc++_shared",
"-C", "link-arg=-llog",
"-C", "link-arg=-landroid",
"-C",
"link-arg=-lc++_shared",
"-C",
"link-arg=-llog",
"-C",
"link-arg=-landroid",
]
[target.x86_64-linux-android]
linker = "x86_64-linux-android26-clang"
rustflags = [
"-C", "link-arg=-lc++_shared",
"-C", "link-arg=-llog",
"-C", "link-arg=-landroid",
"-C",
"link-arg=-lc++_shared",
"-C",
"link-arg=-llog",
"-C",
"link-arg=-landroid",
]
# Windows OpenSSL 编译需要原生 Windows Perl (Strawberry Perl)
@@ -53,4 +69,3 @@ PERL = "C:/Strawberry/perl/bin/perl.exe"
[target.aarch64-pc-windows-msvc.env]
PERL = "C:/Strawberry/perl/bin/perl.exe"

3
package.json vendored
View File

@@ -176,7 +176,8 @@
"vite-plugin-vue-setup-extend": "^0.4.0",
"vitest": "^4.0.15",
"vue-tsc": "^3.1.5",
"web-vitals": "^5.1.0"
"web-vitals": "^5.1.0",
"eruda": "^3.4.3"
},
"config": {
"commitizen": {

8
pnpm-lock.yaml generated vendored
View File

@@ -261,6 +261,9 @@ importers:
cz-git:
specifier: ^1.12.0
version: 1.12.0
eruda:
specifier: ^3.4.3
version: 3.4.3
happy-dom:
specifier: ^20.0.2
version: 20.0.2
@@ -3233,6 +3236,9 @@ packages:
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
eruda@3.4.3:
resolution: {integrity: sha512-J2TsF4dXSspOXev5bJ6mljv0dRrxj21wklrDzbvPmYaEmVoC+2psylyRi70nUPFh1mTQfIBsSusUtAMZtUN+/w==}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -8322,6 +8328,8 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
eruda@3.4.3: {}
es-module-lexer@1.7.0: {}
es-toolkit@1.43.0: {}

View File

@@ -150,6 +150,7 @@ screenshots = "0.8.10"
[target."cfg(target_os =\"macos\")".dependencies]
objc2-app-kit = "0.3.2"
objc2 = "0.6.3"
objc2-core-foundation = "0.3.2"
core-foundation = "0.10.1"
core-graphics = "0.25.0"
libc = "0.2"

View File

@@ -1,26 +1,17 @@
#[path = "src/mobiles/ios/build_support.rs"]
mod ios_build_support;
use std::{env, fs, io};
fn main() -> Result<(), Box<dyn std::error::Error>> {
compile_ios_splash();
ios_build_support::add_clang_runtime_search_path();
ios_build_support::compile_ios_splash();
ensure_frontend_dist()?;
tauri_build::build();
Ok(())
}
fn compile_ios_splash() {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("ios") {
return;
}
println!("cargo:rerun-if-changed=gen/apple/Sources/hula/SplashScreen.mm");
cc::Build::new()
.file("gen/apple/Sources/hula/SplashScreen.mm")
.flag("-fobjc-arc")
.compile("hula_ios_splash");
}
fn ensure_frontend_dist() -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let parent_dir = current_dir

View File

@@ -11,6 +11,8 @@ use tauri::{AppHandle, LogicalSize, Manager, ResourceId, Runtime, Webview};
use objc2::rc::Retained;
#[cfg(target_os = "macos")]
use objc2_app_kit::NSWindow;
#[cfg(target_os = "macos")]
use objc2_core_foundation::{CGPoint, CGRect};
#[cfg(target_os = "windows")]
use serde::Serialize;
#[cfg(target_os = "macos")]
@@ -171,6 +173,61 @@ fn set_window_movable_state(ns_window: &NSWindow, movable: bool) {
ns_window.setMovableByWindowBackground(movable);
}
#[cfg(target_os = "macos")]
fn apply_traffic_lights_spacing(ns_window: &NSWindow, spacing: f64) -> Result<(), String> {
use objc2_app_kit::NSWindowButton;
let close = ns_window
.standardWindowButton(NSWindowButton::CloseButton)
.ok_or_else(|| "CloseButton not found".to_string())?;
let minimize = ns_window
.standardWindowButton(NSWindowButton::MiniaturizeButton)
.ok_or_else(|| "MiniaturizeButton not found".to_string())?;
let zoom = ns_window
.standardWindowButton(NSWindowButton::ZoomButton)
.ok_or_else(|| "ZoomButton not found".to_string())?;
let close_frame: CGRect = unsafe { objc2::msg_send![&*close, frame] };
let min_frame: CGRect = unsafe { objc2::msg_send![&*minimize, frame] };
let zoom_frame: CGRect = unsafe { objc2::msg_send![&*zoom, frame] };
let new_min_x = close_frame.origin.x + close_frame.size.width + spacing;
let new_zoom_x = new_min_x + min_frame.size.width + spacing;
let _: () = unsafe {
objc2::msg_send![
&*minimize,
setFrameOrigin: CGPoint {
x: new_min_x,
y: min_frame.origin.y
}
]
};
let _: () = unsafe {
objc2::msg_send![
&*zoom,
setFrameOrigin: CGPoint {
x: new_zoom_x,
y: zoom_frame.origin.y
}
]
};
Ok(())
}
#[cfg(target_os = "macos")]
pub(crate) fn apply_macos_traffic_lights_spacing_default<R: Runtime>(
window_label: &str,
handle: AppHandle<R>,
) -> Result<(), String> {
const DEFAULT_SPACING: f64 = 6.0;
let webview_window = get_webview_window(&handle, window_label)?;
let ns_window = get_nswindow_from_webview_window(&webview_window)?;
apply_traffic_lights_spacing(&ns_window, DEFAULT_SPACING)
}
/// 隐藏Mac窗口的标题栏按钮红绿灯按钮和标题
///
/// # 参数
@@ -230,6 +287,21 @@ pub fn show_title_bar_buttons(window_label: &str, handle: AppHandle) -> Result<(
Ok(())
}
#[tauri::command]
#[cfg(target_os = "macos")]
pub fn set_macos_traffic_lights_spacing(
window_label: &str,
spacing: f64,
handle: AppHandle,
) -> Result<(), String> {
if !(0.0..=30.0).contains(&spacing) {
return Err("Invalid spacing value".to_string());
}
let webview_window = get_webview_window(&handle, window_label)?;
let ns_window = get_nswindow_from_webview_window(&webview_window)?;
apply_traffic_lights_spacing(&ns_window, spacing)
}
/// 设置 macOS 窗口是否可拖动
#[tauri::command]
#[cfg(target_os = "macos")]
@@ -257,15 +329,18 @@ pub fn set_window_level_above_menubar(window_label: &str, handle: AppHandle) ->
}
#[cfg(target_os = "macos")]
fn get_webview_window(handle: &AppHandle, window_label: &str) -> Result<WebviewWindow, String> {
fn get_webview_window<R: Runtime>(
handle: &AppHandle<R>,
window_label: &str,
) -> Result<WebviewWindow<R>, String> {
handle
.get_webview_window(window_label)
.ok_or_else(|| format!("Window '{}' not found", window_label))
}
#[cfg(target_os = "macos")]
fn get_nswindow_from_webview_window(
webview_window: &WebviewWindow,
fn get_nswindow_from_webview_window<R: Runtime>(
webview_window: &WebviewWindow<R>,
) -> Result<Retained<NSWindow>, String> {
webview_window
.ns_window()

View File

@@ -1,4 +1,6 @@
use crate::common::init::{CustomInit, init_common_plugins};
#[cfg(target_os = "macos")]
use crate::desktops::common_cmd::apply_macos_traffic_lights_spacing_default;
use tauri::{Manager, Runtime, WindowEvent};
use tauri_plugin_autostart::MacosLauncher;
@@ -60,6 +62,13 @@ impl<R: Runtime> DesktopCustomInit for tauri::Builder<R> {
fn init_window_event(self) -> Self {
self.on_window_event(|window, event: &WindowEvent| match event {
WindowEvent::Focused(flag) => {
#[cfg(target_os = "macos")]
if *flag {
let _ = apply_macos_traffic_lights_spacing_default(
window.label(),
window.app_handle().clone(),
);
}
// 自定义系统托盘-实现托盘菜单失去焦点时隐藏
#[cfg(not(target_os = "macos"))]
if !window.label().eq("tray") && *flag {
@@ -127,7 +136,15 @@ impl<R: Runtime> DesktopCustomInit for tauri::Builder<R> {
// 如果有home窗口说明是登录成功后的正常关闭允许关闭
}
}
WindowEvent::Resized(_ps) => {}
WindowEvent::Resized(_ps) => {
#[cfg(target_os = "macos")]
{
let _ = apply_macos_traffic_lights_spacing_default(
window.label(),
window.app_handle().clone(),
);
}
}
_ => (),
})
}

View File

@@ -10,8 +10,8 @@ use common_cmd::get_windows_scale_info;
use common_cmd::{audio, default_window_icon, screenshot, set_height};
#[cfg(target_os = "macos")]
use common_cmd::{
hide_title_bar_buttons, set_window_level_above_menubar, set_window_movable,
show_title_bar_buttons,
hide_title_bar_buttons, set_macos_traffic_lights_spacing, set_window_level_above_menubar,
set_window_movable, show_title_bar_buttons,
};
#[cfg(target_os = "macos")]
use desktops::app_event;
@@ -415,6 +415,8 @@ fn get_invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Se
#[cfg(target_os = "macos")]
show_title_bar_buttons,
#[cfg(target_os = "macos")]
set_macos_traffic_lights_spacing,
#[cfg(target_os = "macos")]
set_window_level_above_menubar,
#[cfg(target_os = "macos")]
set_window_movable,

View File

@@ -0,0 +1,57 @@
use std::{env, process::Command};
pub(crate) fn add_clang_runtime_search_path() {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("ios") {
return;
}
println!("cargo:rerun-if-env-changed=DEVELOPER_DIR");
println!("cargo:rerun-if-env-changed=SDKROOT");
println!("cargo:rerun-if-env-changed=TARGET");
let target = env::var("TARGET").unwrap_or_default();
let sdk = if target.contains("ios-sim") {
"iphonesimulator"
} else {
"iphoneos"
};
let Ok(output) = Command::new("xcrun")
.args(["--sdk", sdk, "clang", "-print-resource-dir"])
.output()
else {
return;
};
if !output.status.success() {
return;
}
let resource_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
if resource_dir.is_empty() {
return;
}
let darwin_lib_dir = std::path::Path::new(&resource_dir)
.join("lib")
.join("darwin");
if darwin_lib_dir.is_dir() {
println!(
"cargo:rustc-link-search=native={}",
darwin_lib_dir.display()
);
}
}
pub(crate) fn compile_ios_splash() {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("ios") {
return;
}
println!("cargo:rerun-if-changed=gen/apple/Sources/hula/SplashScreen.mm");
cc::Build::new()
.file("gen/apple/Sources/hula/SplashScreen.mm")
.flag("-fobjc-arc")
.compile("hula_ios_splash");
}

View File

@@ -22,6 +22,10 @@ export const useCheckUpdate = () => {
* @param initialCheck 是否是初始检查默认为false。初始检查时只显示强制更新提示不显示普通更新提示
*/
const checkUpdate = async (closeWin: string, initialCheck: boolean = false) => {
if (import.meta.env.DEV) {
return
}
await check({
timeout: 5000 /* 接口请求时长 5秒 */,
headers: {

View File

@@ -13,6 +13,7 @@ const isCompatibilityMode = computed(() => isCompatibility())
const WINDOW_SAFE_PADDING = 32
const MIN_LOGICAL_WIDTH = 320
const MIN_LOGICAL_HEIGHT = 200
const MAC_TRAFFIC_LIGHTS_SPACING = 6
const clampSizeToMonitor = (width: number, height: number, monitor?: Monitor | null) => {
if (!monitor) {
@@ -161,6 +162,14 @@ export const useWindow = () => {
})
await webview.once('tauri://created', async () => {
if (isMac()) {
try {
await invoke('set_macos_traffic_lights_spacing', {
windowLabel: label,
spacing: MAC_TRAFFIC_LIGHTS_SPACING
})
} catch {}
}
if (wantCloseWindow) {
const win = await WebviewWindow.getByLabel(wantCloseWindow)
win?.close()
@@ -334,6 +343,12 @@ export const useWindow = () => {
} catch (error) {
console.error('设置子窗口不可拖动失败:', error)
}
try {
await invoke('set_macos_traffic_lights_spacing', {
windowLabel: label,
spacing: MAC_TRAFFIC_LIGHTS_SPACING
})
} catch {}
attachMacModalOverlay(label)
}
})

View File

@@ -1,7 +1,9 @@
<template>
<div style="background: var(--left-bg-color)" class="h-full">
<div style="background: var(--left-bg-color)" class="h-30px"></div>
<main class="left min-w-64px h-full p-[0_6px_40px] box-border flex-col-center select-none" data-tauri-drag-region>
<main
:class="`left ${leftMinWidthClass} h-full p-[0_6px_40px] box-border flex-col-center select-none`"
data-tauri-drag-region>
<p class="text-(16px [--left-text-color]) cursor-default select-none m-[4px_0_16px_0]">HuLa</p>
<!-- 头像模块 -->
<LeftAvatar />
@@ -19,6 +21,7 @@
import type { Component } from 'vue'
import { MittEnum, ModalEnum } from '@/enums'
import { useMitt } from '@/hooks/useMitt.ts'
import { isMac26 } from '@/utils/PlatformConstants'
import ActionList from './components/ActionList.vue'
import InfoEdit from './components/InfoEdit.vue'
import LeftAvatar from './components/LeftAvatar.vue'
@@ -33,6 +36,8 @@ const componentMapping: Record<number, Component> = {
[ModalEnum.CHECK_UPDATE]: CheckUpdate
}
const leftMinWidthClass = computed(() => (isMac26() ? 'min-w-68px' : 'min-w-64px'))
onMounted(() => {
useMitt.on(MittEnum.LEFT_MODAL_SHOW, (event: { type: ModalEnum; props?: Record<string, any> }) => {
componentMap.value = componentMapping[event.type]

View File

@@ -22,6 +22,13 @@ if (process.env.NODE_ENV === 'development') {
/**! 控制台打印项目版本信息(不需要可手动关闭)*/
module.consolePrint()
})
if (isMobile()) {
import('eruda').then((module) => {
const eruda = 'default' in module ? module.default : module
eruda.init()
})
}
}
export const forceUpdateMessageTop = (topValue: number) => {

View File

@@ -21,6 +21,11 @@ declare namespace Common {
/** 构建时间 */
declare const PROJECT_BUILD_TIME: string
declare module 'eruda' {
const eruda: { init: (options?: unknown) => void }
export default eruda
}
export type ProxySettings = {
apiType: string
apiIp: string

View File

@@ -147,6 +147,19 @@ export const isWindows10 = (): boolean => PlatformDetector.isWindows10
*/
export const isMac = (): boolean => PlatformDetector.osType === 'macos'
export const isMac26 = (): boolean => {
if (!isMac()) return false
const osVersion = getOSVersion()
if (!osVersion) return false
const numbers = osVersion
.match(/\d+/g)
?.map((num) => Number.parseInt(num, 10))
.filter((num) => !Number.isNaN(num))
if (!numbers || numbers.length === 0) return false
const [major] = numbers
return major >= 26
}
/**
* 是否为 Linux 系统
*/
@@ -184,6 +197,7 @@ export const Platform = {
isWindows,
isWindows10,
isMac,
isMac26,
isLinux,
isAndroid,
isIOS,

View File

@@ -304,6 +304,10 @@ const init = async () => {
onMounted(async () => {
await init()
if (import.meta.env.DEV) {
loading.value = false
return
}
const url = `https://gitee.com/api/v5/repos/HuLaSpark/HuLa/releases/tags/v${currentVersion.value}?access_token=${import.meta.env.VITE_GITEE_TOKEN}`
await getCommitLog(url)
await checkUpdate()