Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update widget design #1343

Open
wants to merge 31 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6165769
WIP - layout changes to support new designs
jgcaruso Nov 13, 2023
17ef6a5
update logo for widgets
jgcaruso Nov 30, 2023
37eac96
fixed logic for displaying toggle button, set proper spacing based on…
jgcaruso Nov 30, 2023
65d6a5f
set colors based on designs
jgcaruso Nov 30, 2023
8879b96
button sizing and font spacing based on designs
jgcaruso Nov 30, 2023
cf37730
fix alignment of episode art and buttons
jgcaruso Nov 30, 2023
9ca3f8c
convert medium widget to new design
jgcaruso Nov 30, 2023
174cb80
initial now playing widget color changes
jgcaruso Dec 4, 2023
c974dae
text change for empty Up Next list
jgcaruso Dec 4, 2023
a556518
only show discover text when Up Next is empty
jgcaruso Jan 4, 2024
df15ef1
new colour scheme definitions and widgets in the new scheme
jgcaruso Jan 4, 2024
c2d5902
show the new style widgets first
jgcaruso Jan 4, 2024
2c218c1
new now playing theming
jgcaruso Jan 4, 2024
b60aa81
replaced new separate coloured logos with transparent logo
jgcaruso Jan 9, 2024
1a635ba
change passing of color scheme to an environment variable
jgcaruso Jan 11, 2024
be32a48
new design for solid color "contrast" now playing widget
jgcaruso Jan 11, 2024
a7194fb
fix artwork size and spacing in now playing small widget
jgcaruso Jan 11, 2024
7690b05
fix button spacing and text sizing on large toggle button
jgcaruso Jan 11, 2024
27464a8
make art size on medium/large view consistent
jgcaruso Jan 11, 2024
2877b05
cleaned up Filter widgets to better match new style
jgcaruso Jan 11, 2024
04aa9b0
matched new style for "Nothing Playing" widgets
jgcaruso Jan 11, 2024
7c85359
fixed spacing for HungryForMore view in Filter view
jgcaruso Jan 11, 2024
fd86fef
remove todo
jgcaruso Jan 11, 2024
200f1ee
fix background for Now Playing lock screen widget
jgcaruso Feb 8, 2024
5cebbfc
lint format
jgcaruso Feb 8, 2024
9fc6a2e
fix iOS 16 now playing text colour not matching theme
jgcaruso Mar 28, 2024
613b554
add support for dark mode to Contrast themed widgets
jgcaruso Mar 28, 2024
89da42b
Merge branch 'trunk' into update/widget-design
jgcaruso Mar 28, 2024
6753db8
fix dark mode button colour
jgcaruso Mar 28, 2024
db79b64
Merge branch 'trunk' into update/widget-design
jgcaruso Apr 24, 2024
af146e3
updated dark mode contrast widget to use cool grey instead of red as …
jgcaruso Apr 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "logo_white_small_transparent@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "logo_white_small_transparent@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
132 changes: 80 additions & 52 deletions WidgetExtension/Now Playing/NowPlayingEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,65 +7,86 @@ struct NowPlayingWidgetEntryView: View {

@Environment(\.showsWidgetContainerBackground) var showsWidgetBackground

@Environment(\.colorScheme) var colorScheme
var widgetColorSchemeLight: PCWidgetColorScheme
var widgetColorSchemeDark: PCWidgetColorScheme
var widgetColorScheme: PCWidgetColorScheme {
get {
colorScheme == .dark ? widgetColorSchemeDark : widgetColorSchemeLight
}
}

var body: some View {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be more readable broken into either separate Views or several ViewBuilder functions. Maybe an Artwork, EpisodeTitle, and PlayingToggle?

if let playingEpisode = entry.episode {
VStack(alignment: .leading, spacing: 3) {
GeometryReader { geometry in
ZStack {
if showsWidgetBackground {
Rectangle().fill(widgetColorScheme.bottomBackgroundColor)
}
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top) {
if #available(iOS 17, *) {
Toggle(isOn: entry.isPlaying, intent: PlayEpisodeIntent(episodeUuid: playingEpisode.episodeUuid)) {
LargeArtworkView(imageData: playingEpisode.imageData, showShadow: showsWidgetBackground)
}
.toggleStyle(WidgetPlayToggleStyle())
} else {
LargeArtworkView(imageData: playingEpisode.imageData)
}
LargeArtworkView(imageData: playingEpisode.imageData)
.frame(width: 64, height: 64)
Spacer()
Image("logo-transparent")
Image(widgetColorScheme.iconAssetName)
.frame(width: 28, height: 28)
}.padding(topPadding)
.background(
VStack {
if showsWidgetBackground {
Rectangle()
.fill(Color(UIColor(hex: playingEpisode.podcastColor)).opacity(0.85))
.frame(height: 0.667 * geometry.size.height, alignment: .top)
}
Spacer()
})
}
Text(playingEpisode.episodeTitle)
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(Color.primary)
.lineLimit(2)
.frame(height: 38, alignment: .center)
.layoutPriority(1)
.padding(episodeTitlePadding)
.unredacted()
}
.padding(topPadding)

if entry.isPlaying {
Text(L10n.nowPlaying.localizedUppercase)
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(Color.secondary)
.padding(bottomTextPadding)
} else {
Text(L10n.podcastTimeLeft(CommonWidgetHelper.durationString(duration: playingEpisode.duration)).localizedUppercase)
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(Color.secondary)
.padding(bottomTextPadding)
Text(playingEpisode.episodeTitle)
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(widgetColorScheme.bottomTextColor)
.lineLimit(1)
.frame(height: 12, alignment: .center)
.layoutPriority(1)
.padding(episodeTitlePadding)

if #available(iOS 17, *) {
Toggle(isOn: entry.isPlaying, intent: PlayEpisodeIntent(episodeUuid: playingEpisode.episodeUuid)) {

if entry.isPlaying {
Text(L10n.nowPlaying)
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(widgetColorScheme.topButtonTextColor)
} else {
Text(L10n.podcastTimeLeft(CommonWidgetHelper.durationString(duration: playingEpisode.duration)))
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(widgetColorScheme.topButtonTextColor)
.layoutPriority(1)
}
}
.toggleStyle(WidgetFirstEpisodePlayToggleStyle(colorScheme: widgetColorScheme))
.padding(bottomTextPadding)
} else {
if entry.isPlaying {
Text(L10n.nowPlaying)
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(widgetColorScheme.bottomTextColor.opacity(0.6))
.padding(bottomTextPadding)
} else {
Text(L10n.podcastTimeLeft(CommonWidgetHelper.durationString(duration: playingEpisode.duration)))
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(widgetColorScheme.bottomTextColor.opacity(0.6))
.padding(bottomTextPadding)
.layoutPriority(1)
}
}
}
.widgetURL(URL(string: "pktc://last_opened"))
.clearBackground()
.if(!showsWidgetBackground) { view in
view
.padding(.top)
.padding(.bottom)
}
}
.widgetURL(URL(string: "pktc://last_opened"))
.clearBackground()
.if(!showsWidgetBackground) { view in
view
.padding(.top)
.padding(.bottom)
}
} else if !showsWidgetBackground {
}
else if !showsWidgetBackground {
nothingPlaying
} else {
ZStack {
Expand Down Expand Up @@ -129,11 +150,18 @@ struct NowPlayingWidgetEntryView: View {
struct NowPlayingEntryView_Previews: PreviewProvider {
static var previews: some View {
Group {
NowPlayingWidgetEntryView(entry: .init(date: Date(), episode: WidgetEpisode(commonItem: CommonUpNextItem.init(episodeUuid: "foo", imageUrl: "", episodeTitle: "foo", podcastName: "foo", podcastColor: "#999999", duration: 400, isPlaying: true)), isPlaying: true))
NowPlayingWidgetEntryView(entry: .init(date: Date(), episode: WidgetEpisode(commonItem: CommonUpNextItem.init(episodeUuid: "foo", imageUrl: "", episodeTitle: "foo", podcastName: "foo", podcastColor: "#999999", duration: 400, isPlaying: true)), isPlaying: true), widgetColorSchemeLight: widgetColorSchemeBold,
widgetColorSchemeDark: widgetColorSchemeBold)
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("Episode Playing")

NowPlayingWidgetEntryView(entry: .init(date: Date(), episode: nil, isPlaying: true))
NowPlayingWidgetEntryView(entry: .init(date: Date(), episode: WidgetEpisode(commonItem: CommonUpNextItem.init(episodeUuid: "foo", imageUrl: "", episodeTitle: "foo", podcastName: "foo", podcastColor: "#999999", duration: 400, isPlaying: true)), isPlaying: false), widgetColorSchemeLight: widgetColorSchemeBold,
widgetColorSchemeDark: widgetColorSchemeBold)
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("Episode Paused")

NowPlayingWidgetEntryView(entry: .init(date: Date(), episode: nil, isPlaying: true), widgetColorSchemeLight: widgetColorSchemeBold,
widgetColorSchemeDark: widgetColorSchemeBold)
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("Nothing Playing")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ struct NowPlayingLockscreenWidgetEntryView: View {
.foregroundColor(Color.secondary)
}
}
}.widgetURL(URL(string: widgetURL))
}
.widgetURL(URL(string: widgetURL))
.clearBackground()
}
}
2 changes: 1 addition & 1 deletion WidgetExtension/Now Playing/NowPlayingWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import WidgetKit
struct NowPlayingWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "Now_Playing_Widget", provider: NowPlayingProvider()) { entry in
NowPlayingWidgetEntryView(entry: entry)
NowPlayingWidgetEntryView(entry: entry, widgetColorSchemeLight: widgetColorSchemeContrastNowPlaying, widgetColorSchemeDark: widgetColorSchemeContrastNowPlayingDark)
.clearBackground()
}
.contentMarginsDisabledIfAvailable()
Expand Down
15 changes: 15 additions & 0 deletions WidgetExtension/Now Playing/NowPlayingWidgetBold.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import SwiftUI
import WidgetKit

