Skip to content

Commit

Permalink
Merge pull request #61 from GetToSet/ethanwong/filter-bar
Browse files Browse the repository at this point in the history
Implement the filter bar for podcasts.
  • Loading branch information
dyerc authored Mar 11, 2022
2 parents 64e1050 + 35bb8af commit 6fe0a4d
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 50 deletions.
Binary file modified Assets/icons.sketch
Binary file not shown.
8 changes: 8 additions & 0 deletions Doughnut.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
5E0288D17E6858855545E02E /* Pods_DoughnutTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BE29F62CFF7B122D5C39998 /* Pods_DoughnutTests.framework */; };
6B0233BD27B2CA6500500E28 /* ControlMenuProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0233BC27B2CA6500500E28 /* ControlMenuProvider.swift */; };
6B0605C52788627D00A8A91E /* NSMenu+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0605C42788627D00A8A91E /* NSMenu+Extensions.swift */; };
6B36624627CFB339008E1CA5 /* NSImage+Tint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B36624527CFB339008E1CA5 /* NSImage+Tint.swift */; };
6B3A75F8278F44F500F25578 /* NSView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3A75F7278F44F500F25578 /* NSView+Extensions.swift */; };
6B94DF4C278968F500BCB149 /* NSTableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B94DF4B278968F500BCB149 /* NSTableView+Extensions.swift */; };
6B96F45D27CE6F10001941BA /* PodcastSearchFiled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B96F45C27CE6F10001941BA /* PodcastSearchFiled.swift */; };
6B9C30BB27B5708300D462BE /* BaseTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9C30BA27B5708300D462BE /* BaseTableView.swift */; };
6B9E154727C9EA5F00C919D5 /* AppIcon_Big_Sur.icns in Resources */ = {isa = PBXBuildFile; fileRef = 6B9E154627C9EA5F00C919D5 /* AppIcon_Big_Sur.icns */; };
6BA21C4F279D690700CD3672 /* WindowController+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA21C4E279D690700CD3672 /* WindowController+Toolbar.swift */; };
Expand Down Expand Up @@ -101,10 +103,12 @@
5E1DC050EEE121FD033C44DF /* Pods-Doughnut.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Doughnut.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Doughnut/Pods-Doughnut.debug.xcconfig"; sourceTree = "<group>"; };
6B0233BC27B2CA6500500E28 /* ControlMenuProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlMenuProvider.swift; sourceTree = "<group>"; };
6B0605C42788627D00A8A91E /* NSMenu+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenu+Extensions.swift"; sourceTree = "<group>"; };
6B36624527CFB339008E1CA5 /* NSImage+Tint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Tint.swift"; sourceTree = "<group>"; };
6B3A75F7278F44F500F25578 /* NSView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView+Extensions.swift"; sourceTree = "<group>"; };
6B3ACC982773555700CF1EF1 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
6B730B732767A90900FB5F84 /* Doughnut-Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Doughnut-Release.entitlements"; sourceTree = "<group>"; };
6B94DF4B278968F500BCB149 /* NSTableView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Extensions.swift"; sourceTree = "<group>"; };
6B96F45C27CE6F10001941BA /* PodcastSearchFiled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastSearchFiled.swift; sourceTree = "<group>"; };
6B9C30BA27B5708300D462BE /* BaseTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableView.swift; sourceTree = "<group>"; };
6B9E154627C9EA5F00C919D5 /* AppIcon_Big_Sur.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon_Big_Sur.icns; sourceTree = "<group>"; };
6BA21C4E279D690700CD3672 /* WindowController+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowController+Toolbar.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -237,6 +241,7 @@
6B0605C42788627D00A8A91E /* NSMenu+Extensions.swift */,
6B94DF4B278968F500BCB149 /* NSTableView+Extensions.swift */,
6B3A75F7278F44F500F25578 /* NSView+Extensions.swift */,
6B36624527CFB339008E1CA5 /* NSImage+Tint.swift */,
);
name = AppKit;
sourceTree = "<group>";
Expand Down Expand Up @@ -374,6 +379,7 @@
6B0233BC27B2CA6500500E28 /* ControlMenuProvider.swift */,
832A04331F76EBDC00C92D25 /* WindowController.swift */,
6BA21C4E279D690700CD3672 /* WindowController+Toolbar.swift */,
6B96F45C27CE6F10001941BA /* PodcastSearchFiled.swift */,
832BB7511F95184700988AE8 /* Doughnut-Bridging-Header.h */,
838257D11F759F6F00DB4FD1 /* Assets.xcassets */,
831EBAD927CC11E700F212B4 /* Credits.rtf */,
Expand Down Expand Up @@ -809,6 +815,7 @@
83659AB81F7D8A0300E09833 /* Podcast.swift in Sources */,
837D52BE1F8E622200C17514 /* TasksViewController.swift in Sources */,
8379899B1F81616C00234577 /* SeekSlider.swift in Sources */,
6B36624627CFB339008E1CA5 /* NSImage+Tint.swift in Sources */,
837068131F7BBD63007FE973 /* EpisodeCellView.swift in Sources */,
832FF90B1F8D60430065E593 /* DownloadManager.swift in Sources */,
83667DF41F76D22600F1ABC0 /* DetailViewController.swift in Sources */,
Expand All @@ -824,6 +831,7 @@
83235C012009472F00BC356F /* PrefGeneralViewController.swift in Sources */,
83235BFE2009472900BC356F /* Preference.swift in Sources */,
83FB4EB81F7BD9A1001CD842 /* Library.swift in Sources */,
6B96F45D27CE6F10001941BA /* PodcastSearchFiled.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
15 changes: 15 additions & 0 deletions Doughnut/Assets.xcassets/PodcastFilter.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "[email protected]",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "[email protected]",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 19 additions & 20 deletions Doughnut/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -460,15 +460,15 @@ Gw
</scroller>
</scrollView>
<visualEffectView blendingMode="behindWindow" material="sidebar" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="Mge-rT-KIQ">
<rect key="frame" x="0.0" y="0.0" width="251" height="30"/>
<rect key="frame" x="0.0" y="0.0" width="251" height="28"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="Pkz-GU-AL9">
<rect key="frame" x="10" y="7" width="16" height="16"/>
<rect key="frame" x="8" y="3.5" width="16" height="21"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="1ey-UM-1K6"/>
<constraint firstAttribute="height" constant="16" id="edJ-L8-BeT"/>
</constraints>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="FilterInactive" imagePosition="only" alignment="center" controlSize="small" imageScaling="proportionallyUpOrDown" inset="2" id="1pl-TM-f2l">
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSActionTemplate" imagePosition="only" alignment="center" controlSize="small" imageScaling="proportionallyUpOrDown" inset="2" id="1pl-TM-f2l">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystem"/>
</buttonCell>
Expand All @@ -478,19 +478,30 @@ Gw
</connections>
</button>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Q9V-7G-TPX">
<rect key="frame" x="0.0" y="27" width="251" height="5"/>
<rect key="frame" x="0.0" y="25" width="251" height="5"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="yYT-XN-B8Z"/>
</constraints>
</box>
<searchField wantsLayer="YES" focusRingType="none" verticalHuggingPriority="750" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mtS-jJ-Pmb" customClass="PodcastSearchFiled" customModule="Doughnut" customModuleProvider="target">
<rect key="frame" x="30" y="3" width="217" height="22"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" focusRingType="none" placeholderString="Filter" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" maximumRecents="0" id="kGs-iR-hTg">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</searchFieldCell>
</searchField>
</subviews>
<constraints>
<constraint firstItem="Q9V-7G-TPX" firstAttribute="leading" secondItem="Mge-rT-KIQ" secondAttribute="leading" id="212-QD-g9h"/>
<constraint firstItem="mtS-jJ-Pmb" firstAttribute="centerY" secondItem="Mge-rT-KIQ" secondAttribute="centerY" id="GEi-rY-ITF"/>
<constraint firstAttribute="trailing" secondItem="mtS-jJ-Pmb" secondAttribute="trailing" constant="4" id="MWF-dz-0Mc"/>
<constraint firstItem="mtS-jJ-Pmb" firstAttribute="leading" secondItem="Pkz-GU-AL9" secondAttribute="trailing" constant="6" id="MWU-Fi-5X6"/>
<constraint firstItem="Q9V-7G-TPX" firstAttribute="top" secondItem="Mge-rT-KIQ" secondAttribute="top" id="gmI-qG-Rft"/>
<constraint firstItem="Pkz-GU-AL9" firstAttribute="leading" secondItem="Mge-rT-KIQ" secondAttribute="leading" constant="10" id="ll2-91-qQc"/>
<constraint firstItem="Pkz-GU-AL9" firstAttribute="leading" secondItem="Mge-rT-KIQ" secondAttribute="leading" constant="8" id="ll2-91-qQc"/>
<constraint firstItem="Pkz-GU-AL9" firstAttribute="centerY" secondItem="Mge-rT-KIQ" secondAttribute="centerY" id="m3p-Vc-TfH"/>
<constraint firstAttribute="trailing" secondItem="Q9V-7G-TPX" secondAttribute="trailing" id="t9t-Ca-5lf"/>
<constraint firstAttribute="height" constant="30" id="zJ3-3v-cjb"/>
<constraint firstAttribute="height" constant="28" id="zJ3-3v-cjb"/>
</constraints>
</visualEffectView>
</subviews>
Expand All @@ -507,7 +518,8 @@ Gw
<viewLayoutGuide key="layoutMargins" id="8eP-ga-5Bq"/>
</view>
<connections>
<outlet property="filteringButton" destination="Pkz-GU-AL9" id="0A7-FY-mz9"/>
<outlet property="moreButton" destination="Pkz-GU-AL9" id="O9K-hE-JvA"/>
<outlet property="searchField" destination="mtS-jJ-Pmb" id="OFh-oJ-WxW"/>
<outlet property="sortView" destination="Mge-rT-KIQ" id="UXl-BK-k3s"/>
<outlet property="tableView" destination="aVy-1A-ODd" id="MSD-C6-DW1"/>
</connections>
Expand Down Expand Up @@ -571,19 +583,6 @@ Gw
<menuItem title="Sort By" identifier="podcastViewSortBy" id="dyf-fU-Ugh">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Filter By" id="sC2-Ue-2nz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Filter By" id="731-j4-Wha">
<items>
<menuItem title="New Episodes" id="jv1-Gl-Reg">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleFilterPodcasts:" target="ioO-eb-ohg" id="1pb-k4-Zho"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
<connections>
<outlet property="delegate" destination="hBf-lZ-L6q" id="xzQ-ok-WgY"/>
Expand Down
156 changes: 156 additions & 0 deletions Doughnut/PodcastSearchFiled.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Doughnut Podcast Client
* Copyright (C) 2017 - 2022 Chris Dyer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import AppKit

protocol PodcastSearchFiledDelegate: AnyObject {

func podcastSearchFiledDidUpdate(withFilter filter: PodcastViewController.Filter)

}

final class PodcastSearchFiled: NSSearchField {

weak var searchFieldDelegate: PodcastSearchFiledDelegate?

private var filter: PodcastViewController.Filter = .all {
didSet {
updateFilteringButtonState()
searchFieldDelegate?.podcastSearchFiledDidUpdate(withFilter: filter)
}
}

private var previousFilterCategory: PodcastViewController.Filter.Category = .newEpisodes

// NSButtonCell has no methods fo tintColor, images have to be tinted manually
private var filterImage: NSImage?
private var filterImageActive: NSImage?
private var cancelImage: NSImage?
private var controlSelectedImage: NSImage?

private static let searchButtonSize = CGSize(width: 32, height: 16)

init() {
super.init(frame: .zero)
commonInit()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}

private func commonInit() {
updateIconImages()

let searchFieldCell = cell as? NSSearchFieldCell
searchFieldCell?.searchButtonCell?.image = filterImage
searchFieldCell?.searchButtonCell?.alternateImage = filterImageActive
searchFieldCell?.searchButtonCell?.showsStateBy = [.contentsCellMask]
searchFieldCell?.searchButtonCell?.imageScaling = .scaleProportionallyDown

searchFieldCell?.cancelButtonCell?.image = cancelImage
searchFieldCell?.cancelButtonCell?.alternateImage = controlSelectedImage

target = self
action = #selector(onSearchTextChange(_:))

let menu = NSMenu()
menu.addItem(withTitle: "New Episodes", action: #selector(toggleFilterPodcasts(_:)), keyEquivalent: "")
for item in menu.items[0...] {
item.configureWithDefaultFont()
}
searchMenuTemplate = menu
}

private func updateIconImages() {
// FIXME: refresh icon image on appearance or control tint color change.
filterImage = NSImage(named: "PodcastFilter")?.tinted(with: .secondaryLabelColor)
filterImageActive = NSImage(named: "PodcastFilterActive")?.tinted(with: .controlAccentColor)

cancelImage = NSImage(named: NSImage.stopProgressFreestandingTemplateName)?.tinted(with: .secondaryLabelColor)
controlSelectedImage = NSImage(named: NSImage.stopProgressFreestandingTemplateName)?.tinted(with: .labelColor)
}

override func draw(_ dirtyRect: NSRect) {
// This override is required, otherwise icon images won't update
super.draw(dirtyRect)
}

override func rectForSearchButton(whenCentered isCentered: Bool) -> NSRect {
let originalRect = super.rectForSearchButton(whenCentered: isCentered)
return CGRect(
x: originalRect.origin.x,
y: originalRect.origin.y - (Self.searchButtonSize.height - originalRect.size.height) / 2,
width: Self.searchButtonSize.width,
height: Self.searchButtonSize.height
)
}

override func rectForSearchText(whenCentered isCentered: Bool) -> NSRect {
var rect = super.rectForSearchText(whenCentered: isCentered)
let searchButtonRect = super.rectForSearchButton(whenCentered: isCentered)
rect.origin.x += Self.searchButtonSize.width - searchButtonRect.width
rect.size.width -= Self.searchButtonSize.width - searchButtonRect.width
return rect
}

override func mouseDown(with event: NSEvent) {
// If the click resides on the left half of the filter icon, perform a
// toggle against previous selected filter category, otherwise, fall back to
// the default behavior that shows the category menu.
let searchButtonRect = rectForSearchButton(whenCentered: centersPlaceholder)
let pointInSearchField = convert(event.locationInWindow, from: nil)
if
searchButtonRect.contains(pointInSearchField),
pointInSearchField.x <= searchButtonRect.midX,
filter.query.isEmpty
{
swap(&previousFilterCategory, &filter.category)
} else {
super.mouseDown(with: event)
}
}

private func updateFilteringButtonState() {
let isActive = !filter.query.isEmpty || filter.category != .all
let searchFieldCell = cell as? NSSearchFieldCell
searchFieldCell?.searchButtonCell?.state = isActive ? .on : .off
needsDisplay = true
}

@objc func toggleFilterPodcasts(_ sender: Any) {
previousFilterCategory = filter.category
filter.category = (filter.category == .newEpisodes) ? .all : .newEpisodes
}

@objc func onSearchTextChange(_ sender: Any) {
filter.query = stringValue
}

@objc func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
switch menuItem.action {
case #selector(toggleFilterPodcasts(_:)):
menuItem.state = filter.category == .newEpisodes ? .on : .off
return true
default:
return false
}
}

}
Loading

0 comments on commit 6fe0a4d

Please sign in to comment.