~not/sonar

7cdd9f6c3cf878d6aafb60a7d32bdfcafb43b34d — b123400 a month ago db75158 + 96678ab
Merge branch 'search'
M Sonar.xcodeproj/project.pbxproj => Sonar.xcodeproj/project.pbxproj +12 -0
@@ 33,6 33,7 @@
		CFB55EA92781C39B004F9C47 /* BRSonicAPIResponsePlaylistSongs.m in Sources */ = {isa = PBXBuildFile; fileRef = CFB55EA82781C39B004F9C47 /* BRSonicAPIResponsePlaylistSongs.m */; };
		CFB55EAF2781DBA6004F9C47 /* Playlist.m in Sources */ = {isa = PBXBuildFile; fileRef = CFB55EAE2781DBA6004F9C47 /* Playlist.m */; };
		CFC62B2428ACB1F4006E08B6 /* SonarApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = CFC62B2328ACB1F4006E08B6 /* SonarApplication.m */; };
		CFC743762BB315D10053ABC1 /* PlayerSearchFieldCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CFC743752BB315D10053ABC1 /* PlayerSearchFieldCell.m */; };
		CFD1C4D8273E958B009ED1CD /* BRSonicAPIArtist.m in Sources */ = {isa = PBXBuildFile; fileRef = CFD1C4D7273E958B009ED1CD /* BRSonicAPIArtist.m */; };
		CFD908B62732B1F600FEFD53 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CFD908B52732B1F600FEFD53 /* AppDelegate.m */; };
		CFD908B82732B1F800FEFD53 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CFD908B72732B1F800FEFD53 /* Assets.xcassets */; };


@@ 40,6 41,7 @@
		CFD908BD2732B1F800FEFD53 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CFD908BC2732B1F800FEFD53 /* main.m */; };
		CFD908C72732C5E800FEFD53 /* PlayerWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CFD908C52732C5E800FEFD53 /* PlayerWindowController.m */; };
		CFD908CB2732CC2800FEFD53 /* PlayerHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = CFD908CA2732CC2800FEFD53 /* PlayerHeaderView.m */; };
		CFDB4AD22BAB117D00A36AEE /* BRSonicAPIResponseSearchResult.m in Sources */ = {isa = PBXBuildFile; fileRef = CFDB4AD12BAB117D00A36AEE /* BRSonicAPIResponseSearchResult.m */; };
		CFDC05452743BE820065F13D /* BRSonicAPISong.m in Sources */ = {isa = PBXBuildFile; fileRef = CFDC05442743BE820065F13D /* BRSonicAPISong.m */; };
		CFDC05482743BE9A0065F13D /* BRSonicAPIResponseSongs.m in Sources */ = {isa = PBXBuildFile; fileRef = CFDC05472743BE9A0065F13D /* BRSonicAPIResponseSongs.m */; };
		CFDC054B2743C0A50065F13D /* BRSonicAPIResponseAlbums.m in Sources */ = {isa = PBXBuildFile; fileRef = CFDC054A2743C0A50065F13D /* BRSonicAPIResponseAlbums.m */; };


@@ 110,6 112,8 @@
		CFB55EAE2781DBA6004F9C47 /* Playlist.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Playlist.m; sourceTree = "<group>"; };
		CFC62B2228ACB1F4006E08B6 /* SonarApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SonarApplication.h; sourceTree = "<group>"; };
		CFC62B2328ACB1F4006E08B6 /* SonarApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SonarApplication.m; sourceTree = "<group>"; };
		CFC743742BB315D10053ABC1 /* PlayerSearchFieldCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlayerSearchFieldCell.h; sourceTree = "<group>"; };
		CFC743752BB315D10053ABC1 /* PlayerSearchFieldCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlayerSearchFieldCell.m; sourceTree = "<group>"; };
		CFD1C4D6273E958B009ED1CD /* BRSonicAPIArtist.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BRSonicAPIArtist.h; sourceTree = "<group>"; };
		CFD1C4D7273E958B009ED1CD /* BRSonicAPIArtist.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BRSonicAPIArtist.m; sourceTree = "<group>"; };
		CFD908B12732B1F600FEFD53 /* Sonar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sonar.app; sourceTree = BUILT_PRODUCTS_DIR; };


@@ 123,6 127,8 @@
		CFD908C52732C5E800FEFD53 /* PlayerWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlayerWindowController.m; sourceTree = "<group>"; };
		CFD908C92732CC2800FEFD53 /* PlayerHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlayerHeaderView.h; sourceTree = "<group>"; };
		CFD908CA2732CC2800FEFD53 /* PlayerHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlayerHeaderView.m; sourceTree = "<group>"; };
		CFDB4AD02BAB117D00A36AEE /* BRSonicAPIResponseSearchResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BRSonicAPIResponseSearchResult.h; sourceTree = "<group>"; };
		CFDB4AD12BAB117D00A36AEE /* BRSonicAPIResponseSearchResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BRSonicAPIResponseSearchResult.m; sourceTree = "<group>"; };
		CFDC05432743BE820065F13D /* BRSonicAPISong.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BRSonicAPISong.h; sourceTree = "<group>"; };
		CFDC05442743BE820065F13D /* BRSonicAPISong.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BRSonicAPISong.m; sourceTree = "<group>"; };
		CFDC05462743BE9A0065F13D /* BRSonicAPIResponseSongs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BRSonicAPIResponseSongs.h; sourceTree = "<group>"; };


@@ 275,6 281,8 @@
				CFEC2EAC273D1A5C002D5BD0 /* PlayerVolumeSliderCell.m */,
				CFEC3AF628EA7FBB004A4C6A /* PlayerViewModeState.h */,
				CFEC3AF728EA7FBB004A4C6A /* PlayerViewModeState.m */,
				CFC743742BB315D10053ABC1 /* PlayerSearchFieldCell.h */,
				CFC743752BB315D10053ABC1 /* PlayerSearchFieldCell.m */,
			);
			path = Player;
			sourceTree = "<group>";


