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.54")
|
|
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
|
|
}
|
|
}
|
|
}
|