Skip to content

Commit

Permalink
async search works
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiayang committed Oct 21, 2020
1 parent c8a1bde commit f28ecb4
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 64 deletions.
4 changes: 2 additions & 2 deletions MoeStreamer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.11.0;
MARKETING_VERSION = 0.12.0;
PRODUCT_BUNDLE_IDENTIFIER = com.zhiayang.MoeStreamer;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand Down Expand Up @@ -599,7 +599,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.11.0;
MARKETING_VERSION = 0.12.0;
PRODUCT_BUNDLE_IDENTIFIER = com.zhiayang.MoeStreamer;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand Down
18 changes: 16 additions & 2 deletions MoeStreamer/src/ServiceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Licensed under the Apache License Version 2.0.

import Cocoa
import SwiftUI
import Foundation
import UserNotifications

Expand Down Expand Up @@ -97,8 +98,7 @@ protocol ServiceController : AnyObject
func audioController() -> AudioController
func getCapabilities() -> ServiceCapabilities

func searchSongs(name: String) -> [Song];

func searchSongs(name: String, into: Binding<[Song]>, onComplete: @escaping () -> Void)
func setNextSong(_ song: Song, immediately: Bool)

init(viewModel: ViewModel?)
Expand All @@ -107,6 +107,20 @@ protocol ServiceController : AnyObject
func getViewModel()-> ViewModel?
}

extension ServiceController
{
func nextSong()
{
}

func searchSongs(name: String, into: Binding<[Song]>, onComplete: @escaping () -> Void)
{
}

func setNextSong(_ song: Song, immediately: Bool)
{
}
}

class Notifier
{
Expand Down
16 changes: 1 addition & 15 deletions MoeStreamer/src/backend/ListenMoe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import Just
import Cocoa
import SwiftUI
import Foundation
import Starscream
import SwiftyJSON
Expand Down Expand Up @@ -314,16 +315,6 @@ class ListenMoeController : ServiceController, WebSocketDelegate
return self.audioCon
}

func searchSongs(name: String) -> [Song]
{
return []
}

func setNextSong(_ song: Song, immediately: Bool)
{
// nothing
}

func sessionLogin(activityView: ViewModel?, force: Bool)
{
// try to login again.
Expand Down Expand Up @@ -379,11 +370,6 @@ class ListenMoeController : ServiceController, WebSocketDelegate
self.pingTimer?.invalidate()
}

func nextSong()
{
// we cannot
}

func getCurrentSong() -> Song?
{
return self.currentSong
Expand Down
73 changes: 49 additions & 24 deletions MoeStreamer/src/backend/LocalMusic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright (c) 2020, zhiayang
// Licensed under the Apache License Version 2.0.

import SwiftUI
import Foundation
import iTunesLibrary

Expand Down Expand Up @@ -132,34 +133,51 @@ class LocalMusicController : ServiceController
}
}

func searchSongs(name: String) -> [Song]
// TODO: search songs asynchronously
// func searchSongs(name: String) -> [Song]
// {
// let searchWords = name.words.map({ $0.lowercased() })
//
// return self.songs.filter { (item: MusicItem) -> Bool in
//
// let titleWords = item.song.title.words.map({ $0.lowercased() })
// return searchWords.allSatisfy { (word: String) -> Bool in
// titleWords.contains(where: { $0.hasPrefix(word) })
// }
//
// }.map { $0.song }
// }

func searchSongs(name: String, into: Binding<[Song]>, onComplete: @escaping () -> Void)
{
let searchWords = name.words.map({ $0.lowercased() })

return self.songs.filter { (item: MusicItem) -> Bool in
if name.isEmpty
{
into.wrappedValue = []
onComplete()

let titleWords = item.song.title.words.map({ $0.lowercased() })
return searchWords.allSatisfy { (word: String) -> Bool in
titleWords.contains(where: { $0.hasPrefix(word) })
}
return
}

}.map { $0.song }
DispatchQueue.global().async {

// ideally this would work, but it doesn't
/*
NSArray* list = @[@"test this",@"hello world",@"bye hello",@"helloween"];
NSString* search = [NSString stringWithFormat:@".*\\b%@\\b.*", [NSRegularExpression escapedPatternForString:@"hello"]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",search];
NSArray* filtered = [list filteredArrayUsingPredicate:predicate];
NSLog(@"%lu",(unsigned long)[filtered count]);
for(NSString* filter in filtered){
NSLog(@"%@",filter);
Logger.log(msg: "searching for: \(name)")
let searchWords = name.words.map({ $0.lowercased() })

let search = ".*\\b\(NSRegularExpression.escapedPattern(for: name))\\b.*"
let pred = NSPredicate(format: "SELF.songTitle MATCHES %@", search as NSString)
// i don't believe swift's map/filter are lazy, so just use a for loop
// so we can append iteratively.
for song in self.songs
{
let titleWords = song.song.title.words.map({ $0.lowercased() })
if searchWords.allSatisfy({ word -> Bool in
titleWords.contains(where: { $0.hasPrefix(word) })
}) {
into.wrappedValue.append(song.song)
}
}

let list = (self.songs as NSArray).filtered(using: pred).map { ($0 as! MusicItem).song.title }
*/
Logger.log(msg: "search: found \(into.wrappedValue.count) song\(into.wrappedValue.count == 1 ? "" : "s")")
onComplete()
}
}

