mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Add Remote Servers and File Sources sections to Home
Two new configurable Home sections list enabled sources the way the Sources screen does; tapping a remote server opens its browse view and tapping a file source opens its file browser. A file source with missing credentials shows the auth-required badge and routes to the Sources screen instead. Both sections are hidden by default.
This commit is contained in:
@@ -434,6 +434,12 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
case bookmarks
|
||||
case history
|
||||
case downloads
|
||||
/// All enabled remote servers, listed like the Sources screen; tapping one
|
||||
/// opens its browse view.
|
||||
case remoteServers
|
||||
/// All enabled file sources (WebDAV, SMB, local folders), listed like the
|
||||
/// Sources screen; tapping one opens its file browser.
|
||||
case fileSources
|
||||
case instanceContent(instanceID: UUID, contentType: InstanceContentType)
|
||||
/// Legacy, decode-only. Media sources are shortcuts-only now — a media-source
|
||||
/// "section" was just a browse link. Kept so existing saved `homeSectionOrder`
|
||||
@@ -449,6 +455,8 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
case .bookmarks: return "bookmarks"
|
||||
case .history: return "history"
|
||||
case .downloads: return "downloads"
|
||||
case .remoteServers: return "remoteServers"
|
||||
case .fileSources: return "fileSources"
|
||||
case .instanceContent(let instanceID, let contentType):
|
||||
return "instance_\(instanceID.uuidString)_\(contentType.rawValue)"
|
||||
case .mediaSource(let sourceID):
|
||||
@@ -459,18 +467,18 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
/// Default order for sections.
|
||||
static var defaultOrder: [HomeSectionItem] {
|
||||
#if os(tvOS)
|
||||
[.continueWatching, .feed, .bookmarks, .history]
|
||||
[.continueWatching, .feed, .bookmarks, .history, .remoteServers, .fileSources]
|
||||
#else
|
||||
[.continueWatching, .feed, .bookmarks, .history, .downloads]
|
||||
[.continueWatching, .feed, .bookmarks, .history, .downloads, .remoteServers, .fileSources]
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Default visibility for sections (only continue watching on by default).
|
||||
static var defaultVisibility: [HomeSectionItem: Bool] {
|
||||
#if os(tvOS)
|
||||
[.continueWatching: true, .feed: false, .bookmarks: false, .history: false]
|
||||
[.continueWatching: true, .feed: false, .bookmarks: false, .history: false, .remoteServers: false, .fileSources: false]
|
||||
#else
|
||||
[.continueWatching: true, .feed: false, .bookmarks: false, .history: false, .downloads: false]
|
||||
[.continueWatching: true, .feed: false, .bookmarks: false, .history: false, .downloads: false, .remoteServers: false, .fileSources: false]
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -483,6 +491,8 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
case .bookmarks: return "bookmark.fill"
|
||||
case .history: return "clock.arrow.circlepath"
|
||||
case .downloads: return "arrow.down.circle.fill"
|
||||
case .remoteServers: return "server.rack"
|
||||
case .fileSources: return "externaldrive.connected.to.line.below"
|
||||
case .instanceContent(_, let contentType):
|
||||
return contentType.icon
|
||||
case .mediaSource:
|
||||
@@ -499,6 +509,8 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
case .bookmarks: return String(localized: "home.section.bookmarks")
|
||||
case .history: return String(localized: "home.section.history")
|
||||
case .downloads: return String(localized: "home.section.downloads")
|
||||
case .remoteServers: return String(localized: "sources.section.remoteServers")
|
||||
case .fileSources: return String(localized: "sources.section.fileSources")
|
||||
case .instanceContent(_, let contentType):
|
||||
return contentType.localizedTitle
|
||||
case .mediaSource:
|
||||
@@ -521,6 +533,8 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
case bookmarks
|
||||
case history
|
||||
case downloads
|
||||
case remoteServers
|
||||
case fileSources
|
||||
case instanceContent
|
||||
case mediaSource
|
||||
}
|
||||
@@ -539,6 +553,10 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
try container.encode(SectionType.history, forKey: .type)
|
||||
case .downloads:
|
||||
try container.encode(SectionType.downloads, forKey: .type)
|
||||
case .remoteServers:
|
||||
try container.encode(SectionType.remoteServers, forKey: .type)
|
||||
case .fileSources:
|
||||
try container.encode(SectionType.fileSources, forKey: .type)
|
||||
case .instanceContent(let instanceID, let contentType):
|
||||
try container.encode(SectionType.instanceContent, forKey: .type)
|
||||
try container.encode(instanceID, forKey: .instanceID)
|
||||
@@ -564,6 +582,10 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable {
|
||||
self = .history
|
||||
case .downloads:
|
||||
self = .downloads
|
||||
case .remoteServers:
|
||||
self = .remoteServers
|
||||
case .fileSources:
|
||||
self = .fileSources
|
||||
case .instanceContent:
|
||||
let instanceID = try container.decode(UUID.self, forKey: .instanceID)
|
||||
let contentType = try container.decode(InstanceContentType.self, forKey: .contentType)
|
||||
|
||||
@@ -335,6 +335,10 @@ struct HomeView: View {
|
||||
}
|
||||
case .downloads:
|
||||
break
|
||||
case .remoteServers:
|
||||
if !homeRemoteServers.isEmpty { return false }
|
||||
case .fileSources:
|
||||
if !homeFileSources.isEmpty { return false }
|
||||
case .instanceContent(let instanceID, let contentType):
|
||||
if hasInstanceContent(instanceID: instanceID, contentType: contentType) { return false }
|
||||
case .mediaSource:
|
||||
@@ -1279,6 +1283,14 @@ struct HomeView: View {
|
||||
#else
|
||||
EmptyView()
|
||||
#endif
|
||||
case .remoteServers:
|
||||
if !homeRemoteServers.isEmpty {
|
||||
remoteServersSection
|
||||
}
|
||||
case .fileSources:
|
||||
if !homeFileSources.isEmpty {
|
||||
fileSourcesSection
|
||||
}
|
||||
case .instanceContent(let instanceID, let contentType):
|
||||
instanceContentSection(instanceID: instanceID, contentType: contentType)
|
||||
case .mediaSource:
|
||||
@@ -1289,6 +1301,141 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sources Sections
|
||||
|
||||
/// Enabled remote servers in the Sources screen's order.
|
||||
private var homeRemoteServers: [Instance] {
|
||||
let instances = appEnvironment?.instancesManager.enabledInstances ?? []
|
||||
return appEnvironment?.sourcesSettings.sorted(instances) ?? instances
|
||||
}
|
||||
|
||||
/// Enabled file sources in the Sources screen's order.
|
||||
private var homeFileSources: [MediaSource] {
|
||||
let sources = appEnvironment?.mediaSourcesManager.enabledSources ?? []
|
||||
return appEnvironment?.sourcesSettings.sorted(sources) ?? sources
|
||||
}
|
||||
|
||||
private var remoteServersSection: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
sectionHeader(title: "sources.section.remoteServers") {
|
||||
appEnvironment?.navigationCoordinator.navigate(to: .mediaSources)
|
||||
}
|
||||
|
||||
VideoListContent(listStyle: listStyle) {
|
||||
let instances = homeRemoteServers
|
||||
ForEach(Array(instances.enumerated()), id: \.element.id) { index, instance in
|
||||
SourceListRow(isLast: index == instances.count - 1, listStyle: listStyle) {
|
||||
Button {
|
||||
appEnvironment?.navigationCoordinator.navigate(to: .instanceBrowse(instance))
|
||||
} label: {
|
||||
homeSourceRow(
|
||||
systemImage: instance.type.systemImage,
|
||||
name: instance.displayName,
|
||||
detail: "\(instance.type.displayName) - \(instance.url.host ?? instance.url.absoluteString)"
|
||||
)
|
||||
}
|
||||
#if os(macOS)
|
||||
.buttonStyle(.plain)
|
||||
#elseif os(tvOS)
|
||||
.buttonStyle(TVHomeRowButtonStyle())
|
||||
#else
|
||||
.foregroundStyle(.primary)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var fileSourcesSection: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
sectionHeader(title: "sources.section.fileSources") {
|
||||
appEnvironment?.navigationCoordinator.navigate(to: .mediaSources)
|
||||
}
|
||||
|
||||
VideoListContent(listStyle: listStyle) {
|
||||
let sources = homeFileSources
|
||||
ForEach(Array(sources.enumerated()), id: \.element.id) { index, source in
|
||||
let needsPassword = appEnvironment?.mediaSourcesManager.needsPassword(for: source) ?? false
|
||||
SourceListRow(isLast: index == sources.count - 1, listStyle: listStyle) {
|
||||
Button {
|
||||
if needsPassword {
|
||||
// The browser can't open without credentials; the
|
||||
// Sources screen holds the edit flow that fixes it.
|
||||
appEnvironment?.navigationCoordinator.navigate(to: .mediaSources)
|
||||
} else {
|
||||
appEnvironment?.navigationCoordinator.navigate(to: .mediaBrowser(source, path: "/"))
|
||||
}
|
||||
} label: {
|
||||
homeSourceRow(
|
||||
systemImage: source.type.systemImage,
|
||||
name: source.name,
|
||||
detail: "\(source.type.displayName) - \(source.urlDisplayString)",
|
||||
needsPassword: needsPassword
|
||||
)
|
||||
}
|
||||
#if os(macOS)
|
||||
.buttonStyle(.plain)
|
||||
#elseif os(tvOS)
|
||||
.buttonStyle(TVHomeRowButtonStyle())
|
||||
#else
|
||||
.foregroundStyle(.primary)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Row content matching the Sources screen's listing.
|
||||
private func homeSourceRow(systemImage: String, name: String, detail: String, needsPassword: Bool = false) -> some View {
|
||||
#if os(tvOS)
|
||||
let rowSpacing: CGFloat = 24
|
||||
#else
|
||||
let rowSpacing: CGFloat = 12
|
||||
#endif
|
||||
#if os(macOS)
|
||||
let iconFont: Font = .title3
|
||||
let iconFrameWidth: CGFloat = 24
|
||||
let titleFont: Font = .body
|
||||
#else
|
||||
let iconFont: Font = .title2
|
||||
let iconFrameWidth: CGFloat = 32
|
||||
let titleFont: Font = .headline
|
||||
#endif
|
||||
return HStack(spacing: rowSpacing) {
|
||||
Image(systemName: systemImage)
|
||||
.font(iconFont)
|
||||
.foregroundStyle(.tint)
|
||||
.frame(width: iconFrameWidth)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(name)
|
||||
.font(titleFont)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if needsPassword {
|
||||
Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
#if !os(macOS)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
#endif
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private var continueWatchingSection: some View {
|
||||
let limitedEntries = Array(recentContinueWatching.prefix(sectionItemsLimit))
|
||||
let videoList = limitedEntries.map { $0.toVideo() }
|
||||
|
||||
Reference in New Issue
Block a user