Skip to content
Closed
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
16 changes: 16 additions & 0 deletions flo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,17 @@
C4824D232CE8C41F003EAB52 /* Playable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4824D222CE8C41D003EAB52 /* Playable.swift */; };
C4824D272CE908DC003EAB52 /* SongsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4824D262CE908DA003EAB52 /* SongsView.swift */; };
C4875E002C149D9000D9BAEB /* AlbumService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4875DFF2C149D9000D9BAEB /* AlbumService.swift */; };
C4CACHE012D7B0000003B9C4F /* StreamCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CACHE002D7B0000003B9C4F /* StreamCacheManager.swift */; };
0C1A2B3C4D5E6F7A8B9C0D1E /* LibraryCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3B4C5D6E7F8A9B0C1D2E /* LibraryCacheManager.swift */; };
0D1A2B3C4D5E6F7A8B9C0D1E /* CoverArtCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D2A3B4C5D6E7F8A9B0C1D2E /* CoverArtCacheManager.swift */; };
C4875E022C149DDD00D9BAEB /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4875E012C149DDD00D9BAEB /* AuthService.swift */; };
C4875E042C149F9A00D9BAEB /* APIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4875E032C149F9A00D9BAEB /* APIManager.swift */; };
C49134532C15BE0C00CCF2EB /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49134522C15BE0C00CCF2EB /* Strings.swift */; };
C49495812C1C25E5006B4D1E /* ScanStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49495802C1C25E5006B4D1E /* ScanStatus.swift */; };
C49495852C1C26D4006B4D1E /* ScanStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49495842C1C26D4006B4D1E /* ScanStatusService.swift */; };
C4A4BF312C14433D00363290 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4BF302C14433D00363290 /* HomeView.swift */; };
C4A4BF332C14437700363290 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4BF322C14437700363290 /* LibraryView.swift */; };
02E566BC379A4C25B4AB907C /* CachedSongsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8455A32A2A31C3AA7C4DC99 /* CachedSongsView.swift */; };
C4A4BF372C14442F00363290 /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4BF362C14442F00363290 /* DownloadsView.swift */; };
C4A4BF392C14445000363290 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4BF382C14445000363290 /* PreferencesView.swift */; };
C4A4BF3D2C1455A100363290 /* FloatingPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A4BF3C2C1455A100363290 /* FloatingPlayerView.swift */; };
Expand Down Expand Up @@ -231,6 +235,9 @@
C4824D222CE8C41D003EAB52 /* Playable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playable.swift; sourceTree = "<group>"; };
C4824D262CE908DA003EAB52 /* SongsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongsView.swift; sourceTree = "<group>"; };
C4875DFF2C149D9000D9BAEB /* AlbumService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumService.swift; sourceTree = "<group>"; };
C4CACHE002D7B0000003B9C4F /* StreamCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamCacheManager.swift; sourceTree = "<group>"; };
0C2A3B4C5D6E7F8A9B0C1D2E /* LibraryCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCacheManager.swift; sourceTree = "<group>"; };
0D2A3B4C5D6E7F8A9B0C1D2E /* CoverArtCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverArtCacheManager.swift; sourceTree = "<group>"; };
C4875E012C149DDD00D9BAEB /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
C4875E032C149F9A00D9BAEB /* APIManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIManager.swift; sourceTree = "<group>"; };
C49134522C15BE0C00CCF2EB /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
Expand All @@ -239,6 +246,7 @@
C49495842C1C26D4006B4D1E /* ScanStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanStatusService.swift; sourceTree = "<group>"; };
C4A4BF302C14433D00363290 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
C4A4BF322C14437700363290 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
E8455A32A2A31C3AA7C4DC99 /* CachedSongsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedSongsView.swift; sourceTree = "<group>"; };
C4A4BF362C14442F00363290 /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = "<group>"; };
C4A4BF382C14445000363290 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
C4A4BF3C2C1455A100363290 /* FloatingPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPlayerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -424,6 +432,9 @@
children = (
C4F870CD2CEFCC5B00312F8A /* FloooService.swift */,
C4875DFF2C149D9000D9BAEB /* AlbumService.swift */,
C4CACHE002D7B0000003B9C4F /* StreamCacheManager.swift */,
0C2A3B4C5D6E7F8A9B0C1D2E /* LibraryCacheManager.swift */,
0D2A3B4C5D6E7F8A9B0C1D2E /* CoverArtCacheManager.swift */,
C4875E012C149DDD00D9BAEB /* AuthService.swift */,
C4875E032C149F9A00D9BAEB /* APIManager.swift */,
C4FE524A2C14E1F70053763A /* UserDefaultsManager.swift */,
Expand All @@ -447,6 +458,7 @@
children = (
C4A4BF302C14433D00363290 /* HomeView.swift */,
C4A4BF322C14437700363290 /* LibraryView.swift */,
E8455A32A2A31C3AA7C4DC99 /* CachedSongsView.swift */,
C4A4BF362C14442F00363290 /* DownloadsView.swift */,
C4A4BF382C14445000363290 /* PreferencesView.swift */,
);
Expand Down Expand Up @@ -731,6 +743,7 @@
C415F54E2C11908100E3E1D2 /* AuthViewModel.swift in Sources */,
C42B25842F44551B00E62008 /* PlaybackCoordinator.swift in Sources */,
50C912912F5DD9990087EE61 /* IAPLoginView.swift in Sources */,
02E566BC379A4C25B4AB907C /* CachedSongsView.swift in Sources */,
C4A4BF372C14442F00363290 /* DownloadsView.swift in Sources */,
C4100A692CE78B25001BC9BE /* PlaylistView.swift in Sources */,
C440228D2C09BE2E004EE9CD /* PlayerView.swift in Sources */,
Expand All @@ -741,6 +754,9 @@
C4D7F84F2C7F2C5D00165EFD /* PlaybackService.swift in Sources */,
C49134532C15BE0C00CCF2EB /* Strings.swift in Sources */,
C4875E002C149D9000D9BAEB /* AlbumService.swift in Sources */,
C4CACHE012D7B0000003B9C4F /* StreamCacheManager.swift in Sources */,
0C1A2B3C4D5E6F7A8B9C0D1E /* LibraryCacheManager.swift in Sources */,
0D1A2B3C4D5E6F7A8B9C0D1E /* CoverArtCacheManager.swift in Sources */,
C456D8FC2F2FF39B002AAB8B /* LyricsLine.swift in Sources */,
C429DB322D33C707009F2684 /* DownloadButtonView.swift in Sources */,
C4824D232CE8C41F003EAB52 /* Playable.swift in Sources */,
Expand Down
131 changes: 90 additions & 41 deletions flo/AlbumViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,24 +97,78 @@ class AlbumViewModel: ObservableObject {
}
}