@@ 314,6 322,8 @@
				CF5849B42A41891A00B494C7 /* BRSonicAPIResponsePlaylist.m */,
				CFE160E4279C386E00287C14 /* BRSonicAPIResponseStarred.h */,
				CFE160E5279C386E00287C14 /* BRSonicAPIResponseStarred.m */,
				CFDB4AD02BAB117D00A36AEE /* BRSonicAPIResponseSearchResult.h */,
				CFDB4AD12BAB117D00A36AEE /* BRSonicAPIResponseSearchResult.m */,
				CFD1C4D6273E958B009ED1CD /* BRSonicAPIArtist.h */,
				CFD1C4D7273E958B009ED1CD /* BRSonicAPIArtist.m */,
				CFDC054C2743C0B90065F13D /* BRSonicAPIAlbum.h */,


@@ 473,11 483,13 @@
				CF5849B52A41891A00B494C7 /* BRSonicAPIResponsePlaylist.m in Sources */,
				CF252E90273E61BE00A0D26C /* MediaDB.m in Sources */,
				CF3EA281273369E70010BBE4 /* PlayerHeaderMusicImageView.m in Sources */,
				CFC743762BB315D10053ABC1 /* PlayerSearchFieldCell.m in Sources */,
				CFD908CB2732CC2800FEFD53 /* PlayerHeaderView.m in Sources */,
				CFB55EA62781BDE3004F9C47 /* BRSonicAPIPlaylist.m in Sources */,
				CFDC05512743C3870065F13D /* Album.m in Sources */,
				CF252E93273E634200A0D26C /* Artist.m in Sources */,
				CFC62B2428ACB1F4006E08B6 /* SonarApplication.m in Sources */,
				CFDB4AD22BAB117D00A36AEE /* BRSonicAPIResponseSearchResult.m in Sources */,
				CFDC054B2743C0A50065F13D /* BRSonicAPIResponseAlbums.m in Sources */,
				CFB55EAF2781DBA6004F9C47 /* Playlist.m in Sources */,
				CFDC054E2743C0B90065F13D /* BRSonicAPIAlbum.m in Sources */,

M Sonar/API Client/BRSonicAPIClient.h => Sonar/API Client/BRSonicAPIClient.h +5 -0
@@ 14,6 14,7 @@
#import "BRSonicAPIResponsePlaylist.h"
#import "BRSonicAPIResponsePlaylistSongs.h"
#import "BRSonicAPIResponseStarred.h"
#import "BRSonicAPIResponseSearchResult.h"

NS_ASSUME_NONNULL_BEGIN



@@ 53,6 54,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)getStarredsWithCredential:(BRSonicAPICredential *)credential
                       completion:(void (^) (NSError * _Nullable error, BRSonicAPIResponseStarred *response))callback;

- (void)searchWithKeyword:(NSString *)keyword
               credential:(BRSonicAPICredential *)credential
               completion:(void (^) (NSError * _Nullable error, BRSonicAPIResponseSearchResult *response))callback;

- (void)starWithItem:(id)item
      withCredential:(BRSonicAPICredential *)credential
          completion:(void (^) (NSError * _Nullable error))callback;

M Sonar/API Client/BRSonicAPIClient.m => Sonar/API Client/BRSonicAPIClient.m +40 -0
@@ 373,6 373,46 @@
    }] resume];
}

- (void)searchWithKeyword:(NSString *)keyword
               credential:(BRSonicAPICredential *)credential
               completion:(void (^) (NSError * _Nullable error, BRSonicAPIResponseSearchResult *response))callback {
    NSURLComponents *components = [NSURLComponents componentsWithURL:credential.host resolvingAgainstBaseURL:NO];
    [components setPath:@"/rest/search3"];
    
    [components setQueryItems:
     [[credential baseUrlQueryItems] arrayByAddingObjectsFromArray:@[
        [NSURLQueryItem queryItemWithName:@"query" value:keyword],
        [NSURLQueryItem queryItemWithName:@"artistCount" value:@"500"],
        [NSURLQueryItem queryItemWithName:@"albumCount" value:@"500"],
        [NSURLQueryItem queryItemWithName:@"songCount" value:@"500"]
     ]]];
    
    [[[NSURLSession sharedSession] dataTaskWithURL:components.URL
                                 completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            callback(error, nil);
            return;
        }
        NSError *rError = nil;
        BRSonicAPIResponseSearchResult *xml = [[BRSonicAPIResponseSearchResult alloc] initWithData:data
                                                                                           options:NSXMLNodeOptionsNone
                                                                                             error:&rError];
        if (rError) {
            callback(rError, nil);
            return;
        }
        if (xml.status == BRSonicAPIResponseStatusFailed) {
            callback(xml.error ?: [NSError errorWithDomain:@"net.b123400.sonic"
                                                      code:0
                                                  userInfo:@{
                NSLocalizedDescriptionKey: NSLocalizedString(@"Failed", @"")
            }], nil);
            return;
        }
        callback(nil, xml);
    }] resume];
}

