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
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 683 B After Width: | Height: | Size: 683 B |
|
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 271 B After Width: | Height: | Size: 271 B |
|
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
|
Before Width: | Height: | Size: 780 B After Width: | Height: | Size: 780 B |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -3,6 +3,7 @@ import PackageDescription
|
|||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "MiniPlayer",
|
name: "MiniPlayer",
|
||||||
|
defaultLocalization: "en",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v17),
|
.iOS(.v17),
|
||||||
.macOS(.v14)
|
.macOS(.v14)
|
||||||
@ -14,7 +15,8 @@ let package = Package(
|
|||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "MiniPlayer",
|
name: "MiniPlayer",
|
||||||
dependencies: []
|
dependencies: [],
|
||||||
|
resources: [.process("Resources")]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct MiniPlayerApp: App {
|
struct MiniPlayerApp: App {
|
||||||
@ -14,31 +15,31 @@ struct MiniPlayerApp: App {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.commands {
|
.commands {
|
||||||
CommandGroup(replacing: .newItem) {}
|
CommandGroup(replacing: .newItem) {}
|
||||||
CommandMenu("播放") {
|
CommandMenu(L.playback) {
|
||||||
Button("播放/暂停") { bridge.togglePlayPause() }
|
Button(L.playPause) { bridge.togglePlayPause() }
|
||||||
.keyboardShortcut(" ", modifiers: [])
|
.keyboardShortcut(" ", modifiers: [])
|
||||||
Divider()
|
Divider()
|
||||||
Button("上一首") { bridge.playPrev() }
|
Button(L.prev) { bridge.playPrev() }
|
||||||
.keyboardShortcut("[", modifiers: [])
|
.keyboardShortcut("[", modifiers: [])
|
||||||
Button("下一首") { bridge.playNext() }
|
Button(L.next) { bridge.playNext() }
|
||||||
.keyboardShortcut("]", modifiers: [])
|
.keyboardShortcut("]", modifiers: [])
|
||||||
Divider()
|
Divider()
|
||||||
Button("全屏") { bridge.toggleFullscreen() }
|
Button(L.fullscreen) { bridge.toggleFullscreen() }
|
||||||
.keyboardShortcut("f", modifiers: .command)
|
.keyboardShortcut("f", modifiers: .command)
|
||||||
Divider()
|
Divider()
|
||||||
Button("音量+") { bridge.adjustVolume(by: 0.1) }
|
Button(L.volUp) { bridge.adjustVolume(by: 0.1) }
|
||||||
.keyboardShortcut("=", modifiers: .command)
|
.keyboardShortcut("=", modifiers: .command)
|
||||||
Button("音量-") { bridge.adjustVolume(by: -0.1) }
|
Button(L.volDown) { bridge.adjustVolume(by: -0.1) }
|
||||||
.keyboardShortcut("-", modifiers: .command)
|
.keyboardShortcut("-", modifiers: .command)
|
||||||
Divider()
|
Divider()
|
||||||
Button("循环模式") { bridge.cycleRepeatMode() }
|
Button(L.repeatMode) { bridge.cycleRepeatMode() }
|
||||||
.keyboardShortcut("r", modifiers: .command)
|
.keyboardShortcut("r", modifiers: .command)
|
||||||
}
|
}
|
||||||
CommandMenu("文件") {
|
CommandMenu(L.file) {
|
||||||
Button("打开文件...") { bridge.openFileDialog() }
|
Button(L.openFile) { bridge.openFileDialog() }
|
||||||
.keyboardShortcut("o", modifiers: .command)
|
.keyboardShortcut("o", modifiers: .command)
|
||||||
Divider()
|
Divider()
|
||||||
Button("添加URL...") { bridge.showURLDialog = true }
|
Button(L.addURL) { bridge.showURLDialog = true }
|
||||||
.keyboardShortcut("u", modifiers: .command)
|
.keyboardShortcut("u", modifiers: .command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,9 +47,15 @@ struct MiniPlayerApp: App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 主内容视图(直接SwiftUI,不经过BricksView)
|
// MARK: - 主内容视图
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@ObservedObject var bridge: PlayerBridge
|
@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 {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -61,7 +68,10 @@ struct ContentView: View {
|
|||||||
// 单击播放/暂停 + 双击全屏
|
// 单击播放/暂停 + 双击全屏
|
||||||
Color.clear
|
Color.clear
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { bridge.togglePlayPause() }
|
.onTapGesture {
|
||||||
|
bridge.togglePlayPause()
|
||||||
|
touchInteraction()
|
||||||
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.onTapGesture(count: 2) { bridge.toggleFullscreen() }
|
.onTapGesture(count: 2) { bridge.toggleFullscreen() }
|
||||||
#endif
|
#endif
|
||||||
@ -70,10 +80,13 @@ struct ContentView: View {
|
|||||||
// Logo(左上角,始终显示)
|
// Logo(左上角,始终显示)
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Button(action: { bridge.showToolbar.toggle() }) {
|
Button(action: {
|
||||||
Text("🎬")
|
toolbarVisible.toggle()
|
||||||
.font(.system(size: 28))
|
if toolbarVisible { touchInteraction() }
|
||||||
.padding(8)
|
}) {
|
||||||
|
MiniPlayerIcon()
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
.padding(6)
|
||||||
.background(.black.opacity(0.4))
|
.background(.black.opacity(0.4))
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
@ -85,11 +98,11 @@ struct ContentView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部 Toolbar(半透明,点击logo才显示)
|
// 底部 Toolbar(半透明)
|
||||||
if bridge.showToolbar {
|
if toolbarVisible {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ControlToolbar(bridge: bridge)
|
ControlToolbar(bridge: bridge, isHovering: $isHoveringToolbar, onTouch: touchInteraction)
|
||||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,19 +129,33 @@ struct ContentView: View {
|
|||||||
.sheet(isPresented: $bridge.showPlaylistSheet) {
|
.sheet(isPresented: $bridge.showPlaylistSheet) {
|
||||||
PlaylistWindowView(bridge: bridge)
|
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
|
.onReceive(NotificationCenter.default.publisher(for: .init("ExitFullscreen"))) { _ in
|
||||||
|
DispatchQueue.main.async { [bridge] in
|
||||||
bridge.toggleFullscreen()
|
bridge.toggleFullscreen()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
|
||||||
bridge.cleanup()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func touchInteraction() {
|
||||||
|
lastInteraction = Date()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 底部控制栏
|
// MARK: - 底部控制栏
|
||||||
struct ControlToolbar: View {
|
struct ControlToolbar: View {
|
||||||
@ObservedObject var bridge: PlayerBridge
|
@ObservedObject var bridge: PlayerBridge
|
||||||
|
@Binding var isHovering: Bool
|
||||||
|
let onTouch: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
@ -151,16 +178,16 @@ struct ControlToolbar: View {
|
|||||||
// 按钮行
|
// 按钮行
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
ToolbarButton(label: "📂 打开") { bridge.openFileDialog() }
|
ToolbarButton(label: "📂 \(L.open)") { bridge.openFileDialog(); onTouch() }
|
||||||
#endif
|
#endif
|
||||||
ToolbarButton(label: "⏮") { bridge.playPrev() }
|
ToolbarButton(label: "⏮") { bridge.playPrev(); onTouch() }
|
||||||
ToolbarButton(label: bridge.isPlaying ? "⏸" : "▶️") { bridge.togglePlayPause() }
|
ToolbarButton(label: bridge.isPlaying ? "⏸" : "▶️") { bridge.togglePlayPause(); onTouch() }
|
||||||
ToolbarButton(label: "⏭") { bridge.playNext() }
|
ToolbarButton(label: "⏭") { bridge.playNext(); onTouch() }
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// 音量控制
|
// 音量控制
|
||||||
ToolbarButton(label: "🔈") { bridge.adjustVolume(by: -0.1) }
|
ToolbarButton(label: "🔈") { bridge.adjustVolume(by: -0.1); onTouch() }
|
||||||
|
|
||||||
// 音量条
|
// 音量条
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
@ -178,6 +205,7 @@ struct ControlToolbar: View {
|
|||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
let ratio = max(0, min(1, value.location.x / geo.size.width))
|
let ratio = max(0, min(1, value.location.x / geo.size.width))
|
||||||
bridge.setVolume(Float(ratio))
|
bridge.setVolume(Float(ratio))
|
||||||
|
onTouch()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -188,7 +216,7 @@ struct ControlToolbar: View {
|
|||||||
.foregroundColor(.white.opacity(0.7))
|
.foregroundColor(.white.opacity(0.7))
|
||||||
.frame(width: 30)
|
.frame(width: 30)
|
||||||
|
|
||||||
ToolbarButton(label: "🔊") { bridge.adjustVolume(by: 0.1) }
|
ToolbarButton(label: "🔊") { bridge.adjustVolume(by: 0.1); onTouch() }
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -196,20 +224,19 @@ struct ControlToolbar: View {
|
|||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(.white.opacity(0.7))
|
.foregroundColor(.white.opacity(0.7))
|
||||||
|
|
||||||
ToolbarButton(label: "🎵 音轨") { bridge.showTrackDialog = true }
|
ToolbarButton(label: L.audioTrack) { bridge.showTrackDialog = true; onTouch() }
|
||||||
ToolbarButton(label: "\(bridge.repeatMode.icon) \(bridge.repeatMode.rawValue)") { bridge.cycleRepeatMode() }
|
ToolbarButton(label: "\(bridge.repeatMode.icon) \(bridge.repeatMode.displayName)") { bridge.cycleRepeatMode(); onTouch() }
|
||||||
ToolbarButton(label: "⛶") { bridge.toggleFullscreen() }
|
ToolbarButton(label: "⛶") { bridge.toggleFullscreen(); onTouch() }
|
||||||
ToolbarButton(label: "📋 列表 (\(bridge.queue.count))") { bridge.togglePlaylistWindow() }
|
ToolbarButton(label: "📋 \(L.list) (\(bridge.queue.count))") { bridge.togglePlaylistWindow(); onTouch() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.background(.black.opacity(0.55))
|
.background(.black.opacity(0.55))
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
bridge.isInteracting = hovering
|
isHovering = hovering
|
||||||
if hovering { bridge.recordInteraction() }
|
if hovering { onTouch() }
|
||||||
}
|
}
|
||||||
.onTapGesture { bridge.recordInteraction() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,10 +268,10 @@ struct PlaylistWindowView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack {
|
HStack {
|
||||||
Button("📂 添加文件") { bridge.openFileDialog() }
|
Button(L.addFile) { bridge.openFileDialog() }
|
||||||
Button("🔗 添加URL") { bridge.showURLDialog = true }
|
Button(L.addURLBtn) { bridge.showURLDialog = true }
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(bridge.queue.count) 项")
|
Text("\(bridge.queue.count) \(L.itemsCount)")
|
||||||
.font(.caption).foregroundColor(.secondary)
|
.font(.caption).foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
@ -253,7 +280,7 @@ struct PlaylistWindowView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
if bridge.queue.isEmpty {
|
if bridge.queue.isEmpty {
|
||||||
VStack { Spacer(); Text("暂无媒体").foregroundColor(.secondary); Spacer() }
|
VStack { Spacer(); Text(L.noMedia).foregroundColor(.secondary); Spacer() }
|
||||||
} else {
|
} else {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(spacing: 2) {
|
LazyVStack(spacing: 2) {
|
||||||
@ -292,13 +319,13 @@ struct URLInputDialog: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Text("添加媒体URL").font(.headline)
|
Text(L.addMediaURL).font(.headline)
|
||||||
TextField("https://example.com/video.m3u8", text: $url)
|
TextField("https://example.com/video.m3u8", text: $url)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
HStack {
|
HStack {
|
||||||
Button("取消") { dismiss() }
|
Button(L.cancel) { dismiss() }
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("添加") {
|
Button(L.add) {
|
||||||
if !url.isEmpty { bridge.addURL(url); dismiss() }
|
if !url.isEmpty { bridge.addURL(url); dismiss() }
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
@ -317,11 +344,11 @@ struct TrackSelectDialog: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Text("选择音轨").font(.headline)
|
Text(L.selectTrack).font(.headline)
|
||||||
ForEach(Array(bridge.availableTracks.enumerated()), id: \.offset) { idx, track in
|
ForEach(Array(bridge.availableTracks.enumerated()), id: \.offset) { idx, track in
|
||||||
Button(action: { bridge.selectTrack(index: idx); dismiss() }) {
|
Button(action: { bridge.selectTrack(index: idx); dismiss() }) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Track \(idx + 1)")
|
Text("\(L.track) \(idx + 1)")
|
||||||
Spacer()
|
Spacer()
|
||||||
if idx == bridge.currentTrackIndex {
|
if idx == bridge.currentTrackIndex {
|
||||||
Text("✓").foregroundColor(.green)
|
Text("✓").foregroundColor(.green)
|
||||||
@ -331,7 +358,7 @@ struct TrackSelectDialog: View {
|
|||||||
}.buttonStyle(.plain)
|
}.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Button("关闭") { dismiss() }
|
Button(L.close) { dismiss() }
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(minWidth: 250)
|
.frame(minWidth: 250)
|
||||||
|
|||||||
@ -16,9 +16,17 @@ struct MediaItem: Identifiable, Equatable {
|
|||||||
|
|
||||||
/// 循环模式
|
/// 循环模式
|
||||||
enum RepeatMode: String, CaseIterable {
|
enum RepeatMode: String, CaseIterable {
|
||||||
case none = "不循环"
|
case none = "none"
|
||||||
case single = "单曲循环"
|
case single = "single"
|
||||||
case all = "列表循环"
|
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 {
|
var icon: String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -137,7 +145,7 @@ final class PlayerBridge: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadTrackInfo(playerItem)
|
loadTrackInfo(playerItem)
|
||||||
showToast("正在播放: \(item.name)")
|
showToast(L.nowPlaying(item.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 播放结束
|
// MARK: - 播放结束
|
||||||
@ -246,13 +254,13 @@ final class PlayerBridge: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateTrackLabel() {
|
private func updateTrackLabel() {
|
||||||
currentTrackLabel = "🎵 Track \(currentTrackIndex + 1)/\(max(availableTracks.count, 1))"
|
currentTrackLabel = "🎵 \(L.track) \(currentTrackIndex + 1)/\(max(availableTracks.count, 1))"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 循环模式
|
// MARK: - 循环模式
|
||||||
func cycleRepeatMode() {
|
func cycleRepeatMode() {
|
||||||
repeatMode = repeatMode.next
|
repeatMode = repeatMode.next
|
||||||
showToast("循环模式: \(repeatMode.rawValue)")
|
showToast(L.repeatModeLabel(repeatMode.displayName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 全屏(视频内容全屏,非窗口全屏)
|
// MARK: - 全屏(视频内容全屏,非窗口全屏)
|
||||||
@ -305,7 +313,7 @@ final class PlayerBridge: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addURL(_ urlString: String) {
|
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 name = url.lastPathComponent.isEmpty ? (url.host ?? urlString) : url.lastPathComponent
|
||||||
let type = urlString.contains(".m3u8") ? "stream" : "url"
|
let type = urlString.contains(".m3u8") ? "stream" : "url"
|
||||||
addItem(url: url, name: name, type: type)
|
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),
|
contentRect: NSRect(x: 0, y: 0, width: 400, height: 500),
|
||||||
styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false
|
styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false
|
||||||
)
|
)
|
||||||
win.title = "播放列表 (\(queue.count))"
|
win.title = "\(L.playlist) (\(queue.count))"
|
||||||
win.isReleasedWhenClosed = false
|
win.isReleasedWhenClosed = false
|
||||||
win.center()
|
win.center()
|
||||||
win.contentView = NSHostingView(rootView: PlaylistWindowView(bridge: self))
|
win.contentView = NSHostingView(rootView: PlaylistWindowView(bridge: self))
|
||||||
|
|||||||