struct NowPlayingWidgetBold: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "Now_Playing_Widget_Bold", provider: NowPlayingProvider()) { entry in
NowPlayingWidgetEntryView(entry: entry, widgetColorSchemeLight: widgetColorSchemeBoldNowPlaying, widgetColorSchemeDark: widgetColorSchemeBoldNowPlaying)
.clearBackground()
}
.contentMarginsDisabledIfAvailable()
.configurationDisplayName(L10n.nowPlaying)
.description(L10n.widgetsNowPlayingDesc)
.supportedFamilies([.systemSmall])
}
}
2 changes: 2 additions & 0 deletions WidgetExtension/PocketCastsWidgetBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import WidgetKit
@main
struct PocketCastsWidgetBundle: WidgetBundle {
var body: some Widget {
NowPlayingWidgetBold()
UpNextWidgetBold()
NowPlayingWidget()
UpNextWidget()
NowPlayingLockScreenWidget()
Expand Down
111 changes: 67 additions & 44 deletions WidgetExtension/Up Next/EpisodeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,46 @@ struct EpisodeView: View {
@State var episode: WidgetEpisode
@State var topText: Text
@State var isPlaying: Bool = false

var compactView: Bool {
typeSize >= .xxLarge
}
@State var isFirstEpisode: Bool = false

@Environment(\.dynamicTypeSize) var typeSize
@Environment(\.widgetColorScheme) var colorScheme

var body: some View {
let textColor = isFirstEpisode ? colorScheme.topTextColor : colorScheme.bottomTextColor

Link(destination: CommonWidgetHelper.urlForEpisodeUuid(uuid: episode.episodeUuid)!) {
HStack(spacing: 12) {
if #available(iOS 17, *) {
Toggle(isOn: isPlaying, intent: PlayEpisodeIntent(episodeUuid: episode.episodeUuid)) {
SmallArtworkView(imageData: episode.imageData)
}
.toggleStyle(WidgetPlayToggleStyle())
} else {
SmallArtworkView(imageData: episode.imageData)
}
SmallArtworkView(imageData: episode.imageData)
.frame(maxWidth: 52, maxHeight: 52)
VStack(alignment: .leading) {
if !compactView {
topText
.textCase(.uppercase)
.font(.caption2)
.foregroundColor(Color.secondary)
}
Text(episode.episodeTitle)
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(Color.primary)
.foregroundColor(textColor)
.lineLimit(1)
HStack(alignment: .center, spacing: 5) {
if compactView {
.frame(maxWidth: .infinity, alignment: .leading)
if isFirstEpisode, #available(iOS 17, *) {
Spacer()
Toggle(isOn: isPlaying, intent: PlayEpisodeIntent(episodeUuid: episode.episodeUuid)) {
topText
.textCase(.uppercase)
.font(.caption2)
.foregroundColor(Color.secondary)
Text("•")
.font(.caption2)
.foregroundColor(Color.secondary)
.font(.caption)
.fontWeight(.medium)
.foregroundColor(colorScheme.topButtonTextColor)
}
Text(episode.podcastName)
.toggleStyle(WidgetFirstEpisodePlayToggleStyle(colorScheme: colorScheme))
} else {
Spacer()
.frame(height: 4)
topText
.font(.caption2)
.fontWeight(.semibold)
.foregroundColor(Color.secondary)
.lineLimit(1)
.foregroundColor(textColor.opacity(0.6))
}
}
if !isFirstEpisode, #available(iOS 17, *) {
Toggle(isOn: isPlaying, intent: PlayEpisodeIntent(episodeUuid: episode.episodeUuid)) {}
.toggleStyle(WidgetPlayToggleStyle(colorScheme: colorScheme))
}
}
}
}
Expand All @@ -62,27 +55,57 @@ struct EpisodeView: View {
}
}

struct WidgetPlayToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
ZStack {
configuration.label
.truncationMode(.tail)
struct WidgetFirstEpisodePlayToggleStyle: ToggleStyle {
let colorScheme: PCWidgetColorScheme

Circle()
.foregroundStyle(.white)
.frame(width: 24, height: 24)
func makeBody(configuration: Configuration) -> some View {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be marked @ViewBuilder in case the view is not a single View? I think the protocol method is marked ViewBuilder, so maybe that's automatically applied?

HStack(spacing: 0) {
Group {
configuration.isOn ?
Image("icon-pause")
.resizable()
.foregroundStyle(.black)
.foregroundStyle(colorScheme.topButtonTextColor)
:
Image("icon-play")
Image("icon-play")
.resizable()
.foregroundStyle(.black)
.foregroundStyle(colorScheme.topButtonTextColor)
}
.frame(width: 28, height: 28)
// TODO: Something fun - create a timeline that counts down by the minute instead of showing "now playing"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed I left this here... it is more of a "this would be fun if we could do this" activity, but not something that can easily be done right now.

Comment should probably be removed, but I'll leave it here for the moment incase anyone has thoughts on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the TODO comment add an issue on the repo as an improvement/suggestion to do in the future.

configuration.label
.truncationMode(.tail)
}
.padding(.trailing, 12) // icon has 8px padding built into it, so this should match 8 + .leading
.padding(.leading, 4)
.padding(.vertical, 2) // 2 + 8 (from icon) = 10 in design (actually 9.76)
.background(
RoundedRectangle(cornerRadius: 100)
.foregroundColor(colorScheme.topButtonBackgroundColor)
)
}
}

struct WidgetPlayToggleStyle: ToggleStyle {
let colorScheme: PCWidgetColorScheme

func makeBody(configuration: Configuration) -> some View {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be marked @ViewBuilder in case the view is not a single View? I think the protocol method is marked ViewBuilder, so maybe that's automatically applied?

HStack {
ZStack {
Circle()
.foregroundStyle(colorScheme.bottomButtonBackgroundColor)
.frame(width: 24, height: 24)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this 24 be refactored to an enum constant?

Group {
configuration.isOn ?
Image("icon-pause")
.resizable()
.foregroundStyle(colorScheme.bottomButtonTextColor)
:
Image("icon-play")
.resizable()
.foregroundStyle(colorScheme.bottomButtonTextColor)
}
.frame(width: 24, height: 24)
}
.frame(width: 24, height: 24)
}
.opacity(0.9)
}
}