- (void)starWithItem:(id)item
      withCredential:(BRSonicAPICredential *)credential
          completion:(void (^) (NSError * _Nullable error))callback {

A Sonar/API Client/BRSonicAPIResponseSearchResult.h => Sonar/API Client/BRSonicAPIResponseSearchResult.h +18 -0
@@ 0,0 1,18 @@
//
//  BRSonicAPIResponseSearchResult.h
//  Sonar
//
//  Created by b123400 on 2024/03/20.
//

#import "BRSonicAPIResponse.h"

NS_ASSUME_NONNULL_BEGIN

@interface BRSonicAPIResponseSearchResult : BRSonicAPIResponse

- (NSArray*)items;

@end

NS_ASSUME_NONNULL_END

A Sonar/API Client/BRSonicAPIResponseSearchResult.m => Sonar/API Client/BRSonicAPIResponseSearchResult.m +33 -0
@@ 0,0 1,33 @@
//
//  BRSonicAPIResponseSearchResult.m
//  Sonar
//
//  Created by b123400 on 2024/03/20.
//

#import "BRSonicAPIResponseSearchResult.h"
#import "BRSonicAPISong.h"
#import "BRSonicAPIAlbum.h"
#import "BRSonicAPIArtist.h"

@implementation BRSonicAPIResponseSearchResult

- (NSArray *)items {
    NSXMLElement *starredElement = [[self.rootElement elementsForName:@"searchResult3"] firstObject];
    NSMutableArray *items = [NSMutableArray array];
    for (NSXMLElement *childElement in starredElement.children) {
        if ([[childElement name] isEqualTo:@"artist"]) {
            BRSonicAPIArtist *a = [[BRSonicAPIArtist alloc] initWithXMLElement:childElement];
            [items addObject:a];
        } else if ([[childElement name] isEqualTo:@"album"]) {
            BRSonicAPIAlbum *a = [[BRSonicAPIAlbum alloc] initWithXMLElement:childElement];
            [items addObject:a];
        } else if ([[childElement name] isEqualTo:@"song"]) {
            BRSonicAPISong *a = [[BRSonicAPISong alloc] initWithXMLElement:childElement];
            [items addObject:a];
        }
    }
    return items;
}

@end

M Sonar/Base.lproj/MainMenu.xib => Sonar/Base.lproj/MainMenu.xib +7 -2
@@ 1,8 1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21225" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
    <dependencies>
        <deployment identifier="macosx"/>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21225"/>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
    </dependencies>
    <objects>
        <customObject id="-2" userLabel="File's Owner" customClass="SonarApplication">


@@ 325,6 325,11 @@
                                    <action selector="menubarAlbumsClicked:" target="-1" id="Uay-GB-mwu"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Search" keyEquivalent="5" id="9vo-yp-jvp">
                                <connections>
                                    <action selector="menubarSearchClicked:" target="-1" id="Uay-GB-mxu"/>
                                </connections>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
                            <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
                                <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>

M Sonar/MediaDB/MediaDB.h => Sonar/MediaDB/MediaDB.h +4 -0
@@ 58,6 58,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)listStarredItems:(void (^)(NSError * _Nullable error, NSArray<BaseModel*>* _Nullable items))callback
             fetchRemote:(BOOL)fetch;

- (void)searchWithKeyword:(NSString *)keyword
               completion:(void (^)(NSError * _Nullable error, NSArray<BaseModel*>* _Nullable items))callback
              fetchRemote:(BOOL)fetch;

- (void)starItem:(BaseModel *)item
      completion:(void (^) (NSError * _Nullable error, id updatedItem))callback;


M Sonar/MediaDB/MediaDB.m => Sonar/MediaDB/MediaDB.m +70 -0
@@ 284,6 284,40 @@
    }];
}

- (void)searchWithKeyword:(NSString *)keyword
               completion:(void (^)(NSError * _Nullable error, NSArray<BaseModel*>* _Nullable items))callback
              fetchRemote:(BOOL)fetch {
    BRSonicAPICredential *c = [[PreferencesManager shared] credential];
    if (!c) return;
    NSArray *cached = [self localSearchResultWithQuery:keyword];
    callback(nil, cached);
    if (!fetch) return;
    typeof(self) __weak weakSelf = self;
    [[BRSonicAPIClient shared] searchWithKeyword:keyword
                                      credential:c
                                      completion:^(NSError * _Nullable error, BRSonicAPIResponseSearchResult * _Nonnull response) {
        if (error) {
            callback(error, nil);
            return;
        }
        if (response) {
            for (id item in response.items) {
                if ([item isKindOfClass:[BRSonicAPISong class]]) {
                    weakSelf.songById[[item id]] = item;
                } else if ([item isKindOfClass:[BRSonicAPIAlbum class]]) {
                    weakSelf.albumById[[item id]] = item;
                } else if ([item isKindOfClass:[BRSonicAPIArtist class]]) {
                    weakSelf.artistById[[item id]] = item;
                }
            }
            NSArray *newItems = [self localSearchResultWithQuery:keyword];
            if (![cached isEqualTo:newItems]) {
                callback(nil, newItems);
            }
        }
    }];
}

- (void)starItem:(BaseModel *)item
      completion:(void (^) (NSError * _Nullable error, id updatedItem))callback {
    BRSonicAPICredential *c = [[PreferencesManager shared] credential];


@@ 534,4 568,40 @@
    return result;
}

- (NSArray *)localSearchResultWithQuery:(NSString *)query {
    BRSonicAPICredential *c = [[PreferencesManager shared] credential];
    NSMutableArray *result = [NSMutableArray array];
    query = query.lowercaseString;
    for (NSString *key in self.artistById) {
        BRSonicAPIArtist *a = self.artistById[key];
        if ([a.name.lowercaseString containsString:query]) {
            Artist *artist = [[Artist alloc] initWithAPIResponse:a];
            artist.albums = [self localAlbumsOfArtist:a];
            [result addObject:artist];
        }
    }
    for (NSString *key in self.albumById) {
        BRSonicAPIAlbum *a = self.albumById[key];
        if ([a.name.lowercaseString containsString:query]) {
            Album *album = [[Album alloc] initWithAPIResponse:a
                                                  coverArtURL:[[BRSonicAPIClient shared] urlForCoverArtId:a.coverArt
                                                                                           withCredential:c]];
            album.songs = [self localSongsOfAlbum:a];
            [result addObject:album];
        }
    }
    for (NSString *key in self.songById) {
        BRSonicAPISong *s = self.songById[key];
        if ([s.title.lowercaseString containsString:query]) {
            Song *song = [[Song alloc] initWithAPIResponse:s
                                               coverArtURL:[[BRSonicAPIClient shared] urlForCoverArtId:s.coverArt
                                                                                        withCredential:c]
                                                 streamURL:[[BRSonicAPIClient shared] urlForStreaming:s
                                                                                       withCredential:c]];
            [result addObject:song];
        }
    }
    return result;
}

@end

M Sonar/Player/Base.lproj/PlayerWindowController.xib => Sonar/Player/Base.lproj/PlayerWindowController.xib +73 -39
@@ 1,8 1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21225" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
    <dependencies>
        <deployment identifier="macosx"/>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21225"/>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
        <capability name="Image references" minToolsVersion="12.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>


