770 lines
28 KiB
Swift
770 lines
28 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Main Tab View
|
|
|
|
struct WatchLibraryView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@EnvironmentObject var audioPlayer: WatchAudioPlayer
|
|
@EnvironmentObject var offlineStore: WatchOfflineStore
|
|
|
|
var body: some View {
|
|
TabView {
|
|
WatchNowPlayingView()
|
|
WatchMyMusicView()
|
|
WatchOfflineLibraryView()
|
|
WatchBrowseView()
|
|
WatchSettingsView()
|
|
}
|
|
.tabViewStyle(.verticalPage)
|
|
.task {
|
|
// Sync library in background on launch
|
|
await watchManager.syncLibrary()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - My Music (Artists, Albums, Genres, Songs)
|
|
|
|
struct WatchMyMusicView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
List {
|
|
NavigationLink(destination: WatchArtistsView()) {
|
|
Label("Artists", systemImage: "music.mic")
|
|
}
|
|
NavigationLink(destination: WatchAlbumsView()) {
|
|
Label("Albums", systemImage: "square.stack")
|
|
}
|
|
NavigationLink(destination: WatchGenresView()) {
|
|
Label("Genres", systemImage: "guitars")
|
|
}
|
|
NavigationLink(destination: WatchPlaylistsListView()) {
|
|
Label("Playlists", systemImage: "list.bullet")
|
|
}
|
|
NavigationLink(destination: WatchSearchView()) {
|
|
Label("Search", systemImage: "magnifyingglass")
|
|
}
|
|
}
|
|
.navigationTitle("My Music")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Artists
|
|
|
|
struct WatchArtistsView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@State private var artistIndexes: [ArtistIndex] = []
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if artistIndexes.isEmpty && isLoading {
|
|
ProgressView()
|
|
} else {
|
|
List {
|
|
ForEach(artistIndexes) { index in
|
|
if let artists = index.artist, !artists.isEmpty {
|
|
Section(index.name) {
|
|
ForEach(artists) { artist in
|
|
NavigationLink(destination: WatchArtistDetailView(artistId: artist.id, artistName: artist.name)) {
|
|
Text(artist.name)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Artists")
|
|
.task {
|
|
// Cache first
|
|
let cached = watchManager.cachedArtists()
|
|
if !cached.isEmpty {
|
|
artistIndexes = cached
|
|
isLoading = false
|
|
}
|
|
// Refresh
|
|
do {
|
|
let fresh = try await watchManager.getArtists()
|
|
watchManager.cacheEncode(fresh, key: "artists")
|
|
await MainActor.run {
|
|
artistIndexes = fresh
|
|
isLoading = false
|
|
}
|
|
} catch {
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WatchArtistDetailView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@EnvironmentObject var audioPlayer: WatchAudioPlayer
|
|
@EnvironmentObject var offlineStore: WatchOfflineStore
|
|
|
|
let artistId: String
|
|
let artistName: String
|
|
|
|
@State private var artist: ArtistWithAlbums?
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let artist = artist {
|
|
List {
|
|
if let albums = artist.album, !albums.isEmpty {
|
|
ForEach(albums) { album in
|
|
NavigationLink(destination: WatchAlbumDetailView(albumId: album.id)) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(album.name)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
HStack(spacing: 4) {
|
|
if let year = album.year {
|
|
Text("\(year)")
|
|
.font(.caption2)
|
|
.foregroundColor(.gray)
|
|
}
|
|
Text("\(album.songCount ?? 0) songs")
|
|
.font(.caption2)
|
|
.foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if isLoading {
|
|
ProgressView()
|
|
} else {
|
|
Text("Not available")
|
|
.font(.caption)
|
|
.foregroundColor(.gray)
|
|
}
|
|
}
|
|
.navigationTitle(artistName)
|
|
.task {
|
|
if let cached = watchManager.cachedArtistDetail(id: artistId) {
|
|
artist = cached
|
|
isLoading = false
|
|
}
|
|
do {
|
|
let fresh = try await watchManager.getArtist(id: artistId)
|
|
if let fresh {
|
|
watchManager.cacheEncode(fresh, key: "artist_\(artistId)")
|
|
await MainActor.run { artist = fresh; isLoading = false }
|
|
}
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Albums (All)
|
|
|
|
struct WatchAlbumsView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@State private var albums: [Album] = []
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if albums.isEmpty && isLoading {
|
|
ProgressView()
|
|
} else {
|
|
List(albums) { album in
|
|
NavigationLink(destination: WatchAlbumDetailView(albumId: album.id)) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(album.name)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
Text(album.artist ?? "")
|
|
.font(.caption2)
|
|
.foregroundColor(.gray)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Albums")
|
|
.task {
|
|
let cached = watchManager.cachedAlbums()
|
|
if !cached.isEmpty {
|
|
albums = cached
|
|
isLoading = false
|
|
}
|
|
do {
|
|
let fresh = try await watchManager.getAllAlbums()
|
|
watchManager.cacheEncode(fresh, key: "all_albums")
|
|
await MainActor.run { albums = fresh; isLoading = false }
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Genres
|
|
|
|
struct WatchGenresView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@State private var genres: [Genre] = []
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if genres.isEmpty && isLoading {
|
|
ProgressView()
|
|
} else {
|
|
List(genres) { genre in
|
|
NavigationLink(destination: WatchGenreDetailView(genreName: genre.value)) {
|
|
HStack {
|
|
Text(genre.value)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
Spacer()
|
|
Text("\(genre.albumCount ?? 0)")
|
|
.font(.caption2)
|
|
.foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Genres")
|
|
.task {
|
|
let cached = watchManager.cachedGenres()
|
|
if !cached.isEmpty {
|
|
genres = cached
|
|
isLoading = false
|
|
}
|
|
do {
|
|
let fresh = try await watchManager.getGenres()
|
|
watchManager.cacheEncode(fresh, key: "genres")
|
|
await MainActor.run { genres = fresh; isLoading = false }
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WatchGenreDetailView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
let genreName: String
|
|
|
|
@State private var albums: [Album] = []
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if albums.isEmpty && isLoading {
|
|
ProgressView()
|
|
} else {
|
|
List(albums) { album in
|
|
NavigationLink(destination: WatchAlbumDetailView(albumId: album.id)) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(album.name).font(.caption).lineLimit(1)
|
|
Text(album.artist ?? "").font(.caption2).foregroundColor(.gray).lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(genreName)
|
|
.task {
|
|
do {
|
|
albums = try await watchManager.getAlbumsByGenre(genreName)
|
|
isLoading = false
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Offline Library
|
|
|
|
struct WatchOfflineLibraryView: View {
|
|
@EnvironmentObject var offlineStore: WatchOfflineStore
|
|
@EnvironmentObject var audioPlayer: WatchAudioPlayer
|
|
@State private var showDeleteAll = false
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
if offlineStore.songs.isEmpty {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "arrow.down.circle")
|
|
.font(.title2)
|
|
.foregroundColor(.gray)
|
|
Text("No Offline Songs")
|
|
.font(.caption)
|
|
.foregroundColor(.gray)
|
|
Text("Download from Browse or send from iPhone")
|
|
.font(.caption2)
|
|
.foregroundColor(.gray)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
} else {
|
|
List {
|
|
Section {
|
|
Button(action: playAllOffline) {
|
|
HStack { Image(systemName: "play.fill"); Text("Play All") }.foregroundColor(.pink)
|
|
}
|
|
Button(action: shuffleOffline) {
|
|
HStack { Image(systemName: "shuffle"); Text("Shuffle All") }.foregroundColor(.pink)
|
|
}
|
|
}
|
|
|
|
ForEach(Array(offlineStore.songsByAlbum.keys.sorted()), id: \.self) { album in
|
|
Section(album) {
|
|
ForEach(offlineStore.songsByAlbum[album] ?? []) { offline in
|
|
WatchSongRow(
|
|
song: offline.song,
|
|
isPlaying: audioPlayer.currentSong?.id == offline.id,
|
|
isOffline: true
|
|
) {
|
|
let albumSongs = (offlineStore.songsByAlbum[album] ?? []).map { $0.song }
|
|
let idx = albumSongs.firstIndex(where: { $0.id == offline.id }) ?? 0
|
|
audioPlayer.play(song: offline.song, fromQueue: albumSongs, at: idx)
|
|
}
|
|
}
|
|
.onDelete { indexSet in
|
|
let albumSongs = offlineStore.songsByAlbum[album] ?? []
|
|
for idx in indexSet { if idx < albumSongs.count { offlineStore.removeSong(albumSongs[idx].id) } }
|
|
}
|
|
}
|
|
}
|
|
|
|
Section {
|
|
HStack {
|
|
Text("\(offlineStore.songs.count) songs").font(.caption).foregroundColor(.gray)
|
|
Spacer()
|
|
Text(offlineStore.formattedSize).font(.caption).foregroundColor(.gray)
|
|
}
|
|
Button(role: .destructive, action: { showDeleteAll = true }) {
|
|
HStack { Image(systemName: "trash"); Text("Remove All") }.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Offline")
|
|
.alert("Remove All Songs?", isPresented: $showDeleteAll) {
|
|
Button("Remove", role: .destructive) { offlineStore.removeAll() }
|
|
Button("Cancel", role: .cancel) { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func playAllOffline() {
|
|
let songs = offlineStore.songs.map { $0.song }
|
|
guard let first = songs.first else { return }
|
|
audioPlayer.play(song: first, fromQueue: songs)
|
|
}
|
|
|
|
private func shuffleOffline() {
|
|
let songs = offlineStore.songs.map { $0.song }.shuffled()
|
|
guard let first = songs.first else { return }
|
|
audioPlayer.shuffleEnabled = true
|
|
audioPlayer.play(song: first, fromQueue: songs)
|
|
}
|
|
}
|
|
|
|
// MARK: - Browse Server
|
|
|
|
struct WatchBrowseView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
List {
|
|
NavigationLink(destination: WatchRecentAlbumsView()) {
|
|
Label("Recent Albums", systemImage: "clock")
|
|
}
|
|
NavigationLink(destination: WatchPlaylistsListView()) {
|
|
Label("Playlists", systemImage: "list.bullet")
|
|
}
|
|
NavigationLink(destination: WatchSearchView()) {
|
|
Label("Search", systemImage: "magnifyingglass")
|
|
}
|
|
|
|
if let server = watchManager.activeServer {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(server.name).font(.caption)
|
|
Text(server.url).font(.caption2).foregroundColor(.gray).lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Browse")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Recent Albums
|
|
|
|
struct WatchRecentAlbumsView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@State private var albums: [Album] = []
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if albums.isEmpty && isLoading {
|
|
ProgressView()
|
|
} else {
|
|
List(albums) { album in
|
|
NavigationLink(destination: WatchAlbumDetailView(albumId: album.id)) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(album.name).font(.caption).lineLimit(1)
|
|
Text(album.artist ?? "").font(.caption2).foregroundColor(.gray).lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Recent")
|
|
.task {
|
|
if let cached: [Album] = watchManager.cacheDecode([Album].self, key: "recent") {
|
|
albums = cached
|
|
isLoading = false
|
|
}
|
|
do {
|
|
let fresh = try await watchManager.getAlbumList(type: "newest", size: 30)
|
|
watchManager.cacheEncode(fresh, key: "recent")
|
|
await MainActor.run { albums = fresh; isLoading = false }
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Album Detail
|
|
|
|
struct WatchAlbumDetailView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@EnvironmentObject var audioPlayer: WatchAudioPlayer
|
|
@EnvironmentObject var offlineStore: WatchOfflineStore
|
|
|
|
let albumId: String
|
|
|
|
@State private var album: AlbumWithSongs?
|
|
@State private var isLoading = true
|
|
@State private var isDownloading = false
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let album = album {
|
|
List {
|
|
// Download button
|
|
Section {
|
|
Button(action: downloadAlbum) {
|
|
HStack {
|
|
Image(systemName: isDownloading ? "arrow.down.circle.fill" : "arrow.down.circle")
|
|
.foregroundColor(isDownloading ? .gray : .pink)
|
|
Text(isDownloading ? "Downloading..." : "Download Album")
|
|
.font(.caption)
|
|
}
|
|
}
|
|
.disabled(isDownloading)
|
|
}
|
|
|
|
// Songs
|
|
if let songs = album.song {
|
|
ForEach(songs) { song in
|
|
WatchSongRow(
|
|
song: song,
|
|
isPlaying: audioPlayer.currentSong?.id == song.id,
|
|
isOffline: offlineStore.isSongAvailable(song.id)
|
|
) {
|
|
audioPlayer.play(song: song, fromQueue: songs)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(album.name)
|
|
} else if isLoading {
|
|
ProgressView()
|
|
} else {
|
|
Text("Not available").font(.caption).foregroundColor(.gray)
|
|
}
|
|
}
|
|
.task {
|
|
// Cache first
|
|
if let cached = watchManager.cachedAlbumDetail(id: albumId) {
|
|
album = cached
|
|
isLoading = false
|
|
}
|
|
do {
|
|
let fresh = try await watchManager.getAlbum(id: albumId)
|
|
if let fresh {
|
|
watchManager.cacheEncode(fresh, key: "album_\(albumId)")
|
|
await MainActor.run { album = fresh; isLoading = false }
|
|
}
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
|
|
private func downloadAlbum() {
|
|
guard let album = album else { return }
|
|
isDownloading = true
|
|
Task {
|
|
await offlineStore.downloadAlbum(album)
|
|
await MainActor.run { isDownloading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Playlists
|
|
|
|
struct WatchPlaylistsListView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@State private var playlists: [Playlist] = []
|
|
@State private var isLoading = true
|
|
|
|
var body: some View {
|
|
Group {
|
|
if playlists.isEmpty && isLoading {
|
|
ProgressView()
|
|
} else {
|
|
List(playlists) { playlist in
|
|
NavigationLink(destination: WatchPlaylistDetailView(playlistId: playlist.id)) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(playlist.name).font(.caption).lineLimit(1)
|
|
Text("\(playlist.songCount ?? 0) songs").font(.caption2).foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Playlists")
|
|
.task {
|
|
let cached = watchManager.cachedPlaylists()
|
|
if !cached.isEmpty { playlists = cached; isLoading = false }
|
|
do {
|
|
let fresh = try await watchManager.getPlaylists()
|
|
watchManager.cacheEncode(fresh, key: "playlists")
|
|
await MainActor.run { playlists = fresh; isLoading = false }
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WatchPlaylistDetailView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@EnvironmentObject var audioPlayer: WatchAudioPlayer
|
|
@EnvironmentObject var offlineStore: WatchOfflineStore
|
|
|
|
let playlistId: String
|
|
|
|
@State private var playlist: PlaylistWithSongs?
|
|
@State private var isLoading = true
|
|
@State private var isDownloading = false
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let playlist = playlist, let songs = playlist.entry {
|
|
List {
|
|
Section {
|
|
Button(action: downloadPlaylist) {
|
|
HStack {
|
|
Image(systemName: "arrow.down.circle")
|
|
Text(isDownloading ? "Downloading..." : "Download All")
|
|
.font(.caption)
|
|
}
|
|
.foregroundColor(.pink)
|
|
}
|
|
.disabled(isDownloading)
|
|
}
|
|
|
|
ForEach(songs) { song in
|
|
WatchSongRow(
|
|
song: song,
|
|
isPlaying: audioPlayer.currentSong?.id == song.id,
|
|
isOffline: offlineStore.isSongAvailable(song.id)
|
|
) {
|
|
audioPlayer.play(song: song, fromQueue: songs)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(playlist.name)
|
|
} else if isLoading {
|
|
ProgressView()
|
|
}
|
|
}
|
|
.task {
|
|
do {
|
|
playlist = try await watchManager.getPlaylist(id: playlistId)
|
|
isLoading = false
|
|
} catch { isLoading = false }
|
|
}
|
|
}
|
|
|
|
private func downloadPlaylist() {
|
|
guard let entries = playlist?.entry else { return }
|
|
isDownloading = true
|
|
Task {
|
|
for song in entries { await offlineStore.downloadFromServer(song: song) }
|
|
await MainActor.run { isDownloading = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Search
|
|
|
|
struct WatchSearchView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@EnvironmentObject var audioPlayer: WatchAudioPlayer
|
|
|
|
@State private var query = ""
|
|
@State private var results: SearchResult3?
|
|
@State private var isSearching = false
|
|
|
|
var body: some View {
|
|
VStack {
|
|
TextField("Search", text: $query)
|
|
.onSubmit { performSearch() }
|
|
|
|
if isSearching {
|
|
ProgressView()
|
|
} else if let results = results {
|
|
List {
|
|
if let songs = results.song, !songs.isEmpty {
|
|
Section("Songs") {
|
|
ForEach(songs.prefix(10)) { song in
|
|
WatchSongRow(song: song, isPlaying: audioPlayer.currentSong?.id == song.id) {
|
|
audioPlayer.play(song: song, fromQueue: songs)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let albums = results.album, !albums.isEmpty {
|
|
Section("Albums") {
|
|
ForEach(albums.prefix(5)) { album in
|
|
NavigationLink(destination: WatchAlbumDetailView(albumId: album.id)) {
|
|
VStack(alignment: .leading) {
|
|
Text(album.name).font(.caption).lineLimit(1)
|
|
Text(album.artist ?? "").font(.caption2).foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Search")
|
|
}
|
|
|
|
private func performSearch() {
|
|
guard !query.isEmpty else { return }
|
|
isSearching = true
|
|
Task {
|
|
do { results = try await watchManager.search(query: query); isSearching = false }
|
|
catch { isSearching = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Reusable Song Row
|
|
|
|
struct WatchSongRow: View {
|
|
let song: Song
|
|
let isPlaying: Bool
|
|
var isOffline: Bool = false
|
|
let onTap: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
HStack(spacing: 6) {
|
|
if isPlaying {
|
|
Image(systemName: "waveform")
|
|
.font(.caption2).foregroundColor(.pink).frame(width: 14)
|
|
} else if isOffline {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.caption2).foregroundColor(.green).frame(width: 14)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
Text(song.title)
|
|
.font(.caption)
|
|
.foregroundColor(isPlaying ? .pink : .white)
|
|
.lineLimit(1)
|
|
Text(song.artist ?? "")
|
|
.font(.caption2).foregroundColor(.gray).lineLimit(1)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text(song.durationFormatted)
|
|
.font(.caption2).foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Settings
|
|
|
|
struct WatchSettingsView: View {
|
|
@EnvironmentObject var watchManager: WatchSessionManager
|
|
@EnvironmentObject var offlineStore: WatchOfflineStore
|
|
@State private var showAddServer = false
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
List {
|
|
Section("Server") {
|
|
ForEach(watchManager.servers) { server in
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
HStack {
|
|
Text(server.name).font(.caption)
|
|
if watchManager.activeServer?.id == server.id {
|
|
Image(systemName: "checkmark").font(.caption2).foregroundColor(.pink)
|
|
}
|
|
}
|
|
Text(server.url).font(.caption2).foregroundColor(.gray).lineLimit(1)
|
|
}
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { watchManager.setActive(server) }
|
|
}
|
|
|
|
Button("Add Server") { showAddServer = true }.font(.caption).foregroundColor(.pink)
|
|
Button("Sync from iPhone") { watchManager.requestServersFromPhone() }.font(.caption)
|
|
|
|
if watchManager.isSyncing {
|
|
HStack {
|
|
ProgressView().scaleEffect(0.6)
|
|
Text("Syncing library...").font(.caption2).foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Storage") {
|
|
HStack {
|
|
Text("Offline Songs").font(.caption); Spacer()
|
|
Text("\(offlineStore.songs.count)").font(.caption).foregroundColor(.gray)
|
|
}
|
|
HStack {
|
|
Text("Used").font(.caption); Spacer()
|
|
Text(offlineStore.formattedSize).font(.caption).foregroundColor(.gray)
|
|
}
|
|
if !offlineStore.songs.isEmpty {
|
|
Button("Remove All Downloads", role: .destructive) { offlineStore.removeAll() }.font(.caption)
|
|
}
|
|
}
|
|
|
|
Section("Phone") {
|
|
HStack {
|
|
Circle().fill(watchManager.isPhoneReachable ? .green : .orange).frame(width: 6, height: 6)
|
|
Text(watchManager.isPhoneReachable ? "Connected" : "Not Reachable").font(.caption).foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Settings")
|
|
}
|
|
.sheet(isPresented: $showAddServer) {
|
|
WatchAddServerView()
|
|
}
|
|
}
|
|
}
|