func setNextSong(_ song: Song, immediately: Bool)
Expand All @@ -168,8 +186,15 @@ class LocalMusicController : ServiceController

Logger.log(msg: "queued: \(song.title)")

self.manuallyQueuedSongs.append(item)
if immediately { self.nextSong() }
if immediately
{
self.manuallyQueuedSongs.insert(item, at: 0)
self.nextSong()
}
else
{
self.manuallyQueuedSongs.append(item)
}
}
}

Expand Down
76 changes: 55 additions & 21 deletions MoeStreamer/src/ui/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
import SwiftUI
import Foundation

enum SearchState
{
case None
case InProgress
case Done
}

struct SearchView : View
{
@State var searchString: String = ""
@State var searchField: NSTextField! = nil
@State var searchField: NSSearchField! = nil
@State var scrollPos: CGPoint? = nil
@State var didSearch: Bool = false
@State var searchState: SearchState = .None
@State var searchResults: [Song] = []

@Binding var musicCon: ServiceController
Expand All @@ -28,25 +35,52 @@ struct SearchView : View
self._musicCon = musicCon
}

private func performSearch(with name: String)
{
self.searchResults = []
self.searchState = .InProgress
self.musicCon.searchSongs(name: name, into: self.$searchResults, onComplete: {
DispatchQueue.main.async {
self.searchState = .Done
}
})
}

var body: some View {
VStack(spacing: 5) {
BetterTextField<NSTextField>(
placeholder: "search", text: self.$searchString, field: self.$searchField,
onEnter: { (_, field: NSTextField) in
self.searchResults = self.musicCon.searchSongs(name: field.stringValue)
self.didSearch = true
})
.frame(width: 200)
.onAppear(perform: {
self.didSearch = false
DispatchQueue.main.async {
self.searchField.window?.makeFirstResponder(self.searchField)
}
})
ZStack() {
BetterTextField<NSSearchField>(
placeholder: "search", text: self.$searchString, field: self.$searchField,
setupField: {
$0.sendsWholeSearchString = true
$0.sendsSearchStringImmediately = false

($0.cell as! NSSearchFieldCell).sendsWholeSearchString = true
($0.cell as! NSSearchFieldCell).sendsSearchStringImmediately = false
},
onEnter: { (_, field: NSSearchField) in
self.performSearch(with: field.stringValue)
})
.frame(width: 200)
.onAppear(perform: {
self.searchState = .None
DispatchQueue.main.async {
self.searchField.window?.makeFirstResponder(self.searchField)
}
}).frame(alignment: .center)

if self.searchState == .InProgress
{
ActivityIndicator(size: .small)
.frame(width: 20, height: 20)
.padding(.leading, 240)
}
}
.frame(maxWidth: .infinity)

Spacer()

if self.didSearch && self.searchResults.isEmpty
if self.searchState == .Done && self.searchResults.isEmpty
{
Text("no results")
.frame(height: 30)
Expand Down Expand Up @@ -83,7 +117,7 @@ struct SearchView : View
.frame(width: 18, height: 18)
.foregroundColor(self.iconColour)
}
// .buttonStyle(PlainButtonStyle())
.buttonStyle(PlainButtonStyle())
.tooltip("play the song now")

Button(action: {
Expand All @@ -95,16 +129,16 @@ struct SearchView : View
.foregroundColor(self.iconColour)
.padding(.leading, 4)
}
// .buttonStyle(PlainButtonStyle())
.buttonStyle(PlainButtonStyle())
.tooltip("play after the current song finishes")
}.padding(.trailing, 10)
}.padding(.trailing, 15)

}.frame(height: 50)
}
.frame(maxWidth: .infinity, alignment: .leading)

}.frame(width: 320)
}.frame(minHeight: self.searchResults.isEmpty ? 10 : 150, maxHeight: 400)
}
.frame(minHeight: self.searchResults.isEmpty ? 10 : 150, maxHeight: 400)
}

Spacer()
Expand Down
3 changes: 3 additions & 0 deletions MoeStreamer/src/ui/wrappers/BetterTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ struct BetterTextField<FieldType: NSTextField> : NSViewRepresentable
@Binding var field: FieldType?

var placeholder: String
var setupField: ((FieldType) -> Void)? = nil
var changeHandler: ((String, FieldType) -> Void)? = nil
var finishHandler: ((String, FieldType) -> Void)? = nil
var enterHandler: ((String, FieldType) -> Void)? = nil

init(placeholder: String, text: Binding<String>, field: Binding<FieldType?>,
setupField: ((FieldType) -> Void)? = nil,
onTextChanged: ((String, FieldType) -> Void)? = nil,
onFinishEditing: ((String, FieldType) -> Void)? = nil,
onEnter: ((String, FieldType) -> Void)? = nil)
Expand All @@ -43,6 +45,7 @@ struct BetterTextField<FieldType: NSTextField> : NSViewRepresentable

DispatchQueue.main.async {
self.field = textField
self.setupField?(self.field!)
}

return textField
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ A tiny macOS app that sits in the menubar, to stream music from [LISTEN.moe](htt

# License

Contributions from `my_cat_is_ugly` on twitch

Code is licensed under the Apache License Version 2.
Icons are from Google's [material.io](https://material.io/resources/icons/), which are similarly licensed.

Expand Down

0 comments on commit f28ecb4

Please sign in to comment.