@@ 30,6 30,9 @@
                <outlet property="progressSlider" destination="xON-bw-Mdt" id="hKJ-TJ-dQo"/>
                <outlet property="randomButton" destination="oii-5P-0gx" id="gpc-Ee-nGW"/>
                <outlet property="repeatButton" destination="RCZ-ri-j8B" id="647-y2-fcD"/>
                <outlet property="searchField" destination="hBq-6T-efE" id="9No-8x-EJX"/>
                <outlet property="searchFieldHeightConstraint" destination="pnQ-h2-PiO" id="cRN-S8-9XQ"/>
                <outlet property="searchModeButton" destination="ep7-63-3EP" id="vda-Ns-eFL"/>
                <outlet property="songNameLabel" destination="VBU-8f-bth" id="jcT-oD-R6n"/>
                <outlet property="songsModeButton" destination="0w1-yG-eRB" id="vrz-4r-4vt"/>
                <outlet property="totalDurationLabel" destination="3oy-ZU-bX6" id="ALx-QX-XM3"/>


@@ 55,21 58,24 @@
                            <customView wantsLayer="YES" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="cEF-5D-l59">
                                <rect key="frame" x="0.0" y="0.0" width="400" height="174"/>
                                <subviews>
                                    <visualEffectView wantsLayer="YES" blendingMode="withinWindow" material="headerView" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="lAy-fQ-Mus">
                                        <rect key="frame" x="0.0" y="148" width="400" height="26"/>
                                    <visualEffectView wantsLayer="YES" blendingMode="behindWindow" material="headerView" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="lAy-fQ-Mus">
                                        <rect key="frame" x="0.0" y="122" width="400" height="52"/>
                                    </visualEffectView>
                                    <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="OuU-bf-WX6">
                                        <rect key="frame" x="12" y="152" width="68" height="18"/>
                                        <rect key="frame" x="12" y="152" width="67" height="18"/>
                                        <buttonCell key="cell" type="inline" title="Favourites" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="lhn-HM-t6j">
                                            <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                            <font key="font" metaFont="smallSystemBold"/>
                                        </buttonCell>
                                        <constraints>
                                            <constraint firstAttribute="height" constant="18" id="wxr-Cr-7Rl"/>
                                        </constraints>
                                        <connections>
                                            <action selector="favouritesModeClicked:" target="-2" id="yyV-PH-fXv"/>
                                        </connections>
                                    </button>
                                    <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9fv-nj-PFz">
                                        <rect key="frame" x="152" y="152" width="48" height="18"/>
                                        <rect key="frame" x="151" y="152" width="47" height="18"/>
                                        <buttonCell key="cell" type="inline" title="Artists" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="MWK-pU-Mco">
                                            <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                            <font key="font" metaFont="smallSystemBold"/>


@@ 79,7 85,7 @@
                                        </connections>
                                    </button>
                                    <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xQd-RQ-cMq">
                                        <rect key="frame" x="208" y="152" width="52" height="18"/>
                                        <rect key="frame" x="206" y="152" width="52" height="18"/>
                                        <buttonCell key="cell" type="inline" title="Albums" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="ZNb-vG-Fw3">
                                            <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                            <font key="font" metaFont="smallSystemBold"/>


@@ 89,7 95,7 @@
                                        </connections>
                                    </button>
                                    <button hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0w1-yG-eRB">
                                        <rect key="frame" x="268" y="152" width="46" height="18"/>
                                        <rect key="frame" x="323" y="152" width="45" height="18"/>
                                        <buttonCell key="cell" type="inline" title="Songs" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="cCz-p4-qFI">
                                            <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                            <font key="font" metaFont="smallSystemBold"/>


@@ 98,8 104,18 @@
                                            <action selector="songsModeClicked:" target="-2" id="6aU-sj-cm2"/>
                                        </connections>
                                    </button>
                                    <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ep7-63-3EP">
                                        <rect key="frame" x="266" y="152" width="49" height="18"/>
                                        <buttonCell key="cell" type="inline" title="Search" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="lPI-8O-1ri">
                                            <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                            <font key="font" metaFont="smallSystem"/>
                                        </buttonCell>
                                        <connections>
                                            <action selector="searchModeClicked:" target="-2" id="4CA-81-7ab"/>
                                        </connections>
                                    </button>
                                    <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="vMR-N3-Rxj">
                                        <rect key="frame" x="88" y="152" width="56" height="18"/>
                                        <rect key="frame" x="87" y="152" width="56" height="18"/>
                                        <buttonCell key="cell" type="inline" title="Playlists" bezelStyle="inline" alignment="center" borderStyle="border" inset="2" id="FXn-Ly-bDx">
                                            <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                            <font key="font" metaFont="smallSystemBold"/>


@@ 108,15 124,28 @@
                                            <action selector="playlistsModeClicked:" target="-2" id="zME-iS-vTd"/>
                                        </connections>
                                    </button>
                                    <scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="24" horizontalPageScroll="10" verticalLineScroll="24" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="UXZ-Oi-NW8">
                                        <rect key="frame" x="0.0" y="0.0" width="400" height="148"/>
                                        <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" heightSizable="YES"/>
                                    <searchField wantsLayer="YES" verticalHuggingPriority="750" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hBq-6T-efE">
                                        <rect key="frame" x="20" y="126" width="360" height="22"/>
                                        <constraints>
                                            <constraint firstAttribute="height" constant="22" id="pnQ-h2-PiO"/>
                                        </constraints>
                                        <searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" id="NEh-Om-iU6" customClass="PlayerSearchFieldCell">
                                            <font key="font" metaFont="system"/>
                                            <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
                                            <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
                                        </searchFieldCell>
                                        <connections>
                                            <action selector="didEnteredSearchQuery:" target="-2" id="ooq-7G-zkn"/>
                                        </connections>
                                    </searchField>
                                    <scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="24" horizontalPageScroll="10" verticalLineScroll="24" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UXZ-Oi-NW8">
                                        <rect key="frame" x="0.0" y="0.0" width="400" height="122"/>
                                        <clipView key="contentView" drawsBackground="NO" id="BxI-uN-eTJ">
                                            <rect key="frame" x="0.0" y="0.0" width="400" height="148"/>
                                            <rect key="frame" x="0.0" y="0.0" width="400" height="122"/>
                                            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                                            <subviews>
                                                <outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" tableStyle="fullWidth" multipleSelection="NO" autosaveColumns="NO" rowHeight="24" headerView="TDu-5m-plK" indentationPerLevel="13" autoresizesOutlineColumn="YES" outlineTableColumn="yaF-ma-CSL" id="SQX-Jn-D0S">
                                                    <rect key="frame" x="0.0" y="0.0" width="1147" height="120"/>
                                                    <rect key="frame" x="0.0" y="0.0" width="1147" height="94"/>
                                                    <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                                                    <size key="intercellSpacing" width="17" height="0.0"/>
                                                    <color key="backgroundColor" red="0.1176470588" green="0.1176470588" blue="0.1176470588" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>


