Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions flo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
B0BAAAA62F31F0A0002A5FBB /* RadiosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BAAAA52F31F0A0002A5FBB /* RadiosView.swift */; };
B0BAAAA92F3213AF002A5FBB /* RadiosViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BAAAA82F3213AB002A5FBB /* RadiosViewModel.swift */; };
B0BAAAAB2F3214F7002A5FBB /* Radio.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BAAAAA2F3214F7002A5FBB /* Radio.swift */; };
B0BAAAAD2F3216A0002A5FBB /* RadioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BAAAAC2F321697002A5FBB /* RadioService.swift */; };
C401D09A2C5AED9F009F91C7 /* LocalFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C401D0992C5AED9F009F91C7 /* LocalFileManager.swift */; };
C4051DFF2CD25BBA0039D062 /* ArtistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4051DFE2CD25BBA0039D062 /* ArtistsView.swift */; };
C4100A692CE78B25001BC9BE /* PlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4100A682CE78B21001BC9BE /* PlaylistView.swift */; };
Expand Down Expand Up @@ -78,6 +82,10 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
B0BAAAA52F31F0A0002A5FBB /* RadiosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadiosView.swift; sourceTree = "<group>"; };
B0BAAAA82F3213AB002A5FBB /* RadiosViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadiosViewModel.swift; sourceTree = "<group>"; };
B0BAAAAA2F3214F7002A5FBB /* Radio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Radio.swift; sourceTree = "<group>"; };
B0BAAAAC2F321697002A5FBB /* RadioService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioService.swift; sourceTree = "<group>"; };
C401D0992C5AED9F009F91C7 /* LocalFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileManager.swift; sourceTree = "<group>"; };
C4051DFE2CD25BBA0039D062 /* ArtistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistsView.swift; sourceTree = "<group>"; };
C4100A682CE78B21001BC9BE /* PlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -162,6 +170,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
B0BAAAA72F32139D002A5FBB /* Radios */ = {
isa = PBXGroup;
children = (
B0BAAAA82F3213AB002A5FBB /* RadiosViewModel.swift */,
B0BAAAA52F31F0A0002A5FBB /* RadiosView.swift */,
);
path = Radios;
sourceTree = "<group>";
};
C4289F4B2C1253B800C3A4FD /* Shared */ = {
isa = PBXGroup;
children = (
Expand All @@ -185,6 +202,7 @@
C446A6B62C08DE8800CC9787 /* UserAuth.swift */,
C4120FDC2C15E1C300E712BE /* Song.swift */,
C49495802C1C25E5006B4D1E /* ScanStatus.swift */,
B0BAAAAA2F3214F7002A5FBB /* Radio.swift */,
C4EAA4852C297E35007EB2E0 /* NowPlaying.swift */,
C456D8F52F2FBD61002AAB8B /* LRCLIB.swift */,
C456D8FB2F2FF397002AAB8B /* LyricsLine.swift */,
Expand Down Expand Up @@ -237,6 +255,7 @@
C4DE89172C2FFBC900E078CC /* CoreDataManager.swift */,
C401D0992C5AED9F009F91C7 /* LocalFileManager.swift */,
C4D7F84E2C7F2C5D00165EFD /* PlaybackService.swift */,
B0BAAAAC2F321697002A5FBB /* RadioService.swift */,
C456D8F72F2FBD64002AAB8B /* LRCLIBService.swift */,
);
path = Services;
Expand Down Expand Up @@ -287,6 +306,7 @@
C415F54D2C11908100E3E1D2 /* AuthViewModel.swift */,
C4289F472C12391300C3A4FD /* AlbumViewModel.swift */,
C4289F502C139B2E00C3A4FD /* AlbumView.swift */,
B0BAAAA72F32139D002A5FBB /* Radios */,
C4824D262CE908DA003EAB52 /* SongsView.swift */,
C4A4BF3C2C1455A100363290 /* FloatingPlayerView.swift */,
C42E7E172CE7EF4D00505B4E /* PlaylistDetailView.swift */,
Expand Down Expand Up @@ -408,9 +428,12 @@
C4289F4A2C12392B00C3A4FD /* Album.swift in Sources */,
C4824D252CE90872003EAB52 /* ArtistDetailView.swift in Sources */,
C4E8D95E2B763BA900C2353E /* ContentView.swift in Sources */,
B0BAAAAD2F3216A0002A5FBB /* RadioService.swift in Sources */,
C4FE524D2C14E71B0053763A /* KeychainManager.swift in Sources */,
B0BAAAA62F31F0A0002A5FBB /* RadiosView.swift in Sources */,
C4875E022C149DDD00D9BAEB /* AuthService.swift in Sources */,
C4EAA4862C297E35007EB2E0 /* NowPlaying.swift in Sources */,
B0BAAAAB2F3214F7002A5FBB /* Radio.swift in Sources */,
C467AD552D329C8B00644E68 /* AccountLinkStatus.swift in Sources */,
C4120FDD2C15E1C300E712BE /* Song.swift in Sources */,
C467AD532D3267D000644E68 /* Subsonic.swift in Sources */,
Expand Down Expand Up @@ -455,6 +478,7 @@
C429DB322D33C707009F2684 /* DownloadButtonView.swift in Sources */,
C4824D232CE8C41F003EAB52 /* Playable.swift in Sources */,
C41E15132C0F952A005BAE63 /* PlayerViewModel.swift in Sources */,
B0BAAAA92F3213AF002A5FBB /* RadiosViewModel.swift in Sources */,
C4875E042C149F9A00D9BAEB /* APIManager.swift in Sources */,
C4A4BF312C14433D00363290 /* HomeView.swift in Sources */,
C4A4BF392C14445000363290 /* PreferencesView.swift in Sources */,
Expand Down
13 changes: 10 additions & 3 deletions flo/FloatingPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,17 @@ struct FloatingPlayerView: View {
.aspectRatio(contentMode: .fit)
} else {
LazyImage(url: URL(string: viewModel.getAlbumCoverArt())) { state in
if let image = state.image {
image.resizable().aspectRatio(contentMode: .fit)
} else {
if state.isLoading {
Color.gray.opacity(0.3)
} else {
if let image = state.image {
image.resizable().aspectRatio(contentMode: .fit)
} else {
Image("placeholder")
.resizable()
.scaledToFit()
.padding(8)
}
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions flo/Navigation/LibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ struct LibraryView: View {
}

Divider()

NavigationLink {
RadiosView()
.environmentObject(playerViewModel)
} label: {
HStack {
Image(systemName: "radio")
.frame(width: 20, height: 10)
.foregroundColor(.accent)
Text("Radios")
.customFont(.headline)
.padding(.leading, 8)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
.font(.caption)
}.padding(.horizontal).padding(.vertical, 5)
}

Divider()
}

LazyVGrid(columns: columns) {
Expand Down
34 changes: 22 additions & 12 deletions flo/PlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ struct PlayerView: View {
.padding(.top, 20)

Spacer()

if let image = UIImage(contentsOfFile: viewModel.getAlbumCoverArt()) {
let coverArtUrl = viewModel.getAlbumCoverArt()
if let image = UIImage(contentsOfFile: coverArtUrl) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
Expand All @@ -244,21 +244,31 @@ struct PlayerView: View {
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
} else {
LazyImage(url: URL(string: viewModel.getAlbumCoverArt())) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: imageSize, height: imageSize)
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
} else {
LazyImage(url: URL(string: coverArtUrl)) { state in
if state.isLoading {
Color.gray.opacity(0.3)
.frame(width: imageSize, height: imageSize)
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
} else {
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: imageSize, height: imageSize)
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
} else if state.error != nil {
Image("placeholder")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: imageSize, height: imageSize)
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
}
}
}
}
Expand Down
47 changes: 47 additions & 0 deletions flo/PlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,53 @@ class PlayerViewModel: ObservableObject {

self.addToQueue(idx: 0, item: queue)
}

func playRadioItem(radio: Radio) {
let item = radio.toPlayable()
let queue = PlaybackService.shared.addToQueue(item: item, isFromLocal: false)

self.addToQueue(idx: 0, item: queue)
self.shouldHidePlayer = false
self.isLocallySaved = false

self.resetLyrics()

guard let radioUrl = URL(string: radio.streamUrl) else {
self.isMediaLoading = false
self.isMediaFailed = true
return
}
self.playerItem = AVPlayerItem(url: radioUrl)
self.player?.replaceCurrentItem(with: self.playerItem)

self.playerItemObservation = self.playerItem?.publisher(for: \.status)
.sink { [weak self] status in
guard let self = self else { return }
switch status {
case .readyToPlay:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isMediaLoading = false
self.isMediaFailed = false
}
case .failed:
self.isMediaLoading = false
self.isMediaFailed = true
case .unknown:
self.isMediaLoading = false
@unknown default:
self.isMediaLoading = true
}
}

self.play()

self.initNowPlayingInfo(
title: item.name,
artist: item.artist,
playbackDuration: 0)
PlaybackService.shared.clearQueue()
UserDefaultsManager.removeObject(key: UserDefaultsKeys.nowPlayingProgress)
}

func shuffleItem<T: Playable>(item: T, isFromLocal: Bool) {
var shuffledItem = item
Expand Down
78 changes: 78 additions & 0 deletions flo/Radios/RadiosView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

//
// SongsView.swift
// flo
//
//

import NukeUI
import SwiftUI

struct RadiosView: View {
@EnvironmentObject private var playerViewModel: PlayerViewModel

@StateObject var viewModel = RadiosViewModel()
@State private var searchRadio = ""

var filteredRadios: [Radio] {
if searchRadio.isEmpty {
return viewModel.radios
} else {
return viewModel.radios.filter { radio in
radio.name.localizedCaseInsensitiveContains(searchRadio)
}
}
}

var body: some View {
ScrollView {
LazyVStack {
ForEach(filteredRadios, id: \.id) { radio in
Group {
HStack {
Color("PlayerColor").frame(width: 40, height: 40)
.cornerRadius(5)
.overlay {
Image(systemName: "dot.radiowaves.up.forward")
.resizable()
.scaledToFit()
.foregroundStyle(.white)
.padding(8)
}

VStack(alignment: .leading) {
Text(radio.name)
.customFont(.headline)
.multilineTextAlignment(.leading)
.lineLimit(2)
.padding(.bottom, 3)
}
.padding(.horizontal, 10)

Spacer()
}
.padding(.horizontal)
.background(Color(UIColor.systemBackground))

Divider()
}
.onTapGesture {
playerViewModel.playRadioItem(radio: radio)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.top, 10)
}
.navigationTitle("Radios")
.navigationBarTitleDisplayMode(.large)
.searchable(
text: $searchRadio,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search"
)
.onAppear {
viewModel.fetchAllRadios()
}
}
}
29 changes: 29 additions & 0 deletions flo/Radios/RadiosViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// flo

import Foundation
import Combine

class RadiosViewModel: ObservableObject {
@Published var radios: [Radio] = []

@Published var isLoading = false
@Published var error: Error?

func fetchAllRadios() {
RadioService.shared.getAllRadios { result in
self.isLoading = true

DispatchQueue.main.async {
self.isLoading = false

switch result {
case .success(let radios):
self.radios = radios

case .failure(let error):
self.error = error
}
}
}
}
}
3 changes: 3 additions & 0 deletions flo/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,9 @@
}
}
}
},
"Radios" : {

},
"Redownload Album" : {
"localizations" : {
Expand Down
Loading