func fetchAllSongs() {
AlbumService.shared.getAllSongs { result in
self.isLoading = true
// MARK: - Generic cache helpers

DispatchQueue.main.async {
self.isLoading = false
private enum CacheKey: String {
case albums, artists, playlists, songs
}

private func fetchCached<T: Codable>(
current: [T],
cacheKey: CacheKey,
showsLoading: Bool = false,
assign: @escaping ([T]) -> Void,
request: @escaping (@escaping (Result<[T], Error>) -> Void) -> Void
) {
if current.isEmpty,
let cached = LibraryCacheManager.shared.load([T].self, forKey: cacheKey.rawValue)
{
assign(cached)
}
if showsLoading { isLoading = true }
request { result in
DispatchQueue.main.async {
if showsLoading { self.isLoading = false }
switch result {
case .success(let songs):
self.songs = songs

case .success(let items):
assign(items)
if !items.isEmpty {
DispatchQueue.global(qos: .utility).async {
LibraryCacheManager.shared.save(items, forKey: cacheKey.rawValue)
}
}
case .failure(let error):
self.error = error
}
}
}
}

@MainActor
private func refreshCached<T: Codable>(
cacheKey: CacheKey,
assign: @escaping ([T]) -> Void,
request: @escaping (@escaping (Result<[T], Error>) -> Void) -> Void
) async {
isLoading = true
defer { isLoading = false }
await withCheckedContinuation { continuation in
request { result in
DispatchQueue.main.async {
switch result {
case .success(let items):
assign(items)
if !items.isEmpty {
DispatchQueue.global(qos: .utility).async {
LibraryCacheManager.shared.save(items, forKey: cacheKey.rawValue)
}
}
case .failure(let error):
self.error = error
}
continuation.resume()
}
}
}
}

// MARK: - Fetch methods

func fetchAllSongs() {
fetchCached(current: songs, cacheKey: .songs,
assign: { self.songs = $0 }, request: AlbumService.shared.getAllSongs)
}

func getAlbumInfo() {
AlbumService.shared.getAlbumInfo(id: self.album.id) { result in
DispatchQueue.main.async {
Expand Down Expand Up @@ -275,19 +329,8 @@ class AlbumViewModel: ObservableObject {
}

func fetchAlbums() {
isLoading = true
AlbumService.shared.getAlbum { result in
DispatchQueue.main.async {
self.isLoading = false
switch result {
case .success(let albums):
self.albums = albums
case .failure(let error):
print("error>>>>", error)
self.error = error
}
}
}
fetchCached(current: albums, cacheKey: .albums, showsLoading: true,
assign: { self.albums = $0 }, request: AlbumService.shared.getAlbum)
}

func fetchAlbumsByArtist(id: String) {
Expand Down Expand Up @@ -326,29 +369,35 @@ class AlbumViewModel: ObservableObject {
}

func getPlaylists() {
AlbumService.shared.getPlaylists { result in
DispatchQueue.main.async {
switch result {
case .success(let playlists):
self.playlists = playlists
case .failure(let error):
self.error = error
}
}
}
fetchCached(current: playlists, cacheKey: .playlists,
assign: { self.playlists = $0 }, request: AlbumService.shared.getPlaylists)
}

func getArtists() {
AlbumService.shared.getArtists { result in
DispatchQueue.main.async {
switch result {
case .success(let artists):
self.artists = artists
case .failure(let error):
self.error = error
}
}
}
fetchCached(current: artists, cacheKey: .artists,
assign: { self.artists = $0 }, request: AlbumService.shared.getArtists)
}

// MARK: - Async refresh variants

@MainActor func refreshAlbums() async {
await refreshCached(cacheKey: .albums, assign: { self.albums = $0 },
request: AlbumService.shared.getAlbum)
}

@MainActor func refreshArtists() async {
await refreshCached(cacheKey: .artists, assign: { self.artists = $0 },
request: AlbumService.shared.getArtists)
}

@MainActor func refreshPlaylists() async {
await refreshCached(cacheKey: .playlists, assign: { self.playlists = $0 },
request: AlbumService.shared.getPlaylists)
}

@MainActor func refreshAllSongs() async {
await refreshCached(cacheKey: .songs, assign: { self.songs = $0 },
request: AlbumService.shared.getAllSongs)
}

func fetchDownloadedAlbums() {
Expand Down
4 changes: 4 additions & 0 deletions flo/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import UIKit
struct FloApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() {
StreamCacheManager.shared.reconcile()
}

var body: some Scene {
WindowGroup {
ContentView()
Expand Down
3 changes: 3 additions & 0 deletions flo/Artists/ArtistsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ struct ArtistsView: View {
}.padding(.bottom, 100)
}
.navigationTitle("Artists")
.refreshable {
await viewModel.refreshArtists()
}
.searchable(
text: $searchArtist, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search"
)
Expand Down
3 changes: 3 additions & 0 deletions flo/FloooViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class FloooViewModel: ObservableObject {
@Published var downloadedSongs: Int = 0

@Published var localDirectorySize: String = "0 MB"
@Published var streamCacheSize: String = "0 MB"

@Published var stats: Stats?
@Published var totalPlay: Int = 0
Expand Down Expand Up @@ -60,9 +61,11 @@ class FloooViewModel: ObservableObject {
Task {
do {
let calculateDirectorySize = try await LocalFileManager.shared.calculateDirectorySize()
let cacheSize = await StreamCacheManager.shared.calculateCacheSize()

await MainActor.run {
self.localDirectorySize = calculateDirectorySize
self.streamCacheSize = bytesToMBOrGB(cacheSize)
}
} catch {
print("Error: \(error)")
Expand Down
73 changes: 73 additions & 0 deletions flo/Navigation/CachedSongsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// CachedSongsView.swift
// flo
//

import NukeUI
import SwiftUI

struct CachedSongsView: View {
@ObservedObject var viewModel: AlbumViewModel
@EnvironmentObject private var playerViewModel: PlayerViewModel

let songs: [Song]

var body: some View {
ScrollView {
LazyVStack {
ForEach(Array(songs.enumerated()), id: \.element.id) { idx, song in
VStack {
HStack {
LazyImage(url: URL(string: viewModel.getAlbumCoverArt(id: song.albumId))) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.clipShape(
RoundedRectangle(cornerRadius: 10, style: .continuous)
)
} else {
Color("PlayerColor").frame(width: 60, height: 60)
.cornerRadius(5)
}
}

VStack(alignment: .leading) {
Text(song.title)
.customFont(.headline)
.multilineTextAlignment(.leading)
.lineLimit(2)
.padding(.bottom, 3)

Text(song.artist)
.customFont(.subheadline)
.foregroundColor(.gray)
.lineLimit(2)
.multilineTextAlignment(.leading)
}
.padding(.horizontal, 10)

Spacer()

Text(timeString(for: song.duration)).customFont(.caption1)
}
.padding(.horizontal)
.background(Color(UIColor.systemBackground))

Divider()
}
.onTapGesture {
let cached = SongCollection(id: "cached-songs", name: "Cached", songs: songs)
playerViewModel.playBySong(idx: idx, item: cached, isFromLocal: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.top, 10)
.padding(
.bottom, playerViewModel.hasNowPlaying() && !playerViewModel.shouldHidePlayer ? 100 : 0)
}
.navigationTitle("Cached")
}
}
Loading