@@ 303,11 332,11 @@
                                            <color key="backgroundColor" red="0.1176470588" green="0.1176470588" blue="0.1176470588" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
                                        </clipView>
                                        <scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="KRQ-wE-5Iu">
                                            <rect key="frame" x="0.0" y="132" width="400" height="16"/>
                                            <rect key="frame" x="0.0" y="106" width="400" height="16"/>
                                            <autoresizingMask key="autoresizingMask"/>
                                        </scroller>
                                        <scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="64A-nK-O0T">
                                            <rect key="frame" x="224" y="17" width="15" height="102"/>
                                            <rect key="frame" x="384" y="28" width="16" height="14"/>
                                            <autoresizingMask key="autoresizingMask"/>
                                        </scroller>
                                        <tableHeaderView key="headerView" wantsLayer="YES" id="TDu-5m-plK">


@@ 320,22 349,27 @@
                                    </scrollView>
                                </subviews>
                                <constraints>
                                    <constraint firstAttribute="trailing" secondItem="hBq-6T-efE" secondAttribute="trailing" constant="20" symbolic="YES" id="1sI-TJ-ok1"/>
                                    <constraint firstItem="hBq-6T-efE" firstAttribute="top" secondItem="OuU-bf-WX6" secondAttribute="bottom" constant="4" id="3aE-Z6-nhJ"/>
                                    <constraint firstAttribute="trailing" secondItem="UXZ-Oi-NW8" secondAttribute="trailing" id="71U-mu-lc1"/>
                                    <constraint firstItem="xQd-RQ-cMq" firstAttribute="top" secondItem="cEF-5D-l59" secondAttribute="top" constant="4" id="CG4-3Z-XLS"/>
                                    <constraint firstItem="xQd-RQ-cMq" firstAttribute="leading" secondItem="9fv-nj-PFz" secondAttribute="trailing" constant="8" symbolic="YES" id="CW0-zt-AeF"/>
                                    <constraint firstItem="vMR-N3-Rxj" firstAttribute="leading" secondItem="OuU-bf-WX6" secondAttribute="trailing" constant="8" symbolic="YES" id="DcZ-mC-Lfc"/>
                                    <constraint firstItem="0w1-yG-eRB" firstAttribute="leading" secondItem="xQd-RQ-cMq" secondAttribute="trailing" constant="8" symbolic="YES" id="E07-9e-GrU"/>
                                    <constraint firstItem="0w1-yG-eRB" firstAttribute="leading" secondItem="ep7-63-3EP" secondAttribute="trailing" constant="8" symbolic="YES" id="E07-9e-GrU"/>
                                    <constraint firstItem="lAy-fQ-Mus" firstAttribute="leading" secondItem="cEF-5D-l59" secondAttribute="leading" id="FPc-F9-QFx"/>
                                    <constraint firstItem="9fv-nj-PFz" firstAttribute="top" secondItem="cEF-5D-l59" secondAttribute="top" constant="4" id="Hwu-aO-7cX"/>
                                    <constraint firstItem="UXZ-Oi-NW8" firstAttribute="top" secondItem="lAy-fQ-Mus" secondAttribute="bottom" id="Ovn-Ar-VRf"/>
                                    <constraint firstItem="ep7-63-3EP" firstAttribute="top" secondItem="cEF-5D-l59" secondAttribute="top" constant="4" id="ISt-3x-0uA"/>
                                    <constraint firstItem="UXZ-Oi-NW8" firstAttribute="leading" secondItem="cEF-5D-l59" secondAttribute="leading" id="XF4-YC-uRK"/>
                                    <constraint firstItem="UXZ-Oi-NW8" firstAttribute="top" secondItem="OuU-bf-WX6" secondAttribute="bottom" constant="4" id="YFh-CD-aab"/>
                                    <constraint firstItem="lAy-fQ-Mus" firstAttribute="top" secondItem="cEF-5D-l59" secondAttribute="top" id="YjG-Sw-HEf"/>
                                    <constraint firstItem="OuU-bf-WX6" firstAttribute="leading" secondItem="cEF-5D-l59" secondAttribute="leading" constant="12" id="bpr-Uq-skB"/>
                                    <constraint firstItem="UXZ-Oi-NW8" firstAttribute="top" secondItem="lAy-fQ-Mus" secondAttribute="bottom" id="dhG-Vu-etA"/>
                                    <constraint firstItem="ep7-63-3EP" firstAttribute="leading" secondItem="xQd-RQ-cMq" secondAttribute="trailing" constant="8" symbolic="YES" id="eSN-ZM-wbI"/>
                                    <constraint firstItem="hBq-6T-efE" firstAttribute="leading" secondItem="cEF-5D-l59" secondAttribute="leading" constant="20" symbolic="YES" id="hKW-4l-WLT"/>
                                    <constraint firstAttribute="bottom" secondItem="UXZ-Oi-NW8" secondAttribute="bottom" id="hov-Nl-Bir"/>
                                    <constraint firstItem="9fv-nj-PFz" firstAttribute="leading" secondItem="vMR-N3-Rxj" secondAttribute="trailing" constant="8" symbolic="YES" id="iEH-Rw-7xn"/>
                                    <constraint firstItem="OuU-bf-WX6" firstAttribute="top" secondItem="cEF-5D-l59" secondAttribute="top" constant="4" id="iOf-rV-f2A"/>
                                    <constraint firstItem="vMR-N3-Rxj" firstAttribute="top" secondItem="cEF-5D-l59" secondAttribute="top" constant="4" id="ked-T1-XOp"/>
                                    <constraint firstItem="lAy-fQ-Mus" firstAttribute="bottom" secondItem="hBq-6T-efE" secondAttribute="bottom" constant="4" id="lIX-Gf-us7"/>
                                    <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="0w1-yG-eRB" secondAttribute="trailing" constant="12" id="mUj-NP-RWO"/>
                                    <constraint firstAttribute="trailing" secondItem="lAy-fQ-Mus" secondAttribute="trailing" id="qfY-vN-hAG"/>
                                    <constraint firstItem="0w1-yG-eRB" firstAttribute="top" secondItem="cEF-5D-l59" secondAttribute="top" constant="4" id="rIw-Fh-D2P"/>


