added new, delete. added force touches, added input listing, added open

This commit is contained in:
aj 2020-02-19 23:00:23 +00:00
parent 36320aecc7
commit 19526c2511
17 changed files with 585 additions and 120 deletions

View File

@ -13,9 +13,12 @@
E97AF45E23FC83AF00635494 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = E97AF45D23FC83AF00635494 /* KeychainAccess */; };
E97AF46023FC85D600635494 /* PlaylistApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF45F23FC85D600635494 /* PlaylistApi.swift */; };
E97AF46223FC89CC00635494 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E97AF46123FC89CB00635494 /* Main.storyboard */; };
E97AF46423FD4EEF00635494 /* LiveUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF46323FD4EEF00635494 /* LiveUser.swift */; };
E97AF46723FD650800635494 /* AddPlaylistSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF46623FD650800635494 /* AddPlaylistSheet.swift */; };
E97AF46923FD9E1B00635494 /* PlaylistInputList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF46823FD9E1B00635494 /* PlaylistInputList.swift */; };
E97AF46C23FDA90900635494 /* LoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97AF46B23FDA90900635494 /* LoginController.swift */; };
E98254BD23F9B7A90056D9D3 /* Playlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254BC23F9B7A90056D9D3 /* Playlist.swift */; };
E98254C223F9FFF90056D9D3 /* PlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254C123F9FFF90056D9D3 /* PlaylistView.swift */; };
E98254C823FA25D20056D9D3 /* PlaylistList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254C723FA25D20056D9D3 /* PlaylistList.swift */; };
E98254CA23FA26600056D9D3 /* PlaylistRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254C923FA26600056D9D3 /* PlaylistRow.swift */; };
E98254D023FB00B60056D9D3 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98254CF23FB00B60056D9D3 /* LoginScreen.swift */; };
E98254D923FB53780056D9D3 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = E98254D823FB53780056D9D3 /* Alamofire */; };
@ -52,9 +55,12 @@
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>"; };
E97AF46123FC89CB00635494 /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
E97AF46323FD4EEF00635494 /* LiveUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveUser.swift; sourceTree = "<group>"; };
E97AF46623FD650800635494 /* AddPlaylistSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPlaylistSheet.swift; sourceTree = "<group>"; };
E97AF46823FD9E1B00635494 /* PlaylistInputList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistInputList.swift; sourceTree = "<group>"; };
E97AF46B23FDA90900635494 /* LoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginController.swift; sourceTree = "<group>"; };
E98254BC23F9B7A90056D9D3 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
E98254C123F9FFF90056D9D3 /* PlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistView.swift; sourceTree = "<group>"; };
E98254C723FA25D20056D9D3 /* PlaylistList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistList.swift; sourceTree = "<group>"; };
E98254C923FA26600056D9D3 /* PlaylistRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistRow.swift; sourceTree = "<group>"; };
E98254CF23FB00B60056D9D3 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = "<group>"; };
E98254DA23FB64740056D9D3 /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = "<group>"; };
@ -102,11 +108,20 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
E97AF46A23FDA8ED00635494 /* Controller */ = {
isa = PBXGroup;
children = (
E97AF46B23FDA90900635494 /* LoginController.swift */,
);
path = Controller;
sourceTree = "<group>";
};
E98254BE23F9BD540056D9D3 /* Model */ = {
isa = PBXGroup;
children = (
E98254BC23F9B7A90056D9D3 /* Playlist.swift */,
E97AF45523FC4E7800635494 /* User.swift */,
E97AF46323FD4EEF00635494 /* LiveUser.swift */,
);
path = Model;
sourceTree = "<group>";
@ -126,9 +141,10 @@
children = (
E9EA690E23F9A5430012C3E8 /* RootView.swift */,
E98254C123F9FFF90056D9D3 /* PlaylistView.swift */,
E98254C723FA25D20056D9D3 /* PlaylistList.swift */,
E98254C923FA26600056D9D3 /* PlaylistRow.swift */,
E98254CF23FB00B60056D9D3 /* LoginScreen.swift */,
E97AF46623FD650800635494 /* AddPlaylistSheet.swift */,
E97AF46823FD9E1B00635494 /* PlaylistInputList.swift */,
);
path = Views;
sourceTree = "<group>";
@ -167,6 +183,7 @@
E9EA690923F9A5430012C3E8 /* Music Tools */ = {
isa = PBXGroup;
children = (
E97AF46A23FDA8ED00635494 /* Controller */,
E98254C623FA25280056D9D3 /* Application */,
E98254C023F9FFDD0056D9D3 /* Views */,
E98254BF23F9BE040056D9D3 /* Network */,
@ -350,13 +367,16 @@
E9EA690B23F9A5430012C3E8 /* AppDelegate.swift in Sources */,
E9EA690D23F9A5430012C3E8 /* SceneDelegate.swift in Sources */,
E98254DB23FB64740056D9D3 /* Network.swift in Sources */,
E97AF46C23FDA90900635494 /* LoginController.swift in Sources */,
E97AF46023FC85D600635494 /* PlaylistApi.swift in Sources */,
E98254C823FA25D20056D9D3 /* PlaylistList.swift in Sources */,
E9EA690F23F9A5430012C3E8 /* RootView.swift in Sources */,
E98254BD23F9B7A90056D9D3 /* Playlist.swift in Sources */,
E97AF46723FD650800635494 /* AddPlaylistSheet.swift in Sources */,
E98254C223F9FFF90056D9D3 /* PlaylistView.swift in Sources */,
E97AF46423FD4EEF00635494 /* LiveUser.swift in Sources */,
E97AF45623FC4E7800635494 /* User.swift in Sources */,
E98254D023FB00B60056D9D3 /* LoginScreen.swift in Sources */,
E97AF46923FD9E1B00635494 /* PlaylistInputList.swift in Sources */,
E97AF45B23FC748D00635494 /* UserApi.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -523,7 +543,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"Music Tools/Preview Content\"";
DEVELOPMENT_ASSET_PATHS = "Music\\ Tools/Preview\\ Content";
DEVELOPMENT_TEAM = 8UZ2659FDY;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "Music Tools/Info.plist";
@ -543,7 +563,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"Music Tools/Preview Content\"";
DEVELOPMENT_ASSET_PATHS = "Music\\ Tools/Preview\\ Content";
DEVELOPMENT_TEAM = 8UZ2659FDY;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "Music Tools/Info.plist";

View File

@ -7,14 +7,25 @@
//
import UIKit
import SwiftyJSON
import KeychainAccess
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var liveUser: LiveUser?
var loading = true
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let keychain = Keychain(service: "xyz.sarsoo.music.login")
keychain["username"] = ""
keychain["password"] = ""
liveUser = LiveUser(playlists: [])
return true
}

View File

@ -15,8 +15,8 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" fixedFrame="YES" image="Logo" translatesAutoresizingMaskIntoConstraints="NO" id="nIw-aa-ATp">
<rect key="frame" x="-49" y="192" width="512" height="512"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" fixedFrame="YES" image="MusicToolsLogo" translatesAutoresizingMaskIntoConstraints="NO" id="NIx-qI-fR9">
<rect key="frame" x="-59" y="184" width="512" height="512"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
@ -30,6 +30,6 @@
</scene>
</scenes>
<resources>
<image name="Logo" width="250" height="40"/>
<image name="MusicToolsLogo" width="512" height="512"/>
</resources>
</document>

View File

@ -1,7 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13142" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="aYE-zJ-V1n">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12042"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes/>
<scenes>
<!--View Controller-->
<scene sceneID="a5Z-le-CZi">
<objects>
<viewController id="Spt-Mn-jmg" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="kc5-Ke-ZcW">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="34" translatesAutoresizingMaskIntoConstraints="NO" id="9nJ-jm-QSN">
<rect key="frame" x="-49" y="105" width="512" height="686"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="MusicToolsLogo" translatesAutoresizingMaskIntoConstraints="NO" id="ZdO-HY-xSA">
<rect key="frame" x="0.0" y="0.0" width="512" height="512"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1SI-H8-Rgg">
<rect key="frame" x="215.5" y="546" width="81" height="53"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
<state key="normal" title="Login"/>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6UM-RX-7fe">
<rect key="frame" x="196" y="633" width="120" height="53"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
<state key="normal" title="Register"/>
</button>
</subviews>
<constraints>
<constraint firstItem="6UM-RX-7fe" firstAttribute="top" secondItem="1SI-H8-Rgg" secondAttribute="bottom" constant="34" id="Mc6-3Y-SWR"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="9nJ-jm-QSN" firstAttribute="centerX" secondItem="kc5-Ke-ZcW" secondAttribute="centerX" id="cpW-xl-QNm"/>
<constraint firstItem="9nJ-jm-QSN" firstAttribute="centerY" secondItem="kc5-Ke-ZcW" secondAttribute="centerY" id="kjc-76-4D7"/>
</constraints>
<viewLayoutGuide key="safeArea" id="AaY-2f-fg2"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="0B7-E4-M4W" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-1443.4782608695652" y="-185.49107142857142"/>
</scene>
<!--View Controller-->
<scene sceneID="6At-fb-bfN">
<objects>
<viewController id="TND-IP-OdM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="MhD-yZ-pD8">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="VLy-9d-bfF"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Dgg-BJ-VHU" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="48" y="304"/>
</scene>
<!--Login Controller-->
<scene sceneID="Cbq-wX-wg3">
<objects>
<viewController id="aYE-zJ-V1n" customClass="LoginController" customModule="Music_Tools" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="cjE-Uz-adD">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="MusicToolsLogo" translatesAutoresizingMaskIntoConstraints="NO" id="Aa0-TI-VfP">
<rect key="frame" x="0.0" y="94" width="414" height="433"/>
</imageView>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="bezel" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Jsz-1T-t1A">
<rect key="frame" x="20" y="589" width="374" height="34"/>
<constraints>
<constraint firstAttribute="height" constant="34" id="ThQ-Jz-RB7"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" textContentType="username"/>
</textField>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="bezel" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="c5V-bv-fGr">
<rect key="frame" x="20" y="682" width="374" height="34"/>
<constraints>
<constraint firstAttribute="height" constant="34" id="ynd-9a-coG"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="go" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6YH-OC-12B">
<rect key="frame" x="185.5" y="766" width="43" height="53"/>
<constraints>
<constraint firstAttribute="height" constant="53" id="udu-LZ-6uK"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
<state key="normal" title="Go">
<color key="titleShadowColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="login:" destination="aYE-zJ-V1n" eventType="touchUpInside" id="G3y-eM-24U"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CNh-mk-uV0">
<rect key="frame" x="167.5" y="560" width="79" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="oKd-GV-oVN"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mvf-wa-Hkg">
<rect key="frame" x="170" y="653" width="74" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="glG-PD-dHg"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="Jsz-1T-t1A" firstAttribute="leading" secondItem="c5V-bv-fGr" secondAttribute="leading" id="0wx-Ol-kyH"/>
<constraint firstItem="Aa0-TI-VfP" firstAttribute="top" secondItem="JQv-rE-o2m" secondAttribute="top" constant="50" id="36L-3g-C8l"/>
<constraint firstItem="JQv-rE-o2m" firstAttribute="bottom" secondItem="6YH-OC-12B" secondAttribute="bottom" constant="43" id="4I4-MB-XEO"/>
<constraint firstItem="c5V-bv-fGr" firstAttribute="top" secondItem="mvf-wa-Hkg" secondAttribute="bottom" constant="8" id="57A-nQ-c7X"/>
<constraint firstItem="Aa0-TI-VfP" firstAttribute="centerX" secondItem="CNh-mk-uV0" secondAttribute="centerX" id="CSz-Z9-0h2"/>
<constraint firstItem="Jsz-1T-t1A" firstAttribute="trailing" secondItem="cjE-Uz-adD" secondAttribute="trailingMargin" id="JUi-Dm-bqV"/>
<constraint firstItem="CNh-mk-uV0" firstAttribute="centerX" secondItem="Jsz-1T-t1A" secondAttribute="centerX" id="OGV-Rj-ryd"/>
<constraint firstItem="mvf-wa-Hkg" firstAttribute="top" secondItem="Jsz-1T-t1A" secondAttribute="bottom" constant="30" id="V2C-dy-adh"/>
<constraint firstItem="Jsz-1T-t1A" firstAttribute="top" secondItem="CNh-mk-uV0" secondAttribute="bottom" constant="8" id="WVP-qB-wVP"/>
<constraint firstItem="6YH-OC-12B" firstAttribute="top" secondItem="c5V-bv-fGr" secondAttribute="bottom" constant="50" id="ah4-pZ-kNm"/>
<constraint firstItem="c5V-bv-fGr" firstAttribute="centerX" secondItem="6YH-OC-12B" secondAttribute="centerX" id="b6B-2x-Zf2"/>
<constraint firstItem="Jsz-1T-t1A" firstAttribute="trailing" secondItem="c5V-bv-fGr" secondAttribute="trailing" id="bsE-gu-f8q"/>
<constraint firstItem="Aa0-TI-VfP" firstAttribute="leading" secondItem="JQv-rE-o2m" secondAttribute="leading" id="mrX-sL-EaE"/>
<constraint firstItem="Jsz-1T-t1A" firstAttribute="leading" secondItem="cjE-Uz-adD" secondAttribute="leadingMargin" id="q1D-Aj-DhS"/>
<constraint firstItem="mvf-wa-Hkg" firstAttribute="centerX" secondItem="c5V-bv-fGr" secondAttribute="centerX" id="rPo-wh-BDa"/>
<constraint firstItem="CNh-mk-uV0" firstAttribute="top" secondItem="Aa0-TI-VfP" secondAttribute="bottom" constant="33" id="yXw-NR-ADJ"/>
</constraints>
<viewLayoutGuide key="safeArea" id="JQv-rE-o2m"/>
</view>
<connections>
<outlet property="goButton" destination="6YH-OC-12B" id="JFW-b1-k9S"/>
<outlet property="passwordField" destination="c5V-bv-fGr" id="9LJ-hb-7Jx"/>
<outlet property="usernameField" destination="Jsz-1T-t1A" id="QOC-fI-XsH"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="BJs-Wv-17o" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-68.115942028985515" y="-676.33928571428567"/>
</scene>
</scenes>
<resources>
<image name="MusicToolsLogo" width="512" height="512"/>
</resources>
</document>

View File

@ -8,13 +8,11 @@
import UIKit
import SwiftUI
import KeychainAccess
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
@ -23,14 +21,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Create the SwiftUI view that provides the window contents.
let contentView = RootView()
let keychain = Keychain(service: "xyz.sarsoo.music.login")
keychain["username"] = ""
keychain["password"] = ""
var liveUser: LiveUser?
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
liveUser = appDelegate.liveUser
}
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(liveUser ?? LiveUser(playlists: [])))
self.window = window
window.makeKeyAndVisible()
}

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,50 @@
//
// LoginController.swift
// Music Tools
//
// Created by Ellie McCarthy on 19/02/2020.
// Copyright © 2020 Sarsoo. All rights reserved.
//
import UIKit
class LoginController: UIViewController, UITextFieldDelegate {
// MARK: Properties
@IBOutlet weak var usernameField: UITextField!
@IBOutlet weak var passwordField: UITextField!
@IBOutlet weak var goButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// MARK: Actions
@IBAction func login(_ sender: UIButton) {
debugPrint(usernameField?.text)
}
// MARK: UITextFieldDelegate
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}

View File

@ -0,0 +1,17 @@
//
// LiveUser.swift
// Music Tools
//
// Created by Andy Pack on 19/02/2020.
// Copyright © 2020 Sarsoo. All rights reserved.
//
import Foundation
class LiveUser: ObservableObject {
var playlists: Array<Playlist>
init(playlists: Array<Playlist>) {
self.playlists = playlists
}
}

View File

@ -9,7 +9,7 @@
import UIKit
import SwiftyJSON
class Playlist: Identifiable {
class Playlist: Identifiable, Equatable {
//MARK: Properties
@ -70,4 +70,14 @@ class Playlist: Identifiable {
shuffle: dictionary["shuffle"].boolValue)
}
var link: String {
let uriSplit = self.uri.components(separatedBy: ":")
return "https://open.spotify.com/playlist/\(uriSplit.last ?? "")"
}
static func == (lhs: Playlist, rhs: Playlist) -> Bool {
return lhs.name == rhs.name
// && lhs.username == rhs.username
}
}

View File

@ -10,10 +10,20 @@ import Foundation
import Alamofire
import SwiftyJSON
let txTypeHeaders = ["default", "recents", "fmchart"]
public enum PlaylistType: Int {
case defaultPlaylist = 0
case recents = 1
case fmchart = 2
}
public enum PlaylistApi {
case getPlaylists
case runPlaylist(name: String)
case updatePlaylist(name: String, updates: JSON)
case deletePlaylist(name: String)
case newPlaylist(name: String, type: PlaylistType)
}
extension PlaylistApi: ApiRequest {
@ -29,6 +39,10 @@ extension PlaylistApi: ApiRequest {
return "api/playlist/run"
case .updatePlaylist:
return "api/playlist"
case .deletePlaylist:
return "api/playlist"
case .newPlaylist:
return "api/playlist"
}
}
@ -40,6 +54,10 @@ extension PlaylistApi: ApiRequest {
return .get
case .updatePlaylist:
return .post
case .deletePlaylist:
return .delete
case .newPlaylist:
return .put
}
}
@ -52,8 +70,11 @@ extension PlaylistApi: ApiRequest {
case .updatePlaylist(let name, let updates):
var txUpdates = updates
txUpdates["name"].string = name
debugPrint(txUpdates)
return txUpdates
case .deletePlaylist(let name):
return JSON(["name": name])
case .newPlaylist(let name, let type):
return JSON(["name": name, "type": txTypeHeaders[type.rawValue]])
}
}
@ -65,6 +86,10 @@ extension PlaylistApi: ApiRequest {
return URLEncodedFormParameterEncoder()
case .updatePlaylist:
return JSONParameterEncoder.default
case .deletePlaylist:
return URLEncodedFormParameterEncoder()
case .newPlaylist:
return JSONParameterEncoder.default
}
}

View File

@ -0,0 +1,90 @@
//
// AddPlaylistSheet.swift
// Music Tools
//
// Created by Andy Pack on 19/02/2020.
// Copyright © 2020 Sarsoo. All rights reserved.
//
import SwiftUI
import SwiftyJSON
struct AddPlaylistSheet: View {
@State private var selectedType = 0
@State private var name = ""
@State private var errorMessage = ""
@State private var isLoading = false
@Binding var state: Bool
@Binding var playlists: Array<Playlist>
var body: some View {
VStack {
HStack(alignment: .center) {
Text("New Playlist")
.font(.largeTitle)
.multilineTextAlignment(.center)
.padding([.top, .leading, .trailing], 20.0)
}
Picker(selection: $selectedType, label: Text("Picker")) {
Text("Default").tag(0)
Text("Recents").tag(1)
Text("Last.fm Chart").tag(2)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
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(){
if name.count == 0 {
errorMessage = "Enter Playlist Name"
return
}
var namePresent = false
for playlist in playlists {
if playlist.name == name {
namePresent = true
break
}
}
if namePresent == true {
errorMessage = "Playlist already created"
return
}
isLoading = true
let api = PlaylistApi.newPlaylist(name: self.name,
type: PlaylistType(rawValue: selectedType) ?? .defaultPlaylist)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
self.isLoading = false
self.state = false
}
}
}
struct AddPlaylistSheet_Previews: PreviewProvider {
static var previews: some View {
AddPlaylistSheet(state: .constant(true), playlists: .constant([]))
}
}

View File

@ -0,0 +1,49 @@
//
// PlaylistInputList.swift
// Music Tools
//
// Created by Andy Pack on 19/02/2020.
// Copyright © 2020 Sarsoo. All rights reserved.
//
import SwiftUI
struct Name: Identifiable {
var id = UUID()
var name: String
}
struct PlaylistInputList: View {
var names: Array<Name> = []
var nameType: String
init(names: Array<String>, nameType: String){
self.nameType = nameType
self.names = names.map { (name) -> Name in
return Name(name: name)
}.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
}
var body: some View {
return List(names) { name in
Text(name.name)
}
.navigationBarTitle(Text(nameType))
.navigationBarItems(trailing:
Button(
action: { },
label: { Image(systemName: "plus.circle") }
)
)
}
}
struct PlaylistInputList_Previews: PreviewProvider {
static var previews: some View {
PlaylistInputList(names: [
"name"
], nameType: "Spotify Playlists")
}
}

View File

@ -1,40 +0,0 @@
//
// PlaylistList.swift
// Music Tools
//
// Created by Andy Pack on 17/02/2020.
// Copyright © 2020 Sarsoo. All rights reserved.
//
import SwiftUI
struct PlaylistList: View {
var playlists: Array<Playlist>
var body: some View {
List(playlists) { playlist in
PlaylistRow(playlist: playlist)
}
}
}
struct PlaylistList_Previews: PreviewProvider {
static var previews: some View {
PlaylistList(playlists:
[
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)
]
)
}
}

View File

@ -7,15 +7,37 @@
//
import SwiftUI
import SwiftyJSON
struct PlaylistRow: View {
@EnvironmentObject var liveUser: LiveUser
var playlist: Playlist
var body: some View {
NavigationLink(destination: PlaylistView(playlist: playlist)){
HStack {
Text(playlist.name)
Spacer()
.contextMenu {
Button(action: {
let api = PlaylistApi.runPlaylist(name: self.playlist.name)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
}
}) {
Text("Refresh")
Image(systemName: "arrow.clockwise.circle")
}
Button(action: {
if let url = URL(string: self.playlist.link) {
UIApplication.shared.open(url)
}
}) {
Text("Open")
Image(systemName: "arrowshape.turn.up.right.circle")
}
}
}
}
}

View File

@ -10,108 +10,109 @@ import SwiftUI
import SwiftyJSON
struct PlaylistView: View {
init(playlist: Playlist) {
self.playlist = playlist
UITableView.appearance().tableFooterView = UIView()
}
var playlist: Playlist
@State private var recommendations: Bool = false
@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
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(playlist.name)
.font(.largeTitle)
.multilineTextAlignment(.leading)
.padding(.leading)
Text(playlist.username)
.foregroundColor(Color.gray)
.padding(.leading)
Image("PlaylistCoverImage")
.resizable()
.frame(width: 200.0, height: 200.0, alignment: .trailing)
.cornerRadius(18)
.padding(.all, 20)
List {
Section(header: Text("Options")){
Toggle(isOn: $recommendations) {
Text("Spotify Recommendations")
}
.padding()
if recommendations {
// if recommendations {
Stepper(onIncrement: {
self.$rec_num.wrappedValue += 1
self.updatePlaylist(updates: JSON(["recommendation_sample": self.$rec_num.wrappedValue]))
},
onDecrement: {
self.$rec_num.wrappedValue -= 1
self.updatePlaylist(updates: JSON(["recommendation_sample": self.$rec_num.wrappedValue]))
self.updatePlaylist(updates: JSON(["recommendation_sample": self.$rec_num.wrappedValue]))
}){
Text("#:")
.foregroundColor(Color.gray)
.multilineTextAlignment(.trailing)
.padding(.leading, 20)
Text("\(rec_num)")
.multilineTextAlignment(.trailing)
}.padding()
}
}
// }
Toggle(isOn: $library_Tracks) {
Text("Library Tracks")
}.padding()
}
Toggle(isOn: $shuffle) {
Text("Shuffle")
}.padding()
HStack {
Button(action: { self.runPlaylist() }) {
Text("Update")
}.padding().multilineTextAlignment(.center)
}
}
Section(header: Text("Inputs")){
NavigationLink(destination: PlaylistInputList(names: self.playlist.playlist_references, nameType: "Managed Playlists")) {
HStack {
Text("Managed Playlists")
Spacer()
Text("\(self.playlist.playlist_references.count)")
.foregroundColor(Color.gray)
}
}
Spacer()
NavigationLink(destination: PlaylistInputList(names: self.playlist.parts, nameType: "Spotify Playlists")) {
HStack {
Text("Spotify Playlists")
Spacer()
Text("\(self.playlist.parts.count)")
.foregroundColor(Color.gray)
}
}
}
.padding(.vertical)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
.onAppear {
self.$recommendations.wrappedValue = self.playlist.include_recommendations
self.$library_Tracks.wrappedValue = self.playlist.include_library_tracks
self.$shuffle.wrappedValue = self.playlist.shuffle
Section(header: Text("Actions")){
Button(action: { self.runPlaylist() }) {
Text("Update")
}
self.$rec_num.wrappedValue = self.playlist.recommendation_sample
Button(action: { self.openPlaylist() }) {
Text("Open")
}
}
}
.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
}
}
func runPlaylist() {
let api = PlaylistApi.runPlaylist(name: playlist.name)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
guard let data = response.data else {
fatalError("error getting playlists")
}
guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse")
}
}
//TODO: do better error checking
}
func openPlaylist() {
if let url = URL(string: self.playlist.link) {
UIApplication.shared.open(url)
}
}
func updatePlaylist(updates: JSON) {
let api = PlaylistApi.updatePlaylist(name: playlist.name, updates: updates)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
guard let data = response.data else {
fatalError("error getting playlists")
}
guard let json = try? JSON(data: data) else {
fatalError("error parsing reponse")
}
}
//TODO: do better error checking
}

View File

@ -11,16 +11,56 @@ import Alamofire
import SwiftyJSON
struct RootView: View {
@EnvironmentObject var liveUser: LiveUser
@State private var selection = 0
@State private var playlists: Array<Playlist> = []
@State private var isLoading = true
@State private var showAdd = false
@State private var onClose = onSheetClose
@State private var justDeleted: Array<Playlist> = []
func onSheetClose() {
self.fetch()
return
}
let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
TabView(selection: $selection){
TabView {
NavigationView {
List(playlists) { playlist in
PlaylistRow(playlist: playlist)
List{
ForEach(playlists) { playlist in
PlaylistRow(playlist: playlist)
}
.onDelete { indexSet in
indexSet.forEach { index in
self.justDeleted.append(self.playlists[index])
let api = PlaylistApi.deletePlaylist(name: self.playlists[index].name)
RequestBuilder.buildRequest(apiRequest: api).responseJSON{ response in
}
}
self.playlists.remove(atOffsets: indexSet)
}
}
.navigationBarTitle(Text("Playlists").font(.title))
.navigationBarItems(trailing:
Button(
action: { self.showAdd = true },
label: { Text("Add") }
).sheet(isPresented: $showAdd) {
AddPlaylistSheet(state: self.$showAdd, playlists: self.$playlists)
}
)
}
.tabItem {
VStack {
@ -57,6 +97,9 @@ struct RootView: View {
}
}
.tag(2)
.onReceive(timer) { _ in
self.fetch()
}
}.onAppear {
self.fetch()
}
@ -74,9 +117,23 @@ struct RootView: View {
fatalError("error parsing reponse")
}
self.playlists = json["playlists"].arrayValue.map({ dict in
let playlists = json["playlists"].arrayValue.map({ dict in
Playlist.fromDict(dictionary: dict)
}).sorted(by: { $0.name.lowercased() < $1.name.lowercased() })
.filter { (rxPlaylist) -> Bool in
var deleted = false
for playlist in self.justDeleted {
if playlist == rxPlaylist {
deleted = true
}
}
return !deleted
}
self.justDeleted = []
self.liveUser.playlists = playlists
self.playlists = self.liveUser.playlists
}
//TODO: do better error checking
}