iOS only sent that UA when the hostname looked like viaplay; stored MPDs often do not. Event plays now pin the browser session for the whole fetch chain, still direct to the CDN.
785 lines
29 KiB
Swift
785 lines
29 KiB
Swift
import SwiftUI
|
|
import PhotosUI
|
|
import UIKit
|
|
|
|
struct SettingsScreen: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
|
|
var body: some View {
|
|
List {
|
|
Section {
|
|
NavigationLink {
|
|
EditProfileScreen(profileId: app.store.profileId)
|
|
} label: {
|
|
HStack(spacing: 14) {
|
|
Text(ProfileAvatars.emoji(for: app.store.profileAvatarId))
|
|
.font(.system(size: 40))
|
|
.frame(width: 64, height: 64)
|
|
.background(
|
|
LinearGradient(colors: [McColors.surfaceSoft, McColors.surface], startPoint: .top, endPoint: .bottom),
|
|
in: Circle()
|
|
)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(app.store.profileName ?? "Profiel")
|
|
.font(McFonts.headline)
|
|
.foregroundStyle(McColors.text)
|
|
Text("Naam & avatar wijzigen")
|
|
.font(McFonts.caption)
|
|
.foregroundStyle(McColors.textDim)
|
|
}
|
|
}
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
|
|
Section("Bibliotheek") {
|
|
NavigationLink {
|
|
ShelfPrefsScreen()
|
|
} label: {
|
|
Label("Planken beheren", systemImage: "rectangle.stack")
|
|
.foregroundStyle(McColors.text)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
|
|
Section("Profielen") {
|
|
Button {
|
|
Task { await app.switchProfile() }
|
|
} label: {
|
|
Label("Profiel wisselen", systemImage: "person.2")
|
|
.foregroundStyle(McColors.text)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
if app.canManageProfiles {
|
|
NavigationLink {
|
|
ManageProfilesScreen()
|
|
} label: {
|
|
Label("Profielen beheren", systemImage: "person.crop.circle.badge.plus")
|
|
.foregroundStyle(McColors.text)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
NavigationLink {
|
|
DevicesScreen()
|
|
} label: {
|
|
Label("Apparaten", systemImage: "tv.and.hifispeaker.fill")
|
|
.foregroundStyle(McColors.text)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
}
|
|
|
|
Section("Server") {
|
|
LabeledContent("URL", value: app.store.apiBase)
|
|
if app.canManageProfiles {
|
|
Button {
|
|
Task { await app.logout() }
|
|
} label: {
|
|
Label("Server / account wisselen", systemImage: "server.rack")
|
|
.foregroundStyle(McColors.text)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
}
|
|
|
|
if app.canManageProfiles {
|
|
Section {
|
|
Button(role: .destructive) {
|
|
Task { await app.logout() }
|
|
} label: {
|
|
Label("Uitloggen", systemImage: "rectangle.portrait.and.arrow.right")
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
}
|
|
|
|
Section("Info") {
|
|
LabeledContent("App", value: "VMC iOS")
|
|
LabeledContent("Versie", value: "0.5.57")
|
|
LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit")
|
|
LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit")
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(McColors.bg.ignoresSafeArea())
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .principal) {
|
|
BrandHeader(subtitle: "ACCOUNT")
|
|
}
|
|
}
|
|
.toolbarBackground(McColors.bg, for: .navigationBar)
|
|
.toolbarColorScheme(.dark, for: .navigationBar)
|
|
}
|
|
}
|
|
|
|
struct EditProfileScreen: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
let profileId: String?
|
|
|
|
@State private var name = ""
|
|
@State private var avatarId = "fox"
|
|
@State private var showAvatar = true
|
|
@State private var hasCustomAvatar = false
|
|
@State private var remoteAvatarUrl: String?
|
|
@State private var pin = ""
|
|
@State private var clearPin = false
|
|
@State private var clearCustomAvatar = false
|
|
@State private var pendingImage: UIImage?
|
|
@State private var pickerItem: PhotosPickerItem?
|
|
@State private var message: String?
|
|
@State private var error: String?
|
|
@State private var saving = false
|
|
|
|
private let cols = [GridItem(.adaptive(minimum: 64), spacing: 12)]
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section("Profielfoto") {
|
|
HStack {
|
|
Spacer()
|
|
profilePhotoPreview
|
|
.frame(width: 96, height: 96)
|
|
.clipShape(Circle())
|
|
Spacer()
|
|
}
|
|
Toggle("Profielfoto aan/uit", isOn: $showAvatar)
|
|
.tint(McColors.accent)
|
|
PhotosPicker(selection: $pickerItem, matching: .images) {
|
|
Label("Foto kiezen", systemImage: "photo")
|
|
}
|
|
if hasCustomAvatar || pendingImage != nil {
|
|
Button("Foto verwijderen", role: .destructive) {
|
|
pendingImage = nil
|
|
pickerItem = nil
|
|
clearCustomAvatar = true
|
|
hasCustomAvatar = false
|
|
}
|
|
}
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
.onChange(of: pickerItem) { _, item in
|
|
Task { await loadPicked(item) }
|
|
}
|
|
|
|
Section("Naam") {
|
|
TextField("Naam", text: $name)
|
|
.foregroundStyle(McColors.text)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
Section("Avatar") {
|
|
LazyVGrid(columns: cols, spacing: 12) {
|
|
ForEach(ProfileAvatars.all, id: \.id) { avatar in
|
|
Button {
|
|
avatarId = avatar.id
|
|
} label: {
|
|
Text(avatar.emoji)
|
|
.font(.system(size: 28))
|
|
.frame(width: 56, height: 56)
|
|
.background(
|
|
avatarId == avatar.id ? McColors.accent.opacity(0.25) : McColors.surface,
|
|
in: Circle()
|
|
)
|
|
.overlay(
|
|
Circle().stroke(avatarId == avatar.id ? McColors.accent : Color.clear, lineWidth: 2)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
Section("PIN (optioneel)") {
|
|
SecureField("Nieuwe PIN (4 cijfers)", text: $pin)
|
|
.keyboardType(.numberPad)
|
|
.foregroundStyle(McColors.text)
|
|
Toggle("PIN verwijderen", isOn: $clearPin)
|
|
.tint(McColors.accent)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
if let message {
|
|
Section { Text(message).foregroundStyle(McColors.success) }
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
if let error {
|
|
Section { Text(error).foregroundStyle(McColors.danger) }
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
|
|
Section {
|
|
Button {
|
|
Task { await save() }
|
|
} label: {
|
|
if saving { ProgressView() } else { Text("Opslaan") }
|
|
}
|
|
.disabled(saving || name.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(McColors.bg.ignoresSafeArea())
|
|
.navigationTitle("Mijn profiel")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task { await load() }
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var profilePhotoPreview: some View {
|
|
if let pendingImage {
|
|
Image(uiImage: pendingImage)
|
|
.resizable()
|
|
.scaledToFill()
|
|
} else if hasCustomAvatar, let remoteAvatarUrl {
|
|
AuthenticatedAvatarImage(path: remoteAvatarUrl)
|
|
} else {
|
|
Text(ProfileAvatars.emoji(for: avatarId))
|
|
.font(.system(size: 44))
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(McColors.surface)
|
|
}
|
|
}
|
|
|
|
private func load() async {
|
|
name = app.store.profileName ?? ""
|
|
avatarId = app.store.profileAvatarId ?? "fox"
|
|
showAvatar = app.store.profileShowAvatar
|
|
remoteAvatarUrl = app.store.profileAvatarUrl
|
|
hasCustomAvatar = !(app.store.profileAvatarUrl ?? "").isEmpty
|
|
if let profileId {
|
|
if let payload = try? await app.api.listProfiles(),
|
|
let mine = payload.profiles.first(where: { $0.id == profileId }) {
|
|
name = mine.name
|
|
avatarId = mine.avatarId ?? "fox"
|
|
showAvatar = mine.showsAvatar
|
|
hasCustomAvatar = mine.hasCustomPhoto
|
|
remoteAvatarUrl = mine.avatarUrl
|
|
}
|
|
}
|
|
}
|
|
|
|
private func loadPicked(_ item: PhotosPickerItem?) async {
|
|
guard let item else { return }
|
|
do {
|
|
if let data = try await item.loadTransferable(type: Data.self),
|
|
let image = UIImage(data: data) {
|
|
pendingImage = image.squared()
|
|
clearCustomAvatar = false
|
|
hasCustomAvatar = true
|
|
}
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func save() async {
|
|
guard let profileId else { return }
|
|
saving = true
|
|
error = nil
|
|
message = nil
|
|
do {
|
|
var updated = try await app.api.updateProfile(
|
|
profileId: profileId,
|
|
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
avatarId: avatarId,
|
|
showAvatar: showAvatar,
|
|
clearAvatarImage: clearCustomAvatar && pendingImage == nil,
|
|
pin: clearPin ? nil : (pin.isEmpty ? nil : pin),
|
|
clearPin: clearPin
|
|
)
|
|
if let pendingImage,
|
|
let jpeg = pendingImage.jpegData(compressionQuality: 0.85) {
|
|
let b64 = jpeg.base64EncodedString()
|
|
updated = try await app.api.uploadProfileAvatar(profileId: updated.id, imageBase64: b64)
|
|
}
|
|
app.store.setProfile(
|
|
id: updated.id,
|
|
name: updated.name,
|
|
avatarId: updated.avatarId,
|
|
showAvatar: updated.showsAvatar,
|
|
avatarUrl: updated.avatarUrl ?? ""
|
|
)
|
|
hasCustomAvatar = updated.hasCustomPhoto
|
|
remoteAvatarUrl = updated.avatarUrl
|
|
pendingImage = nil
|
|
clearCustomAvatar = false
|
|
message = "Opgeslagen"
|
|
pin = ""
|
|
clearPin = false
|
|
saving = false
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
saving = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ManageProfilesScreen: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
@State private var profiles: [ViewerProfile] = []
|
|
@State private var canManage = false
|
|
@State private var error: String?
|
|
@State private var showCreate = false
|
|
|
|
var body: some View {
|
|
List {
|
|
ForEach(profiles) { profile in
|
|
NavigationLink {
|
|
AdminEditProfileScreen(profile: profile, canManage: canManage)
|
|
} label: {
|
|
HStack(spacing: 12) {
|
|
Text(ProfileAvatars.emoji(for: profile.avatarId)).font(.title2)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(profile.name).foregroundStyle(McColors.text)
|
|
Text(profile.isOwner == true ? "Hoofdprofiel" : (profile.isKids == true ? "Kids" : "Profiel"))
|
|
.font(McFonts.caption)
|
|
.foregroundStyle(McColors.textDim)
|
|
}
|
|
}
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
|
|
if canManage && profiles.count < 8 {
|
|
Button {
|
|
showCreate = true
|
|
} label: {
|
|
Label("Profiel toevoegen", systemImage: "plus.circle.fill")
|
|
.foregroundStyle(McColors.accent)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
|
|
if let error {
|
|
Text(error).foregroundStyle(McColors.danger)
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(McColors.bg.ignoresSafeArea())
|
|
.navigationTitle("Profielen")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task { await load() }
|
|
.sheet(isPresented: $showCreate) {
|
|
CreateProfileSheet {
|
|
showCreate = false
|
|
Task { await load() }
|
|
}
|
|
.environmentObject(app)
|
|
}
|
|
}
|
|
|
|
private func load() async {
|
|
do {
|
|
let payload = try await app.api.listProfiles()
|
|
profiles = payload.profiles
|
|
canManage = payload.canManage == true
|
|
app.canManageProfiles = canManage
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CreateProfileSheet: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
let onDone: () -> Void
|
|
|
|
@State private var name = ""
|
|
@State private var avatarId = "fox"
|
|
@State private var isKids = false
|
|
@State private var pin = ""
|
|
@State private var error: String?
|
|
@State private var saving = false
|
|
|
|
private let cols = [GridItem(.adaptive(minimum: 56), spacing: 10)]
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("Naam") {
|
|
TextField("Naam", text: $name)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
Section("Avatar") {
|
|
LazyVGrid(columns: cols, spacing: 10) {
|
|
ForEach(ProfileAvatars.all, id: \.id) { avatar in
|
|
Button { avatarId = avatar.id } label: {
|
|
Text(avatar.emoji)
|
|
.font(.title2)
|
|
.frame(width: 48, height: 48)
|
|
.background(avatarId == avatar.id ? McColors.accent.opacity(0.25) : McColors.surface, in: Circle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
Section {
|
|
Toggle("Kids-profiel", isOn: $isKids).tint(McColors.accent)
|
|
SecureField("PIN (optioneel)", text: $pin).keyboardType(.numberPad)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
if let error {
|
|
Text(error).foregroundStyle(McColors.danger)
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(McColors.bg.ignoresSafeArea())
|
|
.navigationTitle("Nieuw profiel")
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Annuleer", action: onDone)
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("Maak") {
|
|
Task { await create() }
|
|
}
|
|
.disabled(saving || name.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func create() async {
|
|
saving = true
|
|
error = nil
|
|
do {
|
|
_ = try await app.api.createProfile(
|
|
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
avatarId: avatarId,
|
|
isKids: isKids,
|
|
pin: pin.isEmpty ? nil : pin
|
|
)
|
|
saving = false
|
|
onDone()
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
saving = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AdminEditProfileScreen: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
let profile: ViewerProfile
|
|
let canManage: Bool
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var name = ""
|
|
@State private var avatarId = "fox"
|
|
@State private var showAvatar = true
|
|
@State private var hasCustomAvatar = false
|
|
@State private var remoteAvatarUrl: String?
|
|
@State private var isKids = false
|
|
@State private var pin = ""
|
|
@State private var clearPin = false
|
|
@State private var clearCustomAvatar = false
|
|
@State private var pendingImage: UIImage?
|
|
@State private var pickerItem: PhotosPickerItem?
|
|
@State private var error: String?
|
|
@State private var message: String?
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section("Profielfoto") {
|
|
HStack {
|
|
Spacer()
|
|
Group {
|
|
if let pendingImage {
|
|
Image(uiImage: pendingImage).resizable().scaledToFill()
|
|
} else if hasCustomAvatar, let remoteAvatarUrl {
|
|
AuthenticatedAvatarImage(path: remoteAvatarUrl)
|
|
} else {
|
|
Text(ProfileAvatars.emoji(for: avatarId))
|
|
.font(.system(size: 40))
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(McColors.surface)
|
|
}
|
|
}
|
|
.frame(width: 88, height: 88)
|
|
.clipShape(Circle())
|
|
Spacer()
|
|
}
|
|
Toggle("Profielfoto aan/uit", isOn: $showAvatar)
|
|
.tint(McColors.accent)
|
|
PhotosPicker(selection: $pickerItem, matching: .images) {
|
|
Label("Foto kiezen", systemImage: "photo")
|
|
}
|
|
if hasCustomAvatar || pendingImage != nil {
|
|
Button("Foto verwijderen", role: .destructive) {
|
|
pendingImage = nil
|
|
pickerItem = nil
|
|
clearCustomAvatar = true
|
|
hasCustomAvatar = false
|
|
}
|
|
}
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
.onChange(of: pickerItem) { _, item in
|
|
Task {
|
|
guard let item else { return }
|
|
if let data = try? await item.loadTransferable(type: Data.self),
|
|
let image = UIImage(data: data) {
|
|
pendingImage = image.squared()
|
|
clearCustomAvatar = false
|
|
hasCustomAvatar = true
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Naam") {
|
|
TextField("Naam", text: $name)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
Section("Avatar") {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack {
|
|
ForEach(ProfileAvatars.all, id: \.id) { avatar in
|
|
Button { avatarId = avatar.id } label: {
|
|
Text(avatar.emoji)
|
|
.font(.title)
|
|
.padding(8)
|
|
.background(avatarId == avatar.id ? McColors.accent.opacity(0.25) : McColors.surface, in: Circle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
if canManage && profile.isOwner != true {
|
|
Section {
|
|
Toggle("Kids-profiel", isOn: $isKids).tint(McColors.accent)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
|
|
Section("PIN") {
|
|
SecureField("Nieuwe PIN", text: $pin).keyboardType(.numberPad)
|
|
Toggle("PIN verwijderen", isOn: $clearPin).tint(McColors.accent)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
Section {
|
|
Button("Opslaan") { Task { await save() } }
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
|
|
if canManage && profile.isOwner != true {
|
|
Section {
|
|
Button("Verwijderen", role: .destructive) {
|
|
Task { await remove() }
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
}
|
|
}
|
|
|
|
if let message { Text(message).foregroundStyle(McColors.success).listRowBackground(McColors.bgElevated) }
|
|
if let error { Text(error).foregroundStyle(McColors.danger).listRowBackground(McColors.bgElevated) }
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(McColors.bg.ignoresSafeArea())
|
|
.navigationTitle(profile.name)
|
|
.task {
|
|
name = profile.name
|
|
avatarId = profile.avatarId ?? "fox"
|
|
isKids = profile.isKids == true
|
|
showAvatar = profile.showsAvatar
|
|
hasCustomAvatar = profile.hasCustomPhoto
|
|
remoteAvatarUrl = profile.avatarUrl
|
|
}
|
|
}
|
|
|
|
private func save() async {
|
|
do {
|
|
var updated = try await app.api.updateProfile(
|
|
profileId: profile.id,
|
|
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
avatarId: avatarId,
|
|
showAvatar: showAvatar,
|
|
clearAvatarImage: clearCustomAvatar && pendingImage == nil,
|
|
isKids: canManage && profile.isOwner != true ? isKids : nil,
|
|
pin: clearPin ? nil : (pin.isEmpty ? nil : pin),
|
|
clearPin: clearPin
|
|
)
|
|
if let pendingImage,
|
|
let jpeg = pendingImage.jpegData(compressionQuality: 0.85) {
|
|
updated = try await app.api.uploadProfileAvatar(
|
|
profileId: updated.id,
|
|
imageBase64: jpeg.base64EncodedString()
|
|
)
|
|
}
|
|
if updated.id == app.store.profileId {
|
|
app.store.setProfile(
|
|
id: updated.id,
|
|
name: updated.name,
|
|
avatarId: updated.avatarId,
|
|
showAvatar: updated.showsAvatar,
|
|
avatarUrl: updated.avatarUrl ?? ""
|
|
)
|
|
}
|
|
hasCustomAvatar = updated.hasCustomPhoto
|
|
remoteAvatarUrl = updated.avatarUrl
|
|
pendingImage = nil
|
|
clearCustomAvatar = false
|
|
message = "Opgeslagen"
|
|
error = nil
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func remove() async {
|
|
do {
|
|
try await app.api.deleteProfile(profileId: profile.id)
|
|
dismiss()
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AuthenticatedAvatarImage: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
let path: String
|
|
@State private var image: UIImage?
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let image {
|
|
Image(uiImage: image)
|
|
.resizable()
|
|
.scaledToFill()
|
|
} else {
|
|
ProgressView()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(McColors.surface)
|
|
}
|
|
}
|
|
.task(id: path) { await load() }
|
|
}
|
|
|
|
private func load() async {
|
|
guard let url = await app.api.absoluteUrl(path) else { return }
|
|
var req = URLRequest(url: url)
|
|
if let token = await app.api.bearerToken() {
|
|
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
}
|
|
if let (data, _) = try? await URLSession.shared.data(for: req),
|
|
let ui = UIImage(data: data) {
|
|
image = ui
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension UIImage {
|
|
func squared() -> UIImage {
|
|
let side = min(size.width, size.height)
|
|
let origin = CGPoint(x: (size.width - side) / 2, y: (size.height - side) / 2)
|
|
let crop = CGRect(origin: origin, size: CGSize(width: side, height: side))
|
|
guard let cg = cgImage?.cropping(to: CGRect(
|
|
x: crop.origin.x * scale,
|
|
y: crop.origin.y * scale,
|
|
width: crop.size.width * scale,
|
|
height: crop.size.height * scale
|
|
)) else { return self }
|
|
let squared = UIImage(cgImage: cg, scale: scale, orientation: imageOrientation)
|
|
let target: CGFloat = 512
|
|
let format = UIGraphicsImageRendererFormat.default()
|
|
format.scale = 1
|
|
let renderer = UIGraphicsImageRenderer(size: CGSize(width: target, height: target), format: format)
|
|
return renderer.image { _ in
|
|
squared.draw(in: CGRect(x: 0, y: 0, width: target, height: target))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ShelfPrefsScreen: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
@State private var prefs: [ShelfPref] = []
|
|
@State private var error: String?
|
|
@State private var message: String?
|
|
|
|
var body: some View {
|
|
List {
|
|
Section {
|
|
Text("Zet planken aan/uit en sleep om te sorteren. Dit bepaalt Home én Films/Series.")
|
|
.font(McFonts.caption)
|
|
.foregroundStyle(McColors.textDim)
|
|
}
|
|
.listRowBackground(McColors.bgElevated)
|
|
|
|
Section("Planken") {
|
|
ForEach($prefs) { $pref in
|
|
Toggle(isOn: $pref.visible) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(pref.name).foregroundStyle(McColors.text)
|
|
Text((pref.kind ?? "").uppercased())
|
|
.font(McFonts.caption)
|
|
.foregroundStyle(McColors.textDim)
|
|
}
|
|
}
|
|
.tint(McColors.accent)
|
|
.listRowBackground(McColors.bgElevated)
|
|
.onChange(of: pref.visible) { _, _ in
|
|
Task { await persist() }
|
|
}
|
|
}
|
|
.onMove { from, to in
|
|
prefs.move(fromOffsets: from, toOffset: to)
|
|
Task { await persist() }
|
|
}
|
|
}
|
|
|
|
if let message { Text(message).foregroundStyle(McColors.success).listRowBackground(McColors.bgElevated) }
|
|
if let error { Text(error).foregroundStyle(McColors.danger).listRowBackground(McColors.bgElevated) }
|
|
}
|
|
.environment(\.editMode, .constant(.active))
|
|
.scrollContentBackground(.hidden)
|
|
.background(McColors.bg.ignoresSafeArea())
|
|
.navigationTitle("Planken")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task { await load() }
|
|
}
|
|
|
|
private func load() async {
|
|
do {
|
|
prefs = try await app.api.getShelfPrefs().sorted { $0.sortOrder < $1.sortOrder }
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func persist() async {
|
|
do {
|
|
let next = prefs.enumerated().map { i, p in
|
|
ShelfPref(
|
|
shelfId: p.shelfId,
|
|
name: p.name,
|
|
slug: p.slug,
|
|
kind: p.kind,
|
|
visible: p.visible,
|
|
sortOrder: i
|
|
)
|
|
}
|
|
prefs = next
|
|
try await app.api.saveShelfPrefs(next)
|
|
message = "Opgeslagen"
|
|
error = nil
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
}
|