@@ 388,15 422,15 @@
                                        <subviews>
                                            <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="6em-6X-jen">
                                                <rect key="frame" x="38" y="38" width="40" height="40"/>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="40" id="tHh-rZ-htn"/>
                                                    <constraint firstAttribute="height" constant="40" id="uWc-zB-Ftk"/>
                                                </constraints>
                                                <buttonCell key="cell" type="bevel" bezelStyle="rounded" imagePosition="overlaps" alignment="center" inset="2" id="Xg5-AJ-RBC">
                                                    <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                                    <font key="font" textStyle="largeTitle" name=".SFNS-Regular"/>
                                                    <imageReference key="image" image="play.fill" symbolScale="medium"/>
                                                </buttonCell>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="40" id="tHh-rZ-htn"/>
                                                    <constraint firstAttribute="height" constant="40" id="uWc-zB-Ftk"/>
                                                </constraints>
                                                <accessibility description="Play"/>
                                                <connections>
                                                    <action selector="playButtonClicked:" target="-2" id="gke-EW-Hhz"/>


@@ 404,15 438,15 @@
                                            </button>
                                            <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hHV-yf-3EL">
                                                <rect key="frame" x="8" y="43" width="30" height="30"/>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="30" id="YWJ-KJ-GYV"/>
                                                    <constraint firstAttribute="height" constant="30" id="bht-yS-W7r"/>
                                                </constraints>
                                                <buttonCell key="cell" type="bevel" bezelStyle="rounded" imagePosition="overlaps" alignment="center" inset="2" id="tHb-BE-KHx">
                                                    <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                                    <font key="font" metaFont="system"/>
                                                    <imageReference key="image" image="backward.fill" symbolScale="default"/>
                                                </buttonCell>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="30" id="YWJ-KJ-GYV"/>
                                                    <constraint firstAttribute="height" constant="30" id="bht-yS-W7r"/>
                                                </constraints>
                                                <accessibility description="Play"/>
                                                <connections>
                                                    <action selector="previousButtonClicked:" target="-2" id="SI7-cl-e1Q"/>


@@ 420,15 454,15 @@
                                            </button>
                                            <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="oii-5P-0gx">
                                                <rect key="frame" x="87" y="11" width="20" height="15"/>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="20" id="PY7-rc-Yo6"/>
                                                    <constraint firstAttribute="height" constant="15" id="h4y-Kr-deC"/>
                                                </constraints>
                                                <buttonCell key="cell" type="bevel" bezelStyle="rounded" imagePosition="overlaps" alignment="center" inset="2" id="7Uv-9E-Hnr">
                                                    <behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
                                                    <font key="font" metaFont="system"/>
                                                    <imageReference key="image" image="shuffle" symbolScale="default"/>
                                                </buttonCell>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="20" id="PY7-rc-Yo6"/>
                                                    <constraint firstAttribute="height" constant="15" id="h4y-Kr-deC"/>
                                                </constraints>
                                                <accessibility description="Play"/>
                                                <connections>
                                                    <action selector="randomButtonClicked:" target="-2" id="Hfa-zJ-O5a"/>


@@ 436,15 470,15 @@
                                            </button>
                                            <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="RCZ-ri-j8B">
                                                <rect key="frame" x="87" y="28" width="20" height="15"/>
                                                <constraints>
                                                    <constraint firstAttribute="height" constant="15" id="aok-1r-Cux"/>
                                                    <constraint firstAttribute="width" constant="20" id="lMP-DI-iLS"/>
                                                </constraints>
                                                <buttonCell key="cell" type="bevel" bezelStyle="regularSquare" imagePosition="overlaps" alignment="center" allowsMixedState="YES" inset="2" id="LYs-fB-IQr">
                                                    <behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
                                                    <font key="font" metaFont="system"/>
                                                    <imageReference key="image" image="repeat" symbolScale="default"/>
                                                </buttonCell>
                                                <constraints>
                                                    <constraint firstAttribute="height" constant="15" id="aok-1r-Cux"/>
                                                    <constraint firstAttribute="width" constant="20" id="lMP-DI-iLS"/>
                                                </constraints>
                                                <accessibility description="Play"/>
                                                <connections>
                                                    <action selector="repeatButtonClicked:" target="-2" id="HiC-J8-3I5"/>


@@ 452,15 486,15 @@
                                            </button>
                                            <button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iZZ-fA-cDk">
                                                <rect key="frame" x="78" y="43" width="30" height="30"/>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="30" id="BeW-j7-svH"/>
                                                    <constraint firstAttribute="height" constant="30" id="iiR-I9-mRn"/>
                                                </constraints>
                                                <buttonCell key="cell" type="bevel" bezelStyle="rounded" imagePosition="overlaps" alignment="center" inset="2" id="5b8-VR-16Z">
                                                    <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
                                                    <font key="font" metaFont="system"/>
                                                    <imageReference key="image" image="forward.fill" symbolScale="default"/>
                                                </buttonCell>
                                                <constraints>
                                                    <constraint firstAttribute="width" constant="30" id="BeW-j7-svH"/>
                                                    <constraint firstAttribute="height" constant="30" id="iiR-I9-mRn"/>
                                                </constraints>
                                                <accessibility description="Play"/>
                                                <connections>
                                                    <action selector="nextButtonClicked:" target="-2" id="uP9-5L-B3M"/>

