From aa19ab9799b2b40f9f389f5bc212e553ee2e9fdb Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 22 Jun 2026 00:40:20 +0800 Subject: [PATCH] feat: i18n support, tri-color icon, fix crash - Add i18n: Localization.swift + zh-Hans/en Localizable.strings - Add MiniPlayerIcon SwiftUI view (tri-color play button + ring) - Fix crash: isInteracting/lastInteraction no longer @Published - Fix crash: ExitFullscreen notification wrapped in DispatchQueue.main.async - Auto-hide toolbar uses local @State + Timer (not @Published) - Replace emoji logo with MiniPlayerIcon - Move icon sets out of Resources/ to avoid SPM conflicts - Package.swift: add defaultLocalization, process Resources --- .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/icon_1024.png | Bin .../AppIcon.appiconset/icon_120.png | Bin .../AppIcon.appiconset/icon_152.png | Bin .../AppIcon.appiconset/icon_167.png | Bin .../AppIcon.appiconset/icon_180.png | Bin .../AppIcon.appiconset/icon_40.png | Bin .../AppIcon.appiconset/icon_58.png | Bin .../AppIcon.appiconset/icon_60.png | Bin .../AppIcon.appiconset/icon_80.png | Bin .../AppIcon.appiconset/icon_87.png | Bin .../MiniPlayer.appiconset/Contents.json | 0 .../MiniPlayer.appiconset/icon_128.png | Bin .../MiniPlayer.appiconset/icon_128@2x.png | Bin .../MiniPlayer.appiconset/icon_16.png | Bin .../MiniPlayer.appiconset/icon_16@2x.png | Bin .../MiniPlayer.appiconset/icon_256.png | Bin .../MiniPlayer.appiconset/icon_256@2x.png | Bin .../MiniPlayer.appiconset/icon_32.png | Bin .../MiniPlayer.appiconset/icon_32@2x.png | Bin .../MiniPlayer.appiconset/icon_512.png | Bin .../MiniPlayer.appiconset/icon_512@2x.png | Bin Package.swift | 4 +- Sources/MiniPlayerApp.swift | 123 +++++++++++------- Sources/PlayerBridge.swift | 24 ++-- 25 files changed, 94 insertions(+), 57 deletions(-) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/Contents.json (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_1024.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_120.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_152.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_167.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_180.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_40.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_58.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_60.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_80.png (100%) rename {Sources/Resources/iOS => Icons_iOS}/AppIcon.appiconset/icon_87.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/Contents.json (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_128.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_128@2x.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_16.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_16@2x.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_256.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_256@2x.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_32.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_32@2x.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_512.png (100%) rename {Sources/Resources/macOS => Icons_macOS}/MiniPlayer.appiconset/icon_512@2x.png (100%) diff --git a/Sources/Resources/iOS/AppIcon.appiconset/Contents.json b/Icons_iOS/AppIcon.appiconset/Contents.json similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/Contents.json rename to Icons_iOS/AppIcon.appiconset/Contents.json diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_1024.png b/Icons_iOS/AppIcon.appiconset/icon_1024.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_1024.png rename to Icons_iOS/AppIcon.appiconset/icon_1024.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_120.png b/Icons_iOS/AppIcon.appiconset/icon_120.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_120.png rename to Icons_iOS/AppIcon.appiconset/icon_120.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_152.png b/Icons_iOS/AppIcon.appiconset/icon_152.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_152.png rename to Icons_iOS/AppIcon.appiconset/icon_152.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_167.png b/Icons_iOS/AppIcon.appiconset/icon_167.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_167.png rename to Icons_iOS/AppIcon.appiconset/icon_167.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_180.png b/Icons_iOS/AppIcon.appiconset/icon_180.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_180.png rename to Icons_iOS/AppIcon.appiconset/icon_180.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_40.png b/Icons_iOS/AppIcon.appiconset/icon_40.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_40.png rename to Icons_iOS/AppIcon.appiconset/icon_40.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_58.png b/Icons_iOS/AppIcon.appiconset/icon_58.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_58.png rename to Icons_iOS/AppIcon.appiconset/icon_58.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_60.png b/Icons_iOS/AppIcon.appiconset/icon_60.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_60.png rename to Icons_iOS/AppIcon.appiconset/icon_60.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_80.png b/Icons_iOS/AppIcon.appiconset/icon_80.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_80.png rename to Icons_iOS/AppIcon.appiconset/icon_80.png diff --git a/Sources/Resources/iOS/AppIcon.appiconset/icon_87.png b/Icons_iOS/AppIcon.appiconset/icon_87.png similarity index 100% rename from Sources/Resources/iOS/AppIcon.appiconset/icon_87.png rename to Icons_iOS/AppIcon.appiconset/icon_87.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/Contents.json b/Icons_macOS/MiniPlayer.appiconset/Contents.json similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/Contents.json rename to Icons_macOS/MiniPlayer.appiconset/Contents.json diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_128.png b/Icons_macOS/MiniPlayer.appiconset/icon_128.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_128.png rename to Icons_macOS/MiniPlayer.appiconset/icon_128.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_128@2x.png b/Icons_macOS/MiniPlayer.appiconset/icon_128@2x.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_128@2x.png rename to Icons_macOS/MiniPlayer.appiconset/icon_128@2x.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_16.png b/Icons_macOS/MiniPlayer.appiconset/icon_16.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_16.png rename to Icons_macOS/MiniPlayer.appiconset/icon_16.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_16@2x.png b/Icons_macOS/MiniPlayer.appiconset/icon_16@2x.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_16@2x.png rename to Icons_macOS/MiniPlayer.appiconset/icon_16@2x.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_256.png b/Icons_macOS/MiniPlayer.appiconset/icon_256.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_256.png rename to Icons_macOS/MiniPlayer.appiconset/icon_256.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_256@2x.png b/Icons_macOS/MiniPlayer.appiconset/icon_256@2x.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_256@2x.png rename to Icons_macOS/MiniPlayer.appiconset/icon_256@2x.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_32.png b/Icons_macOS/MiniPlayer.appiconset/icon_32.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_32.png rename to Icons_macOS/MiniPlayer.appiconset/icon_32.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_32@2x.png b/Icons_macOS/MiniPlayer.appiconset/icon_32@2x.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_32@2x.png rename to Icons_macOS/MiniPlayer.appiconset/icon_32@2x.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_512.png b/Icons_macOS/MiniPlayer.appiconset/icon_512.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_512.png rename to Icons_macOS/MiniPlayer.appiconset/icon_512.png diff --git a/Sources/Resources/macOS/MiniPlayer.appiconset/icon_512@2x.png b/Icons_macOS/MiniPlayer.appiconset/icon_512@2x.png similarity index 100% rename from Sources/Resources/macOS/MiniPlayer.appiconset/icon_512@2x.png rename to Icons_macOS/MiniPlayer.appiconset/icon_512@2x.png diff --git a/Package.swift b/Package.swift index 6bb7ffe..4fb3587 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,7 @@ import PackageDescription let package = Package( name: "MiniPlayer", + defaultLocalization: "en", platforms: [ .iOS(.v17), .macOS(.v14) @@ -14,7 +15,8 @@ let package = Package( targets: [ .executableTarget( name: "MiniPlayer", - dependencies: [] + dependencies: [], + resources: [.process("Resources")] ) ] ) diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index 51f9a64..778d99c 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -1,5 +1,6 @@ import SwiftUI import AVFoundation +import Combine @main struct MiniPlayerApp: App { @@ -14,31 +15,31 @@ struct MiniPlayerApp: App { #if os(macOS) .commands { CommandGroup(replacing: .newItem) {} - CommandMenu("播放") { - Button("播放/暂停") { bridge.togglePlayPause() } + CommandMenu(L.playback) { + Button(L.playPause) { bridge.togglePlayPause() } .keyboardShortcut(" ", modifiers: []) Divider() - Button("上一首") { bridge.playPrev() } + Button(L.prev) { bridge.playPrev() } .keyboardShortcut("[", modifiers: []) - Button("下一首") { bridge.playNext() } + Button(L.next) { bridge.playNext() } .keyboardShortcut("]", modifiers: []) Divider() - Button("全屏") { bridge.toggleFullscreen() } + Button(L.fullscreen) { bridge.toggleFullscreen() } .keyboardShortcut("f", modifiers: .command) Divider() - Button("音量+") { bridge.adjustVolume(by: 0.1) } + Button(L.volUp) { bridge.adjustVolume(by: 0.1) } .keyboardShortcut("=", modifiers: .command) - Button("音量-") { bridge.adjustVolume(by: -0.1) } + Button(L.volDown) { bridge.adjustVolume(by: -0.1) } .keyboardShortcut("-", modifiers: .command) Divider() - Button("循环模式") { bridge.cycleRepeatMode() } + Button(L.repeatMode) { bridge.cycleRepeatMode() } .keyboardShortcut("r", modifiers: .command) } - CommandMenu("文件") { - Button("打开文件...") { bridge.openFileDialog() } + CommandMenu(L.file) { + Button(L.openFile) { bridge.openFileDialog() } .keyboardShortcut("o", modifiers: .command) Divider() - Button("添加URL...") { bridge.showURLDialog = true } + Button(L.addURL) { bridge.showURLDialog = true } .keyboardShortcut("u", modifiers: .command) } } @@ -46,9 +47,15 @@ struct MiniPlayerApp: App { } } -// MARK: - 主内容视图(直接SwiftUI,不经过BricksView) +// MARK: - 主内容视图 struct ContentView: View { @ObservedObject var bridge: PlayerBridge + // 自动隐藏:本地状态,不经过 bridge 的 @Published + @State private var toolbarVisible = false + @State private var isHoveringToolbar = false + @State private var lastInteraction = Date() + private let hideTimer = Timer.publish(every: 2, on: .main, in: .common).autoconnect() + private let autoHideInterval: TimeInterval = 60 var body: some View { ZStack { @@ -61,7 +68,10 @@ struct ContentView: View { // 单击播放/暂停 + 双击全屏 Color.clear .contentShape(Rectangle()) - .onTapGesture { bridge.togglePlayPause() } + .onTapGesture { + bridge.togglePlayPause() + touchInteraction() + } #if os(macOS) .onTapGesture(count: 2) { bridge.toggleFullscreen() } #endif @@ -70,10 +80,13 @@ struct ContentView: View { // Logo(左上角,始终显示) VStack { HStack { - Button(action: { bridge.showToolbar.toggle() }) { - Text("🎬") - .font(.system(size: 28)) - .padding(8) + Button(action: { + toolbarVisible.toggle() + if toolbarVisible { touchInteraction() } + }) { + MiniPlayerIcon() + .frame(width: 36, height: 36) + .padding(6) .background(.black.opacity(0.4)) .cornerRadius(10) } @@ -85,11 +98,11 @@ struct ContentView: View { Spacer() } - // 底部 Toolbar(半透明,点击logo才显示) - if bridge.showToolbar { + // 底部 Toolbar(半透明) + if toolbarVisible { VStack { Spacer() - ControlToolbar(bridge: bridge) + ControlToolbar(bridge: bridge, isHovering: $isHoveringToolbar, onTouch: touchInteraction) .transition(.move(edge: .bottom).combined(with: .opacity)) } } @@ -116,19 +129,33 @@ struct ContentView: View { .sheet(isPresented: $bridge.showPlaylistSheet) { PlaylistWindowView(bridge: bridge) } - .animation(.easeInOut(duration: 0.25), value: bridge.showToolbar) + .animation(.easeInOut(duration: 0.25), value: toolbarVisible) + // 自动隐藏 Timer:每2秒检查一次 + .onReceive(hideTimer) { _ in + guard toolbarVisible else { return } + if isHoveringToolbar { lastInteraction = Date(); return } + if Date().timeIntervalSince(lastInteraction) >= autoHideInterval { + toolbarVisible = false + } + } + // 全屏退出通知 — async 避免在 SwiftUI 更新周期内操作 NSWindow .onReceive(NotificationCenter.default.publisher(for: .init("ExitFullscreen"))) { _ in - bridge.toggleFullscreen() - } - .onDisappear { - bridge.cleanup() + DispatchQueue.main.async { [bridge] in + bridge.toggleFullscreen() + } } } + + private func touchInteraction() { + lastInteraction = Date() + } } // MARK: - 底部控制栏 struct ControlToolbar: View { @ObservedObject var bridge: PlayerBridge + @Binding var isHovering: Bool + let onTouch: () -> Void var body: some View { VStack(spacing: 6) { @@ -151,16 +178,16 @@ struct ControlToolbar: View { // 按钮行 HStack(spacing: 10) { #if os(macOS) - ToolbarButton(label: "📂 打开") { bridge.openFileDialog() } + ToolbarButton(label: "📂 \(L.open)") { bridge.openFileDialog(); onTouch() } #endif - ToolbarButton(label: "⏮") { bridge.playPrev() } - ToolbarButton(label: bridge.isPlaying ? "⏸" : "▶️") { bridge.togglePlayPause() } - ToolbarButton(label: "⏭") { bridge.playNext() } + ToolbarButton(label: "⏮") { bridge.playPrev(); onTouch() } + ToolbarButton(label: bridge.isPlaying ? "⏸" : "▶️") { bridge.togglePlayPause(); onTouch() } + ToolbarButton(label: "⏭") { bridge.playNext(); onTouch() } Spacer() // 音量控制 - ToolbarButton(label: "🔈") { bridge.adjustVolume(by: -0.1) } + ToolbarButton(label: "🔈") { bridge.adjustVolume(by: -0.1); onTouch() } // 音量条 GeometryReader { geo in @@ -178,6 +205,7 @@ struct ControlToolbar: View { .onChanged { value in let ratio = max(0, min(1, value.location.x / geo.size.width)) bridge.setVolume(Float(ratio)) + onTouch() } ) } @@ -188,7 +216,7 @@ struct ControlToolbar: View { .foregroundColor(.white.opacity(0.7)) .frame(width: 30) - ToolbarButton(label: "🔊") { bridge.adjustVolume(by: 0.1) } + ToolbarButton(label: "🔊") { bridge.adjustVolume(by: 0.1); onTouch() } Spacer() @@ -196,20 +224,19 @@ struct ControlToolbar: View { .font(.system(size: 11)) .foregroundColor(.white.opacity(0.7)) - ToolbarButton(label: "🎵 音轨") { bridge.showTrackDialog = true } - ToolbarButton(label: "\(bridge.repeatMode.icon) \(bridge.repeatMode.rawValue)") { bridge.cycleRepeatMode() } - ToolbarButton(label: "⛶") { bridge.toggleFullscreen() } - ToolbarButton(label: "📋 列表 (\(bridge.queue.count))") { bridge.togglePlaylistWindow() } + ToolbarButton(label: L.audioTrack) { bridge.showTrackDialog = true; onTouch() } + ToolbarButton(label: "\(bridge.repeatMode.icon) \(bridge.repeatMode.displayName)") { bridge.cycleRepeatMode(); onTouch() } + ToolbarButton(label: "⛶") { bridge.toggleFullscreen(); onTouch() } + ToolbarButton(label: "📋 \(L.list) (\(bridge.queue.count))") { bridge.togglePlaylistWindow(); onTouch() } } } .padding(.horizontal, 16) .padding(.vertical, 8) .background(.black.opacity(0.55)) .onHover { hovering in - bridge.isInteracting = hovering - if hovering { bridge.recordInteraction() } + isHovering = hovering + if hovering { onTouch() } } - .onTapGesture { bridge.recordInteraction() } } } @@ -241,10 +268,10 @@ struct PlaylistWindowView: View { var body: some View { VStack(spacing: 0) { HStack { - Button("📂 添加文件") { bridge.openFileDialog() } - Button("🔗 添加URL") { bridge.showURLDialog = true } + Button(L.addFile) { bridge.openFileDialog() } + Button(L.addURLBtn) { bridge.showURLDialog = true } Spacer() - Text("\(bridge.queue.count) 项") + Text("\(bridge.queue.count) \(L.itemsCount)") .font(.caption).foregroundColor(.secondary) } .padding(12) @@ -253,7 +280,7 @@ struct PlaylistWindowView: View { Divider() if bridge.queue.isEmpty { - VStack { Spacer(); Text("暂无媒体").foregroundColor(.secondary); Spacer() } + VStack { Spacer(); Text(L.noMedia).foregroundColor(.secondary); Spacer() } } else { ScrollView { LazyVStack(spacing: 2) { @@ -292,13 +319,13 @@ struct URLInputDialog: View { var body: some View { VStack(spacing: 16) { - Text("添加媒体URL").font(.headline) + Text(L.addMediaURL).font(.headline) TextField("https://example.com/video.m3u8", text: $url) .textFieldStyle(.roundedBorder) HStack { - Button("取消") { dismiss() } + Button(L.cancel) { dismiss() } Spacer() - Button("添加") { + Button(L.add) { if !url.isEmpty { bridge.addURL(url); dismiss() } } .keyboardShortcut(.defaultAction) @@ -317,11 +344,11 @@ struct TrackSelectDialog: View { var body: some View { VStack(spacing: 12) { - Text("选择音轨").font(.headline) + Text(L.selectTrack).font(.headline) ForEach(Array(bridge.availableTracks.enumerated()), id: \.offset) { idx, track in Button(action: { bridge.selectTrack(index: idx); dismiss() }) { HStack { - Text("Track \(idx + 1)") + Text("\(L.track) \(idx + 1)") Spacer() if idx == bridge.currentTrackIndex { Text("✓").foregroundColor(.green) @@ -331,7 +358,7 @@ struct TrackSelectDialog: View { }.buttonStyle(.plain) } Divider() - Button("关闭") { dismiss() } + Button(L.close) { dismiss() } } .padding(20) .frame(minWidth: 250) diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 889aab6..87cc614 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -16,9 +16,17 @@ struct MediaItem: Identifiable, Equatable { /// 循环模式 enum RepeatMode: String, CaseIterable { - case none = "不循环" - case single = "单曲循环" - case all = "列表循环" + case none = "none" + case single = "single" + case all = "all" + + var displayName: String { + switch self { + case .none: return L.repeatNone + case .single: return L.repeatSingle + case .all: return L.repeatAll + } + } var icon: String { switch self { @@ -137,7 +145,7 @@ final class PlayerBridge: ObservableObject { } loadTrackInfo(playerItem) - showToast("正在播放: \(item.name)") + showToast(L.nowPlaying(item.name)) } // MARK: - 播放结束 @@ -246,13 +254,13 @@ final class PlayerBridge: ObservableObject { } private func updateTrackLabel() { - currentTrackLabel = "🎵 Track \(currentTrackIndex + 1)/\(max(availableTracks.count, 1))" + currentTrackLabel = "🎵 \(L.track) \(currentTrackIndex + 1)/\(max(availableTracks.count, 1))" } // MARK: - 循环模式 func cycleRepeatMode() { repeatMode = repeatMode.next - showToast("循环模式: \(repeatMode.rawValue)") + showToast(L.repeatModeLabel(repeatMode.displayName)) } // MARK: - 全屏(视频内容全屏,非窗口全屏) @@ -305,7 +313,7 @@ final class PlayerBridge: ObservableObject { } func addURL(_ urlString: String) { - guard let url = URL(string: urlString) else { showToast("无效URL"); return } + guard let url = URL(string: urlString) else { showToast(L.invalidURL); return } let name = url.lastPathComponent.isEmpty ? (url.host ?? urlString) : url.lastPathComponent let type = urlString.contains(".m3u8") ? "stream" : "url" addItem(url: url, name: name, type: type) @@ -351,7 +359,7 @@ final class PlayerBridge: ObservableObject { contentRect: NSRect(x: 0, y: 0, width: 400, height: 500), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) - win.title = "播放列表 (\(queue.count))" + win.title = "\(L.playlist) (\(queue.count))" win.isReleasedWhenClosed = false win.center() win.contentView = NSHostingView(rootView: PlaylistWindowView(bridge: self))