krz/rune

an ios client for njalla

clone: git clone https://gitbay.org/krz/rune.git

v1.0.0: Rune/Views/Tokens/TokenListView.swift · raw

  1import SwiftUI
  2
  3struct TokenListView: View {
  4    @ObservedObject var viewModel: TokenViewModel
  5    let client: NjallaClient?
  6    let onTokenRemoved: (String) -> Void
  7
  8    @State private var showingAddToken = false
  9    @State private var tokenPendingDeletion: APIToken?
 10
 11    var body: some View {
 12        NavigationStack {
 13            Group {
 14                if let client {
 15                    content(client: client)
 16                } else {
 17                    ContentUnavailableView("Sign in required", systemImage: "key.fill", description: Text("Add a valid Njalla API token to load account tokens."))
 18                }
 19            }
 20            .navigationTitle("Tokens")
 21            .toolbar {
 22                if client != nil {
 23                    Button {
 24                        showingAddToken = true
 25                    } label: {
 26                        Label("Add Token", systemImage: "plus")
 27                    }
 28                }
 29            }
 30        }
 31        .sheet(isPresented: $showingAddToken) {
 32            if let client {
 33                NavigationStack {
 34                    TokenAddView(viewModel: viewModel, client: client)
 35                }
 36            }
 37        }
 38        .confirmationDialog(
 39            deletionTitle,
 40            isPresented: deleteBinding,
 41            titleVisibility: .visible
 42        ) {
 43            Button("Delete Token", role: .destructive) {
 44                guard let tokenPendingDeletion, let client else { return }
 45                Task {
 46                    let removed = await viewModel.removeToken(tokenPendingDeletion, client: client)
 47                    if removed {
 48                        onTokenRemoved(tokenPendingDeletion.key)
 49                    }
 50                    self.tokenPendingDeletion = nil
 51                }
 52            }
 53        }
 54        .alert("API Error", isPresented: errorBinding) {
 55            Button("OK", role: .cancel) {}
 56        } message: {
 57            Text(viewModel.errorMessage ?? "")
 58        }
 59    }
 60
 61    @ViewBuilder
 62    private func content(client: NjallaClient) -> some View {
 63        if viewModel.isLoading && viewModel.tokens.isEmpty {
 64            ProgressView()
 65        } else if viewModel.tokens.isEmpty {
 66            ContentUnavailableView("No Tokens", systemImage: "key.horizontal", description: Text("No API tokens were found on this account."))
 67        } else {
 68            List(viewModel.tokens) { token in
 69                Button {
 70                    tokenPendingDeletion = token
 71                } label: {
 72                    TokenRow(token: token, label: viewModel.tokenLabel(for: token))
 73                }
 74                .buttonStyle(.plain)
 75            }
 76            .listStyle(.insetGrouped)
 77            .refreshable {
 78                await viewModel.loadTokens(client: client)
 79            }
 80        }
 81    }
 82
 83    private var deletionTitle: String {
 84        guard let tokenPendingDeletion else {
 85            return ""
 86        }
 87
 88        return "Delete token \(viewModel.tokenLabel(for: tokenPendingDeletion))?"
 89    }
 90
 91    private var deleteBinding: Binding<Bool> {
 92        Binding(
 93            get: { tokenPendingDeletion != nil },
 94            set: { newValue in
 95                if !newValue {
 96                    tokenPendingDeletion = nil
 97                }
 98            }
 99        )
100    }
101
102    private var errorBinding: Binding<Bool> {
103        Binding(
104            get: { viewModel.errorMessage != nil },
105            set: { newValue in
106                if !newValue {
107                    viewModel.errorMessage = nil
108                }
109            }
110        )
111    }
112}
113
114private struct TokenRow: View {
115    let token: APIToken
116    let label: String
117
118    var body: some View {
119        VStack(alignment: .leading, spacing: 6) {
120            Text(label)
121                .font(.headline)
122
123            Text(methodsText)
124                .font(.subheadline)
125                .foregroundStyle(.secondary)
126
127            if let from = token.from, !from.isEmpty {
128                Text("From: \(from.joined(separator: ", "))")
129                    .font(.subheadline)
130                    .foregroundStyle(.secondary)
131            }
132        }
133        .padding(.vertical, 4)
134    }
135
136    private var methodsText: String {
137        guard let methods = token.allowedMethods, !methods.isEmpty else {
138            return "Methods: Unrestricted"
139        }
140
141        return "Methods: \(methods.joined(separator: ", "))"
142    }
143}