A Sonar/Player/PlayerSearchFieldCell.h => Sonar/Player/PlayerSearchFieldCell.h +16 -0
@@ 0,0 1,16 @@
//
//  PlayerSearchFieldCell.h
//  Sonar
//
//  Created by b123400 on 2024/03/26.
//

#import <Cocoa/Cocoa.h>

NS_ASSUME_NONNULL_BEGIN

@interface PlayerSearchFieldCell : NSSearchFieldCell

@end

NS_ASSUME_NONNULL_END

A Sonar/Player/PlayerSearchFieldCell.m => Sonar/Player/PlayerSearchFieldCell.m +18 -0
@@ 0,0 1,18 @@
//
//  PlayerSearchFieldCell.m
//  Sonar
//
//  Created by b123400 on 2024/03/26.
//

#import "PlayerSearchFieldCell.h"

@implementation PlayerSearchFieldCell

- (NSRect)searchButtonRectForBounds:(NSRect)rect {
    NSRect r = [super searchButtonRectForBounds:rect];
    r.origin.y = 0;
    return r;
}

@end

M Sonar/Player/PlayerViewModeState.h => Sonar/Player/PlayerViewModeState.h +1 -0
@@ 15,6 15,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) NSArray<NSArray *> *expandedItems;
@property (nonatomic, assign) CGFloat scrollOffset;
@property (nonatomic, assign) BOOL initialLoaded;
@property (nonatomic, strong) NSString *searchQuery; // Search mode only

@end


M Sonar/Player/PlayerWindowController.m => Sonar/Player/PlayerWindowController.m +63 -10
@@ 22,6 22,7 @@ typedef enum : NSUInteger {
    PlayerWindowControllerViewModePlaylists = 1,
    PlayerWindowControllerViewModeAlbums = 2,
    PlayerWindowControllerViewModeStarred = 3,
    PlayerWindowControllerViewModeSearch = 4,
} PlayerWindowControllerViewMode;

@interface PlayerWindowController () <NSOutlineViewDelegate, NSOutlineViewDataSource, NSMenuDelegate>


@@ 53,10 54,13 @@ typedef enum : NSUInteger {
@property (weak) IBOutlet NSButton *playlistsModeButton;
@property (weak) IBOutlet NSButton *artistsModeButton;
@property (weak) IBOutlet NSButton *albumsModeButton;
@property (weak) IBOutlet NSButton *searchModeButton;
@property (weak) IBOutlet NSButton *songsModeButton;
@property (weak) IBOutlet NSSearchField *searchField;

@property (nonatomic, assign) BOOL isExtraVisible;
@property (strong) IBOutlet NSLayoutConstraint *extraBottomConstraint;
@property (strong) IBOutlet NSLayoutConstraint *searchFieldHeightConstraint;

@property (strong) IBOutlet NSMenu *columnsMenu;
@property (strong) IBOutlet NSMenu *outlineViewMenu;


@@ 84,7 88,7 @@ typedef enum : NSUInteger {
- (instancetype)init {
    self = [super initWithWindowNibName:@"PlayerWindowController"];
    self.isExtraVisible = YES;
    self.viewModeStates = [NSMutableDictionary dictionaryWithCapacity:4];
    self.viewModeStates = [NSMutableDictionary dictionaryWithCapacity:5];
    self.volume = 1.0;
    return self;
}


@@ 125,6 129,11 @@ typedef enum : NSUInteger {
            i++;
        }
    }
    [[MediaDB shared] searchWithKeyword:@"star"
                             completion:^(NSError * _Nullable error, NSArray<BaseModel *> * _Nullable items) {
        
    }
                            fetchRemote:YES];
}

