fully bound tag/playlist views to same environment object authoritative source, added pull to refresh on object views, added footer logo in settings

This commit is contained in:
aj 2020-03-03 00:04:20 +00:00
parent 6318ab4f16
commit a906b5396a
15 changed files with 227 additions and 145 deletions

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
E92F94822401412100B6B721 /* SwiftUIRefresh in Frameworks */ = {isa = PBXBuildFile; productRef = E92F94812401412100B6B721 /* SwiftUIRefresh */; };
E934AC99240DD0E4009869F4 /* AddTagSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E934AC98240DD0E4009869F4 /* AddTagSheet.swift */; };
E97AF45623FC4E7800635494 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF45523FC4E7800635494 /* User.swift */; };
E97AF45923FC50EC00635494 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E97AF45823FC50EC00635494 /* SwiftyJSON */; };
E97AF45B23FC748D00635494 /* UserApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF45A23FC748D00635494 /* UserApi.swift */; };
@ -58,6 +59,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
E934AC98240DD0E4009869F4 /* AddTagSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTagSheet.swift; sourceTree = "<group>"; };
E97AF45523FC4E7800635494 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
E97AF45A23FC748D00635494 /* UserApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserApi.swift; sourceTree = "<group>"; };
E97AF45F23FC85D600635494 /* PlaylistApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistApi.swift; sourceTree = "<group>"; };
@ -192,6 +194,7 @@
E9E30C2923FEAA3A00574EEF /* TagRow.swift */,
E9E30C2C23FEAB0200574EEF /* TagView.swift */,
E9E30C3023FEAF2B00574EEF /* TagObjList.swift */,
E934AC98240DD0E4009869F4 /* AddTagSheet.swift */,
);
path = Tag;
sourceTree = "<group>";
@ -409,6 +412,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E934AC99240DD0E4009869F4 /* AddTagSheet.swift in Sources */,
E9E30C2D23FEAB0200574EEF /* TagView.swift in Sources */,
E9E30C2823FEA6BD00574EEF /* Tag.swift in Sources */,
E9E30C3123FEAF2B00574EEF /* TagObjList.swift in Sources */,

View File

