initial notifications support

This commit is contained in:
Andy Pack 2022-12-09 08:51:42 +00:00
parent a3b1a67b5b
commit 8f103fab3e
Signed by: sarsoo
GPG Key ID: A55BA3536A5E0ED7
26 changed files with 480 additions and 165 deletions

View File

@ -10,6 +10,10 @@
A10C8D29281302050018AE12 /* ToastUI in Frameworks */ = {isa = PBXBuildFile; productRef = A10C8D28281302050018AE12 /* ToastUI */; }; A10C8D29281302050018AE12 /* ToastUI in Frameworks */ = {isa = PBXBuildFile; productRef = A10C8D28281302050018AE12 /* ToastUI */; };
A11AC70628A188AE00645043 /* AuthApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11AC70528A188AE00645043 /* AuthApi.swift */; }; A11AC70628A188AE00645043 /* AuthApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11AC70528A188AE00645043 /* AuthApi.swift */; };
A13C54972928FD7C0034F233 /* ManagedInputList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13C54962928FD7C0034F233 /* ManagedInputList.swift */; }; A13C54972928FD7C0034F233 /* ManagedInputList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13C54962928FD7C0034F233 /* ManagedInputList.swift */; };
A15D257A293421350049055E /* StaticNotif.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D2579293421350049055E /* StaticNotif.swift */; };
A15D257C293425390049055E /* NotificationsControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D257B293425390049055E /* NotificationsControls.swift */; };
A15D257E29342E4F0049055E /* APNSHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D257D29342E4F0049055E /* APNSHandler.swift */; };
A15D258029342EF50049055E /* NetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15D257F29342EF50049055E /* NetworkHelper.swift */; };
A1AF726F28A84F7D00D317C9 /* AdminApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF726E28A84F7D00D317C9 /* AdminApi.swift */; }; A1AF726F28A84F7D00D317C9 /* AdminApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF726E28A84F7D00D317C9 /* AdminApi.swift */; };
A1AF727128A850AE00D317C9 /* UsersList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF727028A850AE00D317C9 /* UsersList.swift */; }; A1AF727128A850AE00D317C9 /* UsersList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF727028A850AE00D317C9 /* UsersList.swift */; };
A1AF727328A9062600D317C9 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF727228A9062600D317C9 /* UserView.swift */; }; A1AF727328A9062600D317C9 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AF727228A9062600D317C9 /* UserView.swift */; };
@ -71,6 +75,10 @@
A11AC70528A188AE00645043 /* AuthApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthApi.swift; sourceTree = "<group>"; }; A11AC70528A188AE00645043 /* AuthApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthApi.swift; sourceTree = "<group>"; };
A13C54962928FD7C0034F233 /* ManagedInputList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedInputList.swift; sourceTree = "<group>"; }; A13C54962928FD7C0034F233 /* ManagedInputList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedInputList.swift; sourceTree = "<group>"; };
A146915A28118F940052999D /* Mixonomer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mixonomer.entitlements; sourceTree = "<group>"; }; A146915A28118F940052999D /* Mixonomer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mixonomer.entitlements; sourceTree = "<group>"; };
A15D2579293421350049055E /* StaticNotif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticNotif.swift; sourceTree = "<group>"; };
A15D257B293425390049055E /* NotificationsControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsControls.swift; sourceTree = "<group>"; };
A15D257D29342E4F0049055E /* APNSHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSHandler.swift; sourceTree = "<group>"; };
A15D257F29342EF50049055E /* NetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHelper.swift; sourceTree = "<group>"; };
A1AF726E28A84F7D00D317C9 /* AdminApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminApi.swift; sourceTree = "<group>"; }; A1AF726E28A84F7D00D317C9 /* AdminApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminApi.swift; sourceTree = "<group>"; };
A1AF727028A850AE00D317C9 /* UsersList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersList.swift; sourceTree = "<group>"; }; A1AF727028A850AE00D317C9 /* UsersList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersList.swift; sourceTree = "<group>"; };
A1AF727228A9062600D317C9 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = "<group>"; }; A1AF727228A9062600D317C9 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = "<group>"; };
@ -143,6 +151,15 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
A15D2578293421250049055E /* Notifications */ = {
isa = PBXGroup;
children = (
A15D2579293421350049055E /* StaticNotif.swift */,
A15D257D29342E4F0049055E /* APNSHandler.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
A1DBCDA428A5184D002CF730 /* Admin */ = { A1DBCDA428A5184D002CF730 /* Admin */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -171,9 +188,10 @@
E97AF45F23FC85D600635494 /* PlaylistApi.swift */, E97AF45F23FC85D600635494 /* PlaylistApi.swift */,
E97AF45A23FC748D00635494 /* UserApi.swift */, E97AF45A23FC748D00635494 /* UserApi.swift */,
E9E30C2523FEA4EF00574EEF /* TagApi.swift */, E9E30C2523FEA4EF00574EEF /* TagApi.swift */,
E906F7F32414019C004E1E31 /* NetworkPersister.swift */,
A11AC70528A188AE00645043 /* AuthApi.swift */, A11AC70528A188AE00645043 /* AuthApi.swift */,
A1AF726E28A84F7D00D317C9 /* AdminApi.swift */, A1AF726E28A84F7D00D317C9 /* AdminApi.swift */,
E906F7F32414019C004E1E31 /* NetworkPersister.swift */,
A15D257F29342EF50049055E /* NetworkHelper.swift */,
); );
path = Network; path = Network;
sourceTree = "<group>"; sourceTree = "<group>";
@ -230,6 +248,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E9E30C3223FF255C00574EEF /* SettingsList.swift */, E9E30C3223FF255C00574EEF /* SettingsList.swift */,
A15D257B293425390049055E /* NotificationsControls.swift */,
); );
path = Settings; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -257,6 +276,7 @@
E9EA690923F9A5430012C3E8 /* Mixonomer */ = { E9EA690923F9A5430012C3E8 /* Mixonomer */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A15D2578293421250049055E /* Notifications */,
A146915A28118F940052999D /* Mixonomer.entitlements */, A146915A28118F940052999D /* Mixonomer.entitlements */,
E98254C623FA25280056D9D3 /* Application */, E98254C623FA25280056D9D3 /* Application */,
E9EA691023F9A54A0012C3E8 /* Assets.xcassets */, E9EA691023F9A54A0012C3E8 /* Assets.xcassets */,
@ -448,6 +468,7 @@
E98254CA23FA26600056D9D3 /* PlaylistRow.swift in Sources */, E98254CA23FA26600056D9D3 /* PlaylistRow.swift in Sources */,
E9EA690B23F9A5430012C3E8 /* AppDelegate.swift in Sources */, E9EA690B23F9A5430012C3E8 /* AppDelegate.swift in Sources */,
E906F7F42414019C004E1E31 /* NetworkPersister.swift in Sources */, E906F7F42414019C004E1E31 /* NetworkPersister.swift in Sources */,
A15D258029342EF50049055E /* NetworkHelper.swift in Sources */,
A1AF727328A9062600D317C9 /* UserView.swift in Sources */, A1AF727328A9062600D317C9 /* UserView.swift in Sources */,
E9E30C3323FF255C00574EEF /* SettingsList.swift in Sources */, E9E30C3323FF255C00574EEF /* SettingsList.swift in Sources */,
E9EA690D23F9A5430012C3E8 /* SceneDelegate.swift in Sources */, E9EA690D23F9A5430012C3E8 /* SceneDelegate.swift in Sources */,
@ -459,8 +480,10 @@
E97AF46023FC85D600635494 /* PlaylistApi.swift in Sources */, E97AF46023FC85D600635494 /* PlaylistApi.swift in Sources */,
A11AC70628A188AE00645043 /* AuthApi.swift in Sources */, A11AC70628A188AE00645043 /* AuthApi.swift in Sources */,
A1AF726F28A84F7D00D317C9 /* AdminApi.swift in Sources */, A1AF726F28A84F7D00D317C9 /* AdminApi.swift in Sources */,
A15D257E29342E4F0049055E /* APNSHandler.swift in Sources */,
A1AF727128A850AE00D317C9 /* UsersList.swift in Sources */, A1AF727128A850AE00D317C9 /* UsersList.swift in Sources */,
E9EA690F23F9A5430012C3E8 /* AppSkeleton.swift in Sources */, E9EA690F23F9A5430012C3E8 /* AppSkeleton.swift in Sources */,
A15D257A293421350049055E /* StaticNotif.swift in Sources */,
E98254BD23F9B7A90056D9D3 /* Playlist.swift in Sources */, E98254BD23F9B7A90056D9D3 /* Playlist.swift in Sources */,
A13C54972928FD7C0034F233 /* ManagedInputList.swift in Sources */, A13C54972928FD7C0034F233 /* ManagedInputList.swift in Sources */,
E97AF46723FD650800635494 /* AddPlaylistSheet.swift in Sources */, E97AF46723FD650800635494 /* AddPlaylistSheet.swift in Sources */,
@ -471,6 +494,7 @@
E98254D023FB00B60056D9D3 /* LoginScreen.swift in Sources */, E98254D023FB00B60056D9D3 /* LoginScreen.swift in Sources */,
E9E30C2623FEA4F000574EEF /* TagApi.swift in Sources */, E9E30C2623FEA4F000574EEF /* TagApi.swift in Sources */,
E97AF46923FD9E1B00635494 /* SpotInputList.swift in Sources */, E97AF46923FD9E1B00635494 /* SpotInputList.swift in Sources */,
A15D257C293425390049055E /* NotificationsControls.swift in Sources */,
E97AF45B23FC748D00635494 /* UserApi.swift in Sources */, E97AF45B23FC748D00635494 /* UserApi.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@ -53,7 +53,7 @@
<EnvironmentVariables> <EnvironmentVariables>
<EnvironmentVariable <EnvironmentVariable
key = "MTOOLS_SERVER" key = "MTOOLS_SERVER"
value = "http://127.0.0.1:5000/" value = "http://127.0.0.1:8080/"
isEnabled = "YES"> isEnabled = "YES">
</EnvironmentVariable> </EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>

View File

@ -15,6 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch. // Override point for customization after application launch.
UIApplication.shared.registerForRemoteNotifications()
return true return true
} }
@ -32,7 +33,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Use this method to release any resources that were specific to the discarded scenes, as they will not return. // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
} }
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken
deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
StaticNotif.token = token
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError
error: Error) {
// Try again later.
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
completionHandler(.noData)
}
} }
extension Logger { extension Logger {

View File

@ -150,6 +150,66 @@
"scale" : "1x", "scale" : "1x",
"size" : "1024x1024" "size" : "1024x1024"
}, },
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
},
{ {
"filename" : "48.png", "filename" : "48.png",
"idiom" : "watch", "idiom" : "watch",
@ -225,6 +285,13 @@
"size" : "51x51", "size" : "51x51",
"subtype" : "45mm" "subtype" : "45mm"
}, },
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{ {
"filename" : "172.png", "filename" : "172.png",
"idiom" : "watch", "idiom" : "watch",
@ -256,71 +323,18 @@
"size" : "117x117", "size" : "117x117",
"subtype" : "45mm" "subtype" : "45mm"
}, },
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{ {
"filename" : "1024.png", "filename" : "1024.png",
"idiom" : "watch-marketing", "idiom" : "watch-marketing",
"scale" : "1x", "scale" : "1x",
"size" : "1024x1024" "size" : "1024x1024"
},
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
} }
], ],
"info" : { "info" : {

View File

@ -43,6 +43,10 @@
</dict> </dict>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>armv7</string> <string>armv7</string>

View File

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.aps-environment</key>
<string>development</string>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>

View File

@ -11,13 +11,14 @@ import Alamofire
import SwiftyJSON import SwiftyJSON
import KeychainAccess import KeychainAccess
import OSLog import OSLog
import UserNotifications
class LiveUser: ObservableObject { class LiveUser: ObservableObject {
@Published var playlists: [Playlist] @Published var playlists: [Playlist]
@Published var tags: [Tag] @Published var tags: [Tag]
@Published var username: String @Published var username: String
@Published var user: User? @Published var user: User
@Published var loggedIn: Bool { @Published var loggedIn: Bool {
didSet { didSet {
@ -25,6 +26,27 @@ class LiveUser: ObservableObject {
} }
} }
func requestAPNSPerms(){
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if error != nil {
self.handleAPNSFailure()
}
else {
// load token from static var and pass to backend server
APNSHandler.pass_token_to_backend(onFailure: {
self.handleAPNSFailure()
})
}
}
}
func handleAPNSFailure(){
Logger.sys.debug("failed to get APNS token")
}
@Published var isRefreshingUser = false @Published var isRefreshingUser = false
@Published var isRefreshingPlaylists = false @Published var isRefreshingPlaylists = false
@Published var isRefreshingTags = false @Published var isRefreshingTags = false
@ -34,6 +56,7 @@ class LiveUser: ObservableObject {
self.tags = tags self.tags = tags
self.username = username self.username = username
self.loggedIn = loggedIn self.loggedIn = loggedIn
self.user = User.get_null_user()
} }
init(playlists: [Playlist], tags: [Tag], username: String, loggedIn: Bool, user: User) { init(playlists: [Playlist], tags: [Tag], username: String, loggedIn: Bool, user: User) {
@ -45,11 +68,7 @@ class LiveUser: ObservableObject {
} }
func lastfm_connected() -> Bool { func lastfm_connected() -> Bool {
if let username = user?.lastfm_username { return username.count > 0
return username.count > 0
}
return false
} }
func logout() { func logout() {
@ -64,7 +83,7 @@ class LiveUser: ObservableObject {
playlists.removeAll() playlists.removeAll()
tags.removeAll() tags.removeAll()
username = "" username = ""
user = nil user = User.get_null_user()
UserDefaults.standard.removeObject(forKey: "playlists") UserDefaults.standard.removeObject(forKey: "playlists")
UserDefaults.standard.removeObject(forKey: "tags") UserDefaults.standard.removeObject(forKey: "tags")
@ -224,27 +243,9 @@ class LiveUser: ObservableObject {
} }
func check_network_response(response: AFDataResponse<Any>) -> Bool { func check_network_response(response: AFDataResponse<Any>) -> Bool {
return NetworkHelper.check_network_response(response: response, onTokenFail: {
if let statusCode = response.response?.statusCode { self.logout()
switch statusCode { })
case 401: // token has expired
Logger.sys.info("token expired, logging user out")
self.logout()
return false
case 400..<500:
Logger.net.error("client fault \(statusCode)")
return false
case 500..<600:
Logger.net.warning("server fault \(statusCode)")
return false
case _: // 200 -> Success
return true
}
}
Logger.net.error("live user failed to access status code to check")
return false
} }
func load_user_defaults() -> LiveUser { func load_user_defaults() -> LiveUser {
@ -277,4 +278,12 @@ class LiveUser: ObservableObject {
return self return self
} }
static func get_preview_user() -> LiveUser {
return LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)
}
static func get_preview_user_with_user() -> LiveUser {
return LiveUser(playlists: [], tags: [], username: "user", loggedIn: false, user: User())
}
} }

View File

@ -30,28 +30,28 @@ class Playlist: Identifiable, Equatable, Codable, ObservableObject {
self.updatePlaylist(updates: JSON(["include_recommendations": self.include_recommendations])) self.updatePlaylist(updates: JSON(["include_recommendations": self.include_recommendations]))
} }
} }
@Published var recommendation_sample: Int{ @Published var recommendation_sample: Int {
didSet { didSet {
self.updatePlaylist(updates: JSON(["recommendation_sample": self.recommendation_sample])) self.updatePlaylist(updates: JSON(["recommendation_sample": self.recommendation_sample]))
} }
} }
@Published var include_library_tracks: Bool{ @Published var include_library_tracks: Bool {
didSet { didSet {
self.updatePlaylist(updates: JSON(["include_library_tracks": self.include_library_tracks])) self.updatePlaylist(updates: JSON(["include_library_tracks": self.include_library_tracks]))
} }
} }
@Published var parts: Array<String>{ @Published var parts: Array<String> {
didSet { didSet {
self.updatePlaylist(updates: JSON(["parts": self.parts])) self.updatePlaylist(updates: JSON(["parts": self.parts]))
} }
} }
@Published var playlist_references: Array<String>{ @Published var playlist_references: Array<String> {
didSet { didSet {
self.updatePlaylist(updates: JSON(["playlist_references": self.playlist_references])) self.updatePlaylist(updates: JSON(["playlist_references": self.playlist_references]))
} }
} }
@Published var shuffle: Bool{ @Published var shuffle: Bool {
didSet { didSet {
self.updatePlaylist(updates: JSON(["shuffle": self.shuffle])) self.updatePlaylist(updates: JSON(["shuffle": self.shuffle]))
} }
@ -88,45 +88,45 @@ class Playlist: Identifiable, Equatable, Codable, ObservableObject {
@Published var lastfm_stat_last_refresh: String? @Published var lastfm_stat_last_refresh: String?
@Published var add_last_month: Bool{ @Published var add_last_month: Bool {
didSet { didSet {
self.updatePlaylist(updates: JSON(["add_last_month": self.add_last_month])) self.updatePlaylist(updates: JSON(["add_last_month": self.add_last_month]))
} }
} }
@Published var add_this_month: Bool{ @Published var add_this_month: Bool {
didSet { didSet {
self.updatePlaylist(updates: JSON(["add_this_month": self.add_this_month])) self.updatePlaylist(updates: JSON(["add_this_month": self.add_this_month]))
} }
} }
@Published var day_boundary: Int{ @Published var day_boundary: Int {
didSet { didSet {
self.updatePlaylist(updates: JSON(["day_boundary": self.day_boundary])) self.updatePlaylist(updates: JSON(["day_boundary": self.day_boundary]))
} }
} }
@Published var chart_range: LastFmRange{ @Published var chart_range: LastFmRange {
didSet { didSet {
self.updatePlaylist(updates: JSON(["chart_range": self.chart_range.rawValue])) self.updatePlaylist(updates: JSON(["chart_range": self.chart_range.rawValue]))
} }
} }
@Published var chart_limit: Int{ @Published var chart_limit: Int {
didSet { didSet {
self.updatePlaylist(updates: JSON(["chart_range": self.chart_range.rawValue])) self.updatePlaylist(updates: JSON(["chart_limit": self.chart_limit]))
} }
} }
func updatePlaylist(updates: JSON) { func updatePlaylist(updates: JSON) {
let api = PlaylistApi.updatePlaylist(name: self.name, updates: updates) let api = PlaylistApi.updatePlaylist(name: self.name, updates: updates)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
switch response.response?.statusCode { switch response.response?.statusCode {
case 200, 201: case 200, 201:
break break
case _: case _:
Logger.net.error("error: \(self.name), \(updates)") Logger.net.error("error: \(self.name), \(updates)")
}
} }
//TODO: do better error checking
} }
//TODO: do better error checking
}
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case name case name

View File

@ -15,7 +15,7 @@ enum UserType: String, Decodable {
case admin = "admin" case admin = "admin"
} }
class User: Identifiable, Decodable { class User: Identifiable, Decodable, ObservableObject {
//MARK: Properties //MARK: Properties
@ -30,7 +30,29 @@ class User: Identifiable, Decodable {
var last_refreshed: String var last_refreshed: String
var spotify_linked: Bool var spotify_linked: Bool
@Published var lastfm_username: String? {
@Published var notify: Bool {
didSet {
self.updateUser(updates: JSON(["notify": self.notify]))
}
}
@Published var notify_playlist_updates: Bool {
didSet {
self.updateUser(updates: JSON(["notify_playlist_updates": self.notify_playlist_updates]))
}
}
@Published var notify_tag_updates: Bool {
didSet {
self.updateUser(updates: JSON(["notify_tag_updates": self.notify_tag_updates]))
}
}
@Published var notify_admins: Bool {
didSet {
self.updateUser(updates: JSON(["notify_admins": self.notify_admins]))
}
}
@Published var lastfm_username: String {
didSet { didSet {
self.updateUser(updates: JSON(["lastfm_username": self.lastfm_username])) self.updateUser(updates: JSON(["lastfm_username": self.lastfm_username]))
} }
@ -48,7 +70,12 @@ class User: Identifiable, Decodable {
last_keygen: String = "", last_keygen: String = "",
last_refreshed: String = "", last_refreshed: String = "",
spotify_linked: Bool = true, spotify_linked: Bool = true,
lastfm_username: String? = nil){ lastfm_username: String = "",
notify: Bool = false,
notify_playlist_updates: Bool = false,
notify_tag_updates: Bool = false,
notify_admins: Bool = false){
self.username = username self.username = username
self.email = email self.email = email
@ -61,19 +88,21 @@ class User: Identifiable, Decodable {
self.last_refreshed = last_refreshed self.last_refreshed = last_refreshed
self.spotify_linked = spotify_linked self.spotify_linked = spotify_linked
self.lastfm_username = lastfm_username self.lastfm_username = lastfm_username
self.notify = notify
self.notify_playlist_updates = notify_playlist_updates
self.notify_tag_updates = notify_tag_updates
self.notify_admins = notify_admins
} }
func updateUser(updates: JSON) { func updateUser(updates: JSON) {
let api = UserApi.updateUser(updates: updates) let api = UserApi.updateUser(updates: updates)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
switch response.response?.statusCode {
case 200, 201: if !NetworkHelper.check_network_response(response: response) {
break
case _:
Logger.net.error("error while updating user: \(updates)") Logger.net.error("error while updating user: \(updates)")
} }
} }
//TODO: do better error checking
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
@ -89,6 +118,11 @@ class User: Identifiable, Decodable {
case spotify_linked case spotify_linked
case lastfm_username case lastfm_username
case notify
case notify_playlist_updates
case notify_tag_updates
case notify_admins
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -120,7 +154,31 @@ class User: Identifiable, Decodable {
do{ do{
lastfm_username = try container.decode(String.self, forKey: .lastfm_username) lastfm_username = try container.decode(String.self, forKey: .lastfm_username)
}catch { }catch {
lastfm_username = nil lastfm_username = ""
}
do{
notify = try container.decode(Bool.self, forKey: .notify)
}catch {
notify = false
}
do{
notify_playlist_updates = try container.decode(Bool.self, forKey: .notify_playlist_updates)
}catch {
notify_playlist_updates = false
}
do{
notify_tag_updates = try container.decode(Bool.self, forKey: .notify_tag_updates)
}catch {
notify_tag_updates = false
}
do{
notify_admins = try container.decode(Bool.self, forKey: .notify_admins)
}catch {
notify_admins = false
} }
} }
@ -139,6 +197,15 @@ class User: Identifiable, Decodable {
try container.encode(self.spotify_linked, forKey: .spotify_linked) try container.encode(self.spotify_linked, forKey: .spotify_linked)
try container.encode(self.lastfm_username, forKey: .lastfm_username) try container.encode(self.lastfm_username, forKey: .lastfm_username)
try container.encode(self.notify, forKey: .notify)
try container.encode(self.notify_playlist_updates, forKey: .notify_playlist_updates)
try container.encode(self.notify_tag_updates, forKey: .notify_tag_updates)
try container.encode(self.notify_admins, forKey: .notify_admins)
}
static func get_null_user() -> User {
return User()
} }
} }

View File

@ -0,0 +1,38 @@
//
// NetworkHelper.swift
// Mixonomer
//
// Created by Andy Pack on 27/11/2022.
// Copyright © 2022 Sarsoo. All rights reserved.
//
import Foundation
import OSLog
import Alamofire
class NetworkHelper {
static func check_network_response(response: AFDataResponse<Any>, onTokenFail: (() -> Void)? = nil) -> Bool {
if let statusCode = response.response?.statusCode {
switch statusCode {
case 401: // token has expired
Logger.sys.info("token expired, logging user out")
onTokenFail?()
return false
case 400..<500:
Logger.net.error("client fault \(statusCode)")
return false
case 500..<600:
Logger.net.warning("server fault \(statusCode)")
return false
case _: // 200 -> Success
return true
}
}
Logger.net.error("failed to access network status code to validate")
return false
}
}

View File

@ -15,6 +15,11 @@ public enum UserApi {
case getUser case getUser
case updateUser(updates: JSON) case updateUser(updates: JSON)
case deleteUser case deleteUser
case passAPNSToken(updates: String)
case updateNotify(state: Bool)
case updateNotifyPlaylist(state: String)
case updateNotifyTag(state: String)
case updateNotifyAdmin(state: String)
} }
extension UserApi: ApiRequest { extension UserApi: ApiRequest {
@ -30,6 +35,10 @@ extension UserApi: ApiRequest {
return "api/user" return "api/user"
case .deleteUser: case .deleteUser:
return "api/user" return "api/user"
case .passAPNSToken:
return "api/user"
case .updateNotify, .updateNotifyPlaylist, .updateNotifyTag, .updateNotifyAdmin:
return "api/user"
} }
} }
@ -41,6 +50,10 @@ extension UserApi: ApiRequest {
return .post return .post
case .deleteUser: case .deleteUser:
return .delete return .delete
case .passAPNSToken:
return .post
case .updateNotify, .updateNotifyPlaylist, .updateNotifyTag, .updateNotifyAdmin:
return .post
} }
} }
@ -50,6 +63,16 @@ extension UserApi: ApiRequest {
return nil return nil
case .updateUser(let updates): case .updateUser(let updates):
return updates return updates
case .passAPNSToken(let token):
return JSON(["apns_token": token])
case .updateNotify(let state):
return JSON(["notify": state])
case .updateNotifyPlaylist(let state):
return JSON(["notify_playlist_updates": state])
case .updateNotifyTag(let state):
return JSON(["notify_tag_updates": state])
case .updateNotifyAdmin(let state):
return JSON(["notify_admins": state])
} }
} }
@ -57,7 +80,9 @@ extension UserApi: ApiRequest {
switch self { switch self {
case .getUser, .deleteUser: case .getUser, .deleteUser:
return nil return nil
case .updateUser: case .updateUser, .passAPNSToken:
return JSONParameterEncoder.default
case .updateNotify, .updateNotifyPlaylist, .updateNotifyTag, .updateNotifyAdmin:
return JSONParameterEncoder.default return JSONParameterEncoder.default
} }
} }
@ -70,7 +95,7 @@ extension UserApi: ApiRequest {
return ApiRequestDefaults.authMethod return ApiRequestDefaults.authMethod
} }
static func fromJSON(user: Data) -> User? { static func fromJSON(user: Data) -> User {
let decoder = JSONDecoder() let decoder = JSONDecoder()
do { do {
@ -78,11 +103,11 @@ extension UserApi: ApiRequest {
return user return user
} catch { } catch {
Logger.parse.error("error parsing user from json: \(error)") Logger.parse.error("error parsing user from json: \(error)")
return User.get_null_user()
} }
return nil
} }
static func fromJSON(user: JSON) -> User? { static func fromJSON(user: JSON) -> User {
let _json = user.rawString()?.data(using: .utf8) let _json = user.rawString()?.data(using: .utf8)
@ -95,16 +120,16 @@ extension UserApi: ApiRequest {
Logger.parse.error("error parsing user from json: \(error)") Logger.parse.error("error parsing user from json: \(error)")
} }
} }
return nil
return User.get_null_user()
} }
static func fromJSON(user: [JSON]) -> [User] { static func fromJSON(user: [JSON]) -> [User] {
var _users: [User] = [] var _users: [User] = []
for dict in user { for dict in user {
let _iter = self.fromJSON(user: dict) let _iter = self.fromJSON(user: dict)
if let returned = _iter {
_users.append(returned) _users.append(_iter)
}
} }
return _users return _users
} }

View File

@ -0,0 +1,37 @@
//
// APNSHandler.swift
// Mixonomer
//
// Created by Andy Pack on 27/11/2022.
// Copyright © 2022 Sarsoo. All rights reserved.
//
import Foundation
import OSLog
class APNSHandler {
static func pass_token_to_backend(onFailure: (() -> Void)? = nil) {
// check if a token is waiting to be handed off
if let token = StaticNotif.token {
if !StaticNotif.hasDelivered {
Logger.sys.info("passing off APNS network token")
let api = UserApi.passAPNSToken(updates: token)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
if NetworkHelper.check_network_response(response: response) {
Logger.net.debug("successfully handed off APNS token")
StaticNotif.hasDelivered = true
} else {
Logger.net.error("failed to hand off APNS token: \(response.response?.statusCode ?? 0)")
onFailure?()
}
}
}
}
else {
Logger.sys.debug("no APNS token waiting, skipping network handoff")
}
}
}

View File

@ -0,0 +1,14 @@
//
// StaticNotif.swift
// Mixonomer
//
// Created by Andy Pack on 27/11/2022.
// Copyright © 2022 Sarsoo. All rights reserved.
//
import Foundation
class StaticNotif {
static var token: String?
static var hasDelivered: Bool = false
}

View File

@ -74,6 +74,6 @@ struct UsersList: View {
struct UsersList_Previews: PreviewProvider { struct UsersList_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
UsersList() UsersList()
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -77,6 +77,6 @@ struct AppSkeleton: View {
struct RootView_Previews: PreviewProvider { struct RootView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
AppSkeleton() AppSkeleton()
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -237,11 +237,11 @@ struct LoginScreen_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group{ Group{
LoginScreen(screenMode: .None) LoginScreen(screenMode: .None)
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
LoginScreen(screenMode: .Login) LoginScreen(screenMode: .Login)
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
LoginScreen(screenMode: .Register) LoginScreen(screenMode: .Register)
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }
} }

View File

@ -112,6 +112,6 @@ struct AddPlaylistSheet: View {
struct AddPlaylistSheet_Previews: PreviewProvider { struct AddPlaylistSheet_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
AddPlaylistSheet(playlists: .constant([]), username: .constant("username")) AddPlaylistSheet(playlists: .constant([]), username: .constant("username"))
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -22,7 +22,7 @@ struct PlaylistList: View {
NavigationView { NavigationView {
List{ List{
if liveUser.user?.spotify_linked == false { if liveUser.user.spotify_linked == false {
Text("Spotify isn't linked, login to the web client to pair") Text("Spotify isn't linked, login to the web client to pair")
Button(action: { Button(action: {
@ -101,9 +101,9 @@ struct PlaylistList_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
PlaylistList() PlaylistList()
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
PlaylistList() PlaylistList()
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false, user: User())) .environmentObject(LiveUser.get_preview_user_with_user())
} }
} }
} }

View File

@ -63,6 +63,6 @@ struct PlaylistRow_Previews: PreviewProvider {
PlaylistView(playlist: .constant( PlaylistView(playlist: .constant(
Playlist(name: "playlist name", username: "username") Playlist(name: "playlist name", username: "username")
)) ))
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -366,7 +366,7 @@ struct PlaylistView_Previews: PreviewProvider {
lastfm_stat_artist_percent: 80 lastfm_stat_artist_percent: 80
) )
)) ))
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
PlaylistView(playlist: .constant( PlaylistView(playlist: .constant(
Playlist(name: "playlist name", Playlist(name: "playlist name",
username: "username", username: "username",
@ -375,7 +375,7 @@ struct PlaylistView_Previews: PreviewProvider {
lastfm_stat_artist_percent: 80 lastfm_stat_artist_percent: 80
) )
)) ))
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false, user: User())) .environmentObject(LiveUser.get_preview_user_with_user())
} }
} }

View File

@ -0,0 +1,52 @@
//
// NotificationsControls.swift
// Mixonomer
//
// Created by Andy Pack on 27/11/2022.
// Copyright © 2022 Sarsoo. All rights reserved.
//
import SwiftUI
struct NotificationsControls: View {
@EnvironmentObject var liveUser: LiveUser
var body: some View {
List {
Section {
Toggle(isOn: self.$liveUser.user.notify) {
Text("Enabled")
}
}
Section {
Button("Request permission on this device") {
self.liveUser.requestAPNSPerms()
}
}
Section {
Toggle(isOn: self.$liveUser.user.notify_playlist_updates) {
Text("Playlist Updates")
}
Toggle(isOn: self.$liveUser.user.notify_tag_updates) {
Text("Tag Updates")
}
if liveUser.user.type == .admin {
Toggle(isOn: self.$liveUser.user.notify_admins) {
Text("Admin Updates")
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Notifications 🔔"))
}
}
struct NotificationsControls_Previews: PreviewProvider {
static var previews: some View {
NotificationsControls()
.environmentObject(LiveUser.get_preview_user())
}
}

View File

@ -16,13 +16,7 @@ struct SettingsList: View {
@State private var deleteAlertShowing = false @State private var deleteAlertShowing = false
var body: some View { var body: some View {
NavigationView {
let spotify_link_bind = Binding<Bool>(get: { liveUser.user?.spotify_linked ?? false},
set: { newVal in liveUser.user?.spotify_linked = newVal })
// let lastfm_bind = Binding<String>(get: { liveUser.user?.lastfm_username ?? ""},
// set: { newVal in liveUser.user?.lastfm_username = newVal })
return NavigationView {
List{ List{
Section { Section {
Button(action: { Button(action: {
@ -47,21 +41,25 @@ struct SettingsList: View {
} }
Section(header: Text("Integrations")) { Section(header: Text("Integrations")) {
Toggle(isOn: spotify_link_bind) { Toggle(isOn: self.$liveUser.user.spotify_linked) {
Text("Spotify Link") Text("Spotify Link")
} }
.disabled(true) .disabled(true)
// NavigationLink("Last.fm Username") { NavigationLink("Last.fm") {
// List{ List{
// TextField("Username", text: lastfm_bind) TextField("Username", text: self.$liveUser.user.lastfm_username)
// } }
// } }
} }
// Section(header: Text("Last.fm")) { Section {
// TextField("Last.fm Username", text: lastfm_bind) NavigationLink(destination: NotificationsControls()) {
// } HStack {
Text("Notifications")
}
}
}
Section { Section {
Button(action: { Button(action: {
@ -128,6 +126,6 @@ struct SettingsList: View {
struct SettingsList_Previews: PreviewProvider { struct SettingsList_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SettingsList() SettingsList()
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -94,6 +94,6 @@ struct AddTagSheet: View {
struct AddTagSheet_Previews: PreviewProvider { struct AddTagSheet_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
AddTagSheet(tags: .constant([]), username: .constant("username")) AddTagSheet(tags: .constant([]), username: .constant("username"))
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -93,6 +93,6 @@ struct TagList: View {
struct TagList_Previews: PreviewProvider { struct TagList_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
TagList() TagList()
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -66,6 +66,6 @@ struct TagRow_Previews: PreviewProvider {
last_updated: "10th Feb") last_updated: "10th Feb")
)) ))
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }

View File

@ -118,32 +118,41 @@ struct TagView: View {
} }
func runTag() { func runTag() {
Logger.net.debug("running tag from view: \(self.tag.name)")
let api = TagApi.runTag(tag_id: tag.tag_id) let api = TagApi.runTag(tag_id: tag.tag_id)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
if self.liveUser.check_network_response(response: response) { if self.liveUser.check_network_response(response: response) {
Logger.net.debug("successfully running tag: \(self.tag.name)")
} else { } else {
Logger.net.error("request failed for running tag: \(self.tag.name)")
} }
} }
//TODO: do better error checking //TODO: do better error checking
} }
func updateTag(updates: JSON) { func updateTag(updates: JSON) {
Logger.net.debug("updating tag from view: \(self.tag.name)")
let api = TagApi.updateTag(tag_id: tag.tag_id, updates: updates) let api = TagApi.updateTag(tag_id: tag.tag_id, updates: updates)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
if self.liveUser.check_network_response(response: response) { if self.liveUser.check_network_response(response: response) {
Logger.net.debug("successfully updated tag: \(self.tag.name)")
} else { } else {
Logger.net.error("request failed for updating tag: \(self.tag.name)")
} }
} }
//TODO: do better error checking //TODO: do better error checking
} }
func refreshTag() { func refreshTag() {
Logger.net.debug("refreshing tag from view: \(self.tag.name)")
let api = TagApi.getTag(tag_id: self.tag.tag_id) let api = TagApi.getTag(tag_id: self.tag.tag_id)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
@ -190,6 +199,6 @@ struct TagView_Previews: PreviewProvider {
last_updated: "10th Feb") last_updated: "10th Feb")
)) ))
.environmentObject(LiveUser(playlists: [], tags: [], username: "user", loggedIn: false)) .environmentObject(LiveUser.get_preview_user())
} }
} }