- (void)mouseEntered:(NSEvent *)event {


@@ 271,6 280,10 @@ typedef enum : NSUInteger {
    self.viewMode = PlayerWindowControllerViewModeAlbums;
}

- (IBAction)menubarSearchClicked:(id)sender {
    self.viewMode = PlayerWindowControllerViewModeSearch;
}

- (IBAction)menubarNewPlaylistClicked:(id)sender {
    NSAlert *alert = [[NSAlert alloc] init];
    [alert setInformativeText:NSLocalizedString(@"Enter playlist name",@"Create playlist dialog")];


@@ 325,12 338,17 @@ typedef enum : NSUInteger {
        [self storeCurrentViewModeState];
    }
    _viewMode = newMode;
    self.favouritesModeButton.highlighted =
    self.playlistsModeButton.highlighted =
    self.artistsModeButton.highlighted =
    self.albumsModeButton.highlighted =
    self.favouritesModeButton.highlighted = newMode == PlayerWindowControllerViewModeStarred;
    self.playlistsModeButton.highlighted = newMode == PlayerWindowControllerViewModePlaylists;
    self.artistsModeButton.highlighted = newMode == PlayerWindowControllerViewModeArtists;
    self.albumsModeButton.highlighted = newMode == PlayerWindowControllerViewModeAlbums;
    self.searchModeButton.highlighted = newMode == PlayerWindowControllerViewModeSearch;
    self.songsModeButton.highlighted = NO;
    
    self.searchField.hidden = newMode != PlayerWindowControllerViewModeSearch;

    self.searchFieldHeightConstraint.constant = 0;
    
    NSMenuItem *viewItem = [[[NSApplication sharedApplication] mainMenu] itemWithTag:30];
    NSMenuItem *favouritesItem = [[viewItem submenu] itemWithTag:31];
    NSMenuItem *playlistsItem = [[viewItem submenu] itemWithTag:32];


@@ 345,7 363,6 @@ typedef enum : NSUInteger {
    typeof(self) __weak weakSelf = self;
    switch (_viewMode) {
        case PlayerWindowControllerViewModeArtists: {
            self.artistsModeButton.highlighted = YES;
            artistsItem.state = NSControlStateValueOff;
            [[MediaDB shared] listArtistsWithCompletion:^(NSError * _Nonnull error, NSArray<Artist *> * _Nonnull artists) {
                if (weakSelf.viewMode != newMode) return;


@@ 364,7 381,6 @@ typedef enum : NSUInteger {
        }
            break;
        case PlayerWindowControllerViewModePlaylists: {
            self.playlistsModeButton.highlighted = YES;
            playlistsItem.state = NSControlStateValueOff;
            [[MediaDB shared] listPlaylistsWithCompletion:^(NSError * _Nullable error, NSArray<Playlist *> * _Nullable playlists) {
                if (weakSelf.viewMode != newMode) return;


@@ 383,7 399,6 @@ typedef enum : NSUInteger {
        }
            break;
        case PlayerWindowControllerViewModeAlbums: {
            self.albumsModeButton.highlighted = YES;
            albumsItem.state = NSControlStateValueOff;
            [[MediaDB shared] listAlbumsWithCompletion:^(NSError * _Nullable error, NSArray<Album *> * _Nullable albums) {
                if (weakSelf.viewMode != newMode) return;


@@ 402,7 417,6 @@ typedef enum : NSUInteger {
        }
            break;
        case PlayerWindowControllerViewModeStarred: {
            self.favouritesModeButton.highlighted = YES;
            favouritesItem.state = NSControlStateValueOff;
            [[MediaDB shared] listStarredItems:^(NSError * _Nullable error, NSArray<BaseModel *> * _Nullable items) {
                if (weakSelf.viewMode != newMode) return;


@@ 420,10 434,36 @@ typedef enum : NSUInteger {
                                   fetchRemote:update];
        }
            break;
        case PlayerWindowControllerViewModeSearch: {
            self.searchFieldHeightConstraint.constant = 22;
            
            NSString *query = self.viewModeStates[@(PlayerWindowControllerViewModeSearch)].searchQuery;
            if (!query.length) {
                [self setOutlineViewItems:[NSMutableArray array]];
                self.viewModeStates[@(PlayerWindowControllerViewModeSearch)] = [[PlayerViewModeState alloc] init];
                [self.outlineView reloadData];
            } else {
                [[MediaDB shared] searchWithKeyword:query completion:^(NSError * _Nullable error, NSArray<BaseModel *> * _Nullable items) {
                    if (weakSelf.viewMode != PlayerWindowControllerViewModeSearch) return;
                    if (![weakSelf.viewModeStates[@(PlayerWindowControllerViewModeSearch)].searchQuery isEqual:query]) return;
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [weakSelf sortAndSetOutlineViewItems:items];
                        if (error) {
                            [weakSelf displayError:error];
                            return;
                        }
                        [weakSelf.outlineView reloadData];
                        [weakSelf restoreCurrentViewModeState];
                        weakSelf.viewModeStates[@(weakSelf.viewMode)].initialLoaded = YES;
                    });
                }
                                        fetchRemote:update];
            }
            [self.window makeFirstResponder:self.searchField];
        }
        default:
            break;
    }
    
    [self invalidateRestorableState];
}



@@ 482,9 522,22 @@ typedef enum : NSUInteger {
- (IBAction)albumsModeClicked:(id)sender {
    self.viewMode = PlayerWindowControllerViewModeAlbums;
}
- (IBAction)searchModeClicked:(id)sender {
    self.viewMode = PlayerWindowControllerViewModeSearch;
}
- (IBAction)songsModeClicked:(id)sender {
}

- (IBAction)didEnteredSearchQuery:(id)sender {
    NSString *query = self.searchField.stringValue;
    PlayerViewModeState *state = self.viewModeStates[@(PlayerWindowControllerViewModeSearch)] =  [[PlayerViewModeState alloc] init];
    state.searchQuery = query;
    state.initialLoaded = NO;
    state.expandedItems = @[];
    state.scrollOffset = 0;
    [self setViewMode:PlayerWindowControllerViewModeSearch updatingItems:YES];
}

- (void)setIsRandomMode:(BOOL)isRandomMode {
    _isRandomMode = isRandomMode;
    [self reloadControls];

M Sonar/Player/ja.lproj/PlayerWindowController.strings => Sonar/Player/ja.lproj/PlayerWindowController.strings +1 -0
@@ 172,3 172,4 @@
/* Class = "NSButtonCell"; title = "Albums"; ObjectID = "ZNb-vG-Fw3"; */
"ZNb-vG-Fw3.title" = "アルバム";

"lPI-8O-1ri.title" = "検索";

M Sonar/Player/zh-HK.lproj/PlayerWindowController.strings => Sonar/Player/zh-HK.lproj/PlayerWindowController.strings +1 -0
@@ 172,3 172,4 @@
/* Class = "NSButtonCell"; title = "Albums"; ObjectID = "ZNb-vG-Fw3"; */
"ZNb-vG-Fw3.title" = "專輯";

"lPI-8O-1ri.title" = "搜索";

M Sonar/ja.lproj/MainMenu.strings => Sonar/ja.lproj/MainMenu.strings +2 -0
@@ 265,6 265,8 @@
/* Class = "NSMenuItem"; title = "Albums"; ObjectID = "yV6-T8-BIz"; */
"yV6-T8-BIz.title" = "アルバム";

"9vo-yp-jvp.title" = "検索";

/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */
"z6F-FW-3nz.title" = "自動置換を表示";


M Sonar/zh-HK.lproj/MainMenu.strings => Sonar/zh-HK.lproj/MainMenu.strings +2 -0
@@ 265,6 265,8 @@
/* Class = "NSMenuItem"; title = "Albums"; ObjectID = "yV6-T8-BIz"; */
"yV6-T8-BIz.title" = "專輯";

"9vo-yp-jvp.title" = "搜索";

/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */
"z6F-FW-3nz.title" = "顯示替代項目";