Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Ruddarr/Services/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class AppSettings: ObservableObject {
@AppStorage("theme", store: dependencies.store) var theme: Theme = .factory
@AppStorage("appearance", store: dependencies.store) var appearance: Appearance = .automatic
@AppStorage("grid", store: dependencies.store) var grid: GridStyle = .posters
@AppStorage("richCalendarDisplay", store: dependencies.store) var richCalendarDisplay: Bool = true

@AppStorage("tab", store: dependencies.store) var tab: TabItem = .movies
@AppStorage("releaseFilters", store: dependencies.store) var releaseFilters: ReleaseFilters = .reset
Expand Down Expand Up @@ -105,6 +106,7 @@ extension AppSettings {
"theme": theme.rawValue,
"tab": tab.rawValue,
"appearance": appearance.rawValue,
"richCalendarDisplay": richCalendarDisplay,
]

for instance in configuredInstances {
Expand Down
105 changes: 87 additions & 18 deletions Ruddarr/Views/Calendar/CalendarDate.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,79 @@
import SwiftUI

enum CalendarDateStyle {
case rich
case classic
}

struct CalendarDate: View {
var date: Date
var style: CalendarDateStyle = .rich

@State var isToday: Bool = false

@EnvironmentObject var settings: AppSettings

@ViewBuilder
var body: some View {
Group {
switch style {
case .rich:
richDate
case .classic:
classicDate
}
}
.onAppear {
isToday = Calendar.current.isDateInToday(date)
}
.onBecomeActive {
isToday = Calendar.current.isDateInToday(date)
}
.transaction { transaction in
transaction.animation = nil // disable animation
}
}

var richDate: some View {
HStack {
dateLockup

Spacer(minLength: 0)
}
.padding(.top, 6)
.padding(.bottom, 10)
.frame(maxWidth: .infinity, alignment: .leading)
}

var dateLockup: some View {
HStack(alignment: .center, spacing: 7) {
VStack(alignment: .leading, spacing: 0) {
Text(CalendarDate.dayOfWeek.string(from: date).uppercased())
.font(.caption2.weight(.semibold))
.kerning(1.05)
.lineLimit(1)
.offset(y: 1)

Text(CalendarDate.nameOfMonth.string(from: date).uppercased())
.font(.caption2)
.kerning(1.05)
.lineLimit(1)
}
.foregroundStyle(isToday ? settings.theme.tint : .secondary)

Text(CalendarDate.dayOfMonth.string(from: date))
.font(.title.bold())
.monospacedDigit()
.foregroundStyle(isToday ? settings.theme.tint : .primary)
}
.fixedSize()
.padding(.vertical, 4)
.padding(.horizontal, 7)
.background(.background.opacity(0.8), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
.offset(x: -7)
}

var classicDate: some View {
VStack(alignment: .center, spacing: 0) {
Text(CalendarDate.dayOfWeek.string(from: date).uppercased())
.font(.caption2)
Expand All @@ -29,15 +95,6 @@ struct CalendarDate: View {
Spacer()
}
.foregroundStyle(isToday ? settings.theme.tint : .primary)
.onAppear {
isToday = Calendar.current.isDateInToday(date)
}
.onBecomeActive {
isToday = Calendar.current.isDateInToday(date)
}
.transaction { transaction in
transaction.animation = nil // disable animation
}
}

static let dayOfWeek: DateFormatter = {
Expand All @@ -61,17 +118,29 @@ struct CalendarDate: View {

struct CalendarWeekRange: View {
var date: Date
var style: CalendarDateStyle = .rich

@ViewBuilder
var body: some View {
Spacer()

Text(weekRange(date))
.font(.subheadline)
.textCase(.uppercase)
.kerning(1.0)
.foregroundStyle(.secondary)
.padding(.bottom, 12)
.padding(.leading, 1)
switch style {
case .rich:
Text(weekRange(date))
.font(.subheadline)
.textCase(.uppercase)
.kerning(1.0)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 10)
.padding(.bottom, 6)
case .classic:
Text(weekRange(date))
.font(.subheadline)
.textCase(.uppercase)
.kerning(1.0)
.foregroundStyle(.secondary)
.padding(.bottom, 12)
.padding(.leading, 1)
}
}

func weekRange(_ date: Date) -> String {
Expand Down
177 changes: 142 additions & 35 deletions Ruddarr/Views/Calendar/CalendarMedia.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,61 @@ struct CalendarMovie: View {
@EnvironmentObject var settings: AppSettings

var body: some View {
Group {
if settings.richCalendarDisplay {
richContent
} else {
classicContent
}
}
.padding(.vertical, 8)
.padding(.horizontal, settings.richCalendarDisplay ? 8 : 12)
.frame(maxWidth: .infinity)
.opacity(shouldFade ? 0.5 : 1)
.background(.card.opacity(shouldFade ? 0.6 : 1))
.clipShape(RoundedRectangle(cornerRadius: 14))
.onTapGesture {
let deeplink = String(
format: "ruddarr://movies/open/%d?instance=%@",
movie.id,
movie.instanceId!.uuidString
)

try? QuickActions.Deeplink(url: URL(string: deeplink)!)()
}
}

var richContent: some View {
HStack(alignment: .center, spacing: 10) {
CalendarPoster(url: movie.remotePoster, title: movie.title)

VStack(alignment: .leading, spacing: 3) {
HStack(alignment: .center) {
Text(movie.title)
.font(.body)
.lineLimit(1)
.foregroundStyle(shouldFade ? .secondary : .primary)

Spacer()

statusIcon
.font(.subheadline)
.imageScale(.small)
.foregroundStyle(.secondary)
}

if let type = movie.releaseType(for: date) {
Text(type)
.font(.caption)
.foregroundStyle(settings.theme.tint)
}
}

Spacer(minLength: 0)
}
}

var classicContent: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .center) {
Expand All @@ -30,21 +85,6 @@ struct CalendarMovie: View {
}
}
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.frame(maxWidth: .infinity)
.opacity(shouldFade ? 0.5 : 1)
.background(.card.opacity(shouldFade ? 0.6 : 1))
.clipShape(RoundedRectangle(cornerRadius: 14))
.onTapGesture {
let deeplink = String(
format: "ruddarr://movies/open/%d?instance=%@",
movie.id,
movie.instanceId!.uuidString
)

try? QuickActions.Deeplink(url: URL(string: deeplink)!)()
}
}

var shouldFade: Bool {
Expand All @@ -71,6 +111,80 @@ struct CalendarEpisode: View {
@EnvironmentObject var settings: AppSettings

var body: some View {
Group {
if settings.richCalendarDisplay {
richContent
} else {
classicContent
}
}
.padding(.vertical, 8)
.padding(.horizontal, settings.richCalendarDisplay ? 8 : 12)
.frame(maxWidth: .infinity)
.opacity(shouldFade ? 0.5 : 1)
.background(.card.opacity(shouldFade ? 0.6 : 1))
.clipShape(RoundedRectangle(cornerRadius: 14))
.onTapGesture {
var deeplink = String(
format: "ruddarr://series/open/%d?season=%d&instance=%@",
episode.seriesId,
episode.seasonNumber,
episode.instanceId!.uuidString
)

if !isGrouped {
deeplink.append("&episode=\(episode.episodeNumber)")
}

try? QuickActions.Deeplink(url: URL(string: deeplink)!)()
}
}

var richContent: some View {
HStack(alignment: .center, spacing: 10) {
CalendarPoster(url: episode.series?.remotePoster, title: episode.series?.title)

VStack(alignment: .leading, spacing: 3) {
HStack {
Text(episode.series?.title ?? "Unknown")
.font(.body)
.lineLimit(1)
.foregroundStyle(shouldFade ? .secondary : .primary)

Spacer()

if let airDate = episode.airDateUtc {
Text(airDate.formatted(date: .omitted, time: .shortened))
.font(.subheadline)
.foregroundStyle(.secondary)
}
}

HStack(alignment: .center, spacing: 6) {
Text(episode.episodeLabel)

if let title = episode.title {
Bullet()
Text(title).lineLimit(1)
}

Spacer()

statusIcon
.foregroundStyle(.secondary)
.imageScale(.small)
}
.foregroundStyle(.secondary)
.font(.subheadline)

tag
}

Spacer(minLength: 0)
}
}

var classicContent: some View {
VStack(alignment: .leading) {
HStack {
Text(episode.series?.title ?? "Unknown")
Expand Down Expand Up @@ -106,26 +220,6 @@ struct CalendarEpisode: View {

tag
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.frame(maxWidth: .infinity)
.opacity(shouldFade ? 0.5 : 1)
.background(.card.opacity(shouldFade ? 0.6 : 1))
.clipShape(RoundedRectangle(cornerRadius: 14))
.onTapGesture {
var deeplink = String(
format: "ruddarr://series/open/%d?season=%d&instance=%@",
episode.seriesId,
episode.seasonNumber,
episode.instanceId!.uuidString
)

if !isGrouped {
deeplink.append("&episode=\(episode.episodeNumber)")
}

try? QuickActions.Deeplink(url: URL(string: deeplink)!)()
}
}

var shouldFade: Bool {
Expand Down Expand Up @@ -172,6 +266,19 @@ struct CalendarEpisode: View {
}
}

private struct CalendarPoster: View {
var url: String?
var title: String?

var body: some View {
CachedAsyncImage(.poster, url, placeholder: title)
.aspectRatio(CGSize(width: 150, height: 225), contentMode: .fill)
.frame(width: 44, height: 66)
.clipShape(RoundedRectangle(cornerRadius: 8))
.contentShape(RoundedRectangle(cornerRadius: 8))
}
}

enum CalendarMediaType: CaseIterable {
case all
case movies
Expand Down
Loading