@ -22,19 +22,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Create the SwiftUI view that provides the window contents.
let contentView = RootView()
let liveUser = LiveUser(playlists: [], tags: [])
let keychain = Keychain(service: "xyz.sarsoo.music.login")
// debugPrint(keychain["username"] ?? "no username")
// debugPrint(keychain["password"] ?? "no password")
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
var controller: UIViewController
if keychain["username"] != nil && keychain["password"] != nil {
let liveUser = LiveUser(playlists: [], tags: [], username: keychain["username"]!)
controller = UIHostingController(rootView: contentView.environmentObject(liveUser))
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ap.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

View File

@ -27,7 +27,8 @@ class LoginController: UIViewController, UITextFieldDelegate {
// MARK: Actions
@IBSegueAction func returnUIView(_ coder: NSCoder) -> UIViewController? {
let liveUser = LiveUser(playlists: [], tags: [])
// TODO add right username
let liveUser = LiveUser(playlists: [], tags: [], username: "")
return UIHostingController(coder: coder, rootView: RootView().environmentObject(liveUser))
}

View File

@ -12,10 +12,12 @@ class LiveUser: ObservableObject {
@Published var playlists: [Playlist]
@Published var tags: [Tag]
@Published var username: String
init(playlists: [Playlist], tags: [Tag]) {
init(playlists: [Playlist], tags: [Tag], username: String) {
self.playlists = playlists
self.tags = tags
self.username = username
}
func updatePlaylist(playlistIn: Playlist) {

View File

@ -16,6 +16,7 @@ public enum TagApi {
case updateTag(tag_id: String, updates: JSON)
case deleteTag(tag_id: String)
case newTag(tag_id: String)
case getTag(tag_id: String)
}
extension TagApi: ApiRequest {
@ -35,6 +36,8 @@ extension TagApi: ApiRequest {
return "api/tag/\(tag_id)"
case .newTag(let tag_id):
return "api/tag/\(tag_id)"
case .getTag(let tag_id):
return "api/tag/\(tag_id)"
}
}
@ -50,6 +53,8 @@ extension TagApi: ApiRequest {
return .delete
case .newTag:
return .post
case .getTag:
return .get
}
}
@ -59,12 +64,14 @@ extension TagApi: ApiRequest {
return nil
case .runTag:
return nil
case .updateTag(let tag_id, let updates):
case .updateTag(let _, let updates):
return updates
case .deleteTag:
return nil
case .newTag:
return nil
case .getTag:
return nil
}
}
@ -80,6 +87,8 @@ extension TagApi: ApiRequest {
return nil
case .newTag:
return nil
case .getTag:
return nil
}
}

View File

@ -18,6 +18,7 @@ struct AddPlaylistSheet: View {
@Binding var state: Bool
@Binding var playlists: Array<Playlist>
@Binding var username: String
var body: some View {
VStack {
@ -73,10 +74,26 @@ struct AddPlaylistSheet: View {
return
}
var playlist: Playlist? = nil
switch PlaylistType(rawValue: selectedType) ?? .defaultPlaylist {
case .defaultPlaylist:
playlist = Playlist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false)
break
case .recents:
playlist = RecentsPlaylist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false, add_last_month: false, add_this_month: false, day_boundary: 14)
break
case .fmchart:
playlist = LastFMChartPlaylist(name: name, uri: "", username: username, include_recommendations: false, recommendation_sample: 10, include_library_tracks: false, parts: [], playlist_references: [], shuffle: false, chart_range: .month, chart_limit: 10)
break
}
isLoading = true
let api = PlaylistApi.newPlaylist(name: self.name,
type: PlaylistType(rawValue: selectedType) ?? .defaultPlaylist)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
self.playlists.append(playlist!)
self.playlists = self.playlists.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
self.isLoading = false
self.state = false
}
@ -85,6 +102,6 @@ struct AddPlaylistSheet: View {
struct AddPlaylistSheet_Previews: PreviewProvider {
static var previews: some View {
AddPlaylistSheet(state: .constant(true), playlists: .constant([]))
AddPlaylistSheet(state: .constant(true), playlists: .constant([]), username: .constant("username"))
}
}

View File

@ -10,13 +10,10 @@ import SwiftUI
import SwiftyJSON
struct PlaylistRow: View {
@EnvironmentObject var liveUser: LiveUser
var playlist: Playlist
@Binding var playlist: Playlist
var body: some View {
NavigationLink(destination: PlaylistView(playlist: playlist)){
NavigationLink(destination: PlaylistView(playlist: $playlist)){
HStack {
Text(playlist.name)
.contextMenu {
@ -50,18 +47,7 @@ struct PlaylistRow: View {
struct PlaylistRow_Previews: PreviewProvider {
static var previews: some View {
PlaylistRow(playlist:
Playlist(name: "playlist name",
uri: "uri",
username: "username",
include_recommendations: true,
recommendation_sample: 5,
include_library_tracks: true,
parts: ["name"],
playlist_references: ["ref name"],
shuffle: true)
.constant(Playlist(name: "", uri: "", username: "", include_recommendations: true, recommendation_sample: 1, include_library_tracks: true, parts: [], playlist_references: [], shuffle: true))
)
}
}

View File

@ -7,41 +7,14 @@
//
import SwiftUI
//import SwiftUIRefresh
import SwiftUIRefresh
import SwiftyJSON
final class ChangeableBool: ObservableObject {
var onClick: () -> ()
init(onClick: @escaping () -> ()) {
self.onClick = onClick
}
@Published var state: Bool = false {
didSet {
self.onClick()
}
}
}
struct PlaylistView: View {
@EnvironmentObject var liveUser: LiveUser
init(playlist: Playlist) {
self.playlist = playlist
// hide empty items below list
UITableView.appearance().tableFooterView = UIView()
}
var playlist: Playlist
@State private var recommendations: Bool = true
@State private var library_Tracks: Bool = false
@State private var shuffle: Bool = false
@State private var rec_num: Int = 0
@Binding var playlist: Playlist
@State private var this_month: Bool = false
@State private var last_month: Bool = false
@ -55,33 +28,33 @@ struct PlaylistView: View {
var body: some View {
List {
Section(header: Text("Options")){
Toggle(isOn: $recommendations) {
Toggle(isOn: self.$playlist.include_recommendations) {
Text("Spotify Recommendations")
}
// if recommendations {
if self.playlist.include_recommendations {
Stepper(onIncrement: {
self.$rec_num.wrappedValue += 1
self.updatePlaylist(updates: JSON(["recommendation_sample": self.$rec_num.wrappedValue]))
self.$playlist.recommendation_sample.wrappedValue += 1
self.updatePlaylist(updates: JSON(["recommendation_sample": self.playlist.recommendation_sample]))
},
onDecrement: {
self.$rec_num.wrappedValue -= 1
self.updatePlaylist(updates: JSON(["recommendation_sample": self.$rec_num.wrappedValue]))
self.$playlist.recommendation_sample.wrappedValue -= 1
self.updatePlaylist(updates: JSON(["recommendation_sample": self.playlist.recommendation_sample]))
}){
Text("#:")
.foregroundColor(Color.gray)
.multilineTextAlignment(.trailing)
Text("\(rec_num)")
Text("\(self.playlist.recommendation_sample)")
.multilineTextAlignment(.trailing)
}
// }
}
Toggle(isOn: $library_Tracks) {
Toggle(isOn: self.$playlist.include_library_tracks) {
Text("Library Tracks")
}
Toggle(isOn: $shuffle) {
Toggle(isOn: self.$playlist.shuffle) {
Text("Shuffle")
}
@ -147,17 +120,13 @@ struct PlaylistView: View {
}
}
}
// .pullToRefresh(isShowing: $isRefreshing) {
// self.refreshPlaylist()
// }
.pullToRefresh(isShowing: $isRefreshing) {
self.refreshPlaylist()
}
.navigationBarTitle(Text(playlist.name))
.onAppear {
self.$recommendations.wrappedValue = self.playlist.include_recommendations
self.$library_Tracks.wrappedValue = self.playlist.include_library_tracks
self.$shuffle.wrappedValue = self.playlist.shuffle
self.$rec_num.wrappedValue = self.playlist.recommendation_sample
// TODO are these binding properly?
if let playlist = self.playlist as? RecentsPlaylist {
self.$this_month.wrappedValue = playlist.add_this_month
self.$last_month.wrappedValue = playlist.add_last_month
@ -204,7 +173,7 @@ struct PlaylistView: View {
//TODO: do better error checking
}
func refreshPlaylist(updates: JSON) {
func refreshPlaylist() {
let api = PlaylistApi.getPlaylist(name: self.playlist.name)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
guard let data = response.data else {
@ -214,11 +183,8 @@ struct PlaylistView: View {
guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse")
}
// let playlist = Playlist.fromDict(json["playlist"])
//
// self.playlist = playlist
// self.isRefreshing = false
self.playlist = Playlist.fromDict(dictionary: json)!
self.isRefreshing = false
}
//TODO: do better error checking
}
@ -226,7 +192,7 @@ struct PlaylistView: View {
struct PlaylistView_Previews: PreviewProvider {
static var previews: some View {
PlaylistView(playlist:
PlaylistView(playlist: .constant(
Playlist(name: "playlist name",
uri: "uri",
username: "username",
@ -239,6 +205,6 @@ struct PlaylistView_Previews: PreviewProvider {
playlist_references: ["ref name"],
shuffle: true)
)
))
}
}

View File

@ -19,30 +19,21 @@ struct RootView: View {
@State private var showAdd = false // State for showing add modal view
@State private var justDeletedPlaylists: Array<Playlist> = [] // Cache of recently deleted playlists for removing from next net request
@State private var justDeletedTags: Array<Tag> = []
@State private var isRefreshingPlaylists = false
@State private var isRefreshingTags = false
// refresh playlist list on interval
// let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
TabView {
// PLAYLISTS
NavigationView {
List{
ForEach(liveUser.playlists) { playlist in
PlaylistRow(playlist: playlist)
ForEach(liveUser.playlists.indices, id:\.self) { idx in
PlaylistRow(playlist: self.$liveUser.playlists[idx])
}
.onDelete { indexSet in
indexSet.forEach { index in
// add to recently deleted playlist cache
self.justDeletedPlaylists.append(self.liveUser.playlists[index])
let api = PlaylistApi.deletePlaylist(name: self.liveUser.playlists[index].name)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
@ -63,7 +54,7 @@ struct RootView: View {
action: { self.showAdd = true },
label: { Text("Add") }
).sheet(isPresented: $showAdd) {
AddPlaylistSheet(state: self.$showAdd, playlists: self.$liveUser.playlists)
AddPlaylistSheet(state: self.$showAdd, playlists: self.$liveUser.playlists, username: self.$liveUser.username)
}
)
}
@ -78,15 +69,12 @@ struct RootView: View {
// TAGS
NavigationView {
List{
ForEach(liveUser.tags) { tag in
TagRow(tag: tag)
ForEach(liveUser.tags.indices, id:\.self) { idx in
TagRow(tag: self.$liveUser.tags[idx])
}
.onDelete { indexSet in
indexSet.forEach { index in
// add to recently deleted playlist cache
self.justDeletedTags.append(self.liveUser.tags[index])
let api = TagApi.deleteTag(tag_id: self.liveUser.tags[index].tag_id)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
@ -107,7 +95,7 @@ struct RootView: View {
action: { self.showAdd = true },
label: { Text("Add") }
).sheet(isPresented: $showAdd) {
AddPlaylistSheet(state: self.$showAdd, playlists: self.$liveUser.playlists)
AddTagSheet(state: self.$showAdd, tags: self.$liveUser.tags, username: self.$liveUser.username)
}
)
}
@ -130,9 +118,6 @@ struct RootView: View {
}
}
.tag(2)
// .onReceive(timer) { _ in
// self.fetch()
// }
}.onAppear {
self.fetchAll()
}
@ -143,7 +128,7 @@ struct RootView: View {
refreshTags()
}
func refreshPlaylists() {
public func refreshPlaylists() {
let api = PlaylistApi.getPlaylists
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
@ -162,19 +147,6 @@ struct RootView: View {
})
// sort
.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
// filter playlists for those recently deleted
.filter { (rxPlaylist) -> Bool in
var deleted = false
for playlist in self.justDeletedPlaylists {
if playlist == rxPlaylist {
deleted = true
}
}
return !deleted
}
// clear cache of recently deleted playlists
self.justDeletedPlaylists = []
// update state
self.liveUser.playlists = playlists
@ -183,7 +155,7 @@ struct RootView: View {
//TODO: do better error checking
}
func refreshTags() {
public func refreshTags() {
let tagApi = TagApi.getTags
RequestBuilder.buildRequest(apiRequest: tagApi).responseJSON{ response in
@ -202,19 +174,6 @@ struct RootView: View {
})
// sort
.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
// filter playlists for those recently deleted
.filter { (rxTag) -> Bool in
var deleted = false
for tag in self.justDeletedTags {
if tag == rxTag {
deleted = true
}
}
return !deleted
}
// clear cache of recently deleted playlists
self.justDeletedTags = []
// update state
self.liveUser.tags = tags

View File

@ -10,8 +10,21 @@ import SwiftUI
import KeychainAccess
struct SettingsList: View {
init(){
UITableView.appearance().tableFooterView = UIView()
}
var body: some View {
VStack{
List{
Button(action: {
if let url = URL(string: "https://music.sarsoo.xyz") {
UIApplication.shared.open(url)
}
}) {
Text("Open Web")
}
Button(action: {
let keychain = Keychain(service: "xyz.sarsoo.music.login")
do {
@ -24,6 +37,11 @@ struct SettingsList: View {
Text("Log out")
}
}
Image("APFooter")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100.0)
}
}
}

View File

@ -0,0 +1,90 @@
//
// AddTagSheet.swift
// Music Tools
//
// Created by Andy Pack on 02/03/2020.
// Copyright © 2020 Sarsoo. All rights reserved.
//
import SwiftUI
import SwiftyJSON
struct AddTagSheet: View {
@State private var name = ""
@State private var errorMessage = ""
@State private var isLoading = false
@Binding var state: Bool
@Binding var tags: Array<Tag>
@Binding var username: String
var body: some View {
VStack {
HStack(alignment: .center) {
Text("New Tag")
.font(.largeTitle)
.multilineTextAlignment(.center)
.padding([.top, .leading, .trailing], 20.0)
}
TextField("Name", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding([.bottom, .leading, .trailing], 20.0)
Button(action: create){
Text("Add")
.font(.title)
}
.disabled(isLoading)
.padding()
Text(errorMessage)
.foregroundColor(Color.red)
.padding()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
}
func create(){
debugPrint(name)
let tag_id = self.$name.wrappedValue.replacingOccurrences(of: " ", with: "_")
if tag_id.count == 0 {
errorMessage = "Enter Tag Name"
return
}
var tagPresent = false
for tag in tags {
if tag.tag_id == tag_id {
tagPresent = true
break
}
}
if tagPresent == true {
errorMessage = "Tag already created"
return
}
let tag = Tag(tag_id: tag_id, name: name, username: self.username, tracks: [], albums: [], artists: [], count: 0, proportion: 0.0, total_user_scrobbles: 0, last_updated: "Never")
isLoading = true
let api = TagApi.newTag(tag_id: tag_id)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
self.tags.append(tag)
self.tags = self.tags.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
self.isLoading = false
self.state = false
}
}
}
struct AddTagSheet_Previews: PreviewProvider {
static var previews: some View {
AddTagSheet(state: .constant(true), tags: .constant([]), username: .constant("username"))
}
}

View File

@ -11,12 +11,10 @@ import SwiftyJSON
struct TagRow: View {
@EnvironmentObject var liveUser: LiveUser
var tag: Tag
@Binding var tag: Tag
var body: some View {
NavigationLink(destination: TagView(tag: tag)){
NavigationLink(destination: TagView(tag: $tag)){
HStack {
Text(tag.name)
.contextMenu {
@ -39,7 +37,7 @@ struct TagRow: View {
struct TagRow_Previews: PreviewProvider {
static var previews: some View {
TagRow(tag:
TagRow(tag: .constant(
Tag(tag_id: "tag_id",
name: "tag name",
username: "andy",
@ -53,6 +51,6 @@ struct TagRow_Previews: PreviewProvider {
total_user_scrobbles: 2000,
last_updated: "10th Feb")
)
))
}
}

View File

@ -7,18 +7,14 @@
//
import SwiftUI
import SwiftUIRefresh
import SwiftyJSON
struct TagView: View {
init(tag: Tag) {
self.tag = tag
@Binding var tag: Tag
// hide empty items below list
UITableView.appearance().tableFooterView = UIView()
}
var tag: Tag
@State private var isRefreshing = false
var body: some View {
List {
@ -82,6 +78,9 @@ struct TagView: View {
}
}
}
.pullToRefresh(isShowing: $isRefreshing) {
self.refreshTag()
}
.navigationBarTitle(Text(tag.name))
.onAppear {
@ -103,11 +102,27 @@ struct TagView: View {
}
//TODO: do better error checking
}
func refreshTag() {
let api = TagApi.getTag(tag_id: self.tag.tag_id)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
guard let data = response.data else {
fatalError("error getting tag")
}
guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse")
}
self.tag = Tag.fromDict(dictionary: json["tag"])
self.isRefreshing = false
}
//TODO: do better error checking
}
}
struct TagView_Previews: PreviewProvider {
static var previews: some View {
TagView(tag:
TagView(tag: .constant(
Tag(tag_id: "tag_id",
name: "tag name",
username: "andy",
@ -121,6 +136,6 @@ struct TagView_Previews: PreviewProvider {
total_user_scrobbles: 2000,
last_updated: "10th Feb")
)
))
}
}