Skip to content

Commit

Permalink
Implement plugin actions in the details screen (#24074)
Browse files Browse the repository at this point in the history
  • Loading branch information
crazytonyli authored Feb 18, 2025
1 parent fcb4a13 commit ad30f1e
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 104 deletions.
49 changes: 22 additions & 27 deletions Modules/Sources/WordPressCore/Plugins/InstalledPlugin.swift
Original file line number Diff line number Diff line change
@@ -1,47 +1,42 @@
import Foundation
import WordPressAPI
import WordPressAPIInternal

public struct InstalledPlugin: Equatable, Hashable, Identifiable, Sendable {
public var slug: PluginSlug
public var iconURL: URL?
public var name: String
public var version: String
public var author: String
public var shortDescription: String
public var isActive: Bool
public var status: PluginStatus
public var networkOnly: Bool

public init(slug: PluginSlug, iconURL: URL?, name: String, version: String, author: String, shortDescription: String, isActive: Bool) {
self.slug = slug
self.iconURL = iconURL
self.name = name
self.version = version
self.author = author
self.shortDescription = shortDescription
self.isActive = isActive
public var isActive: Bool {
status == .active || status == .networkActive
}

public init(plugin: PluginWithViewContext) {
self.slug = plugin.plugin
iconURL = nil
name = plugin.name
version = plugin.version
author = plugin.author
shortDescription = plugin.description.raw
isActive = plugin.status == .active || plugin.status == .networkActive
public var id: String {
slug.slug
}

public init(plugin: PluginWithEditContext) {
init(plugin: PluginWithEditContext) {
self.slug = plugin.plugin
iconURL = nil
name = plugin.name
version = plugin.version
author = plugin.author
shortDescription = plugin.description.raw
isActive = plugin.status == .active || plugin.status == .networkActive
self.name = plugin.name
self.version = plugin.version
self.author = plugin.author
self.shortDescription = plugin.description.raw
self.status = plugin.status
self.networkOnly = plugin.networkOnly
}

public var id: String {
slug.slug
init(plugin: PluginWithViewContext) {
self.slug = plugin.plugin
self.name = plugin.name
self.version = plugin.version
self.author = plugin.author
self.shortDescription = plugin.description.raw
self.status = plugin.status
self.networkOnly = plugin.networkOnly
}

public var possibleWpOrgDirectorySlug: PluginWpOrgDirectorySlug? {
Expand Down
18 changes: 13 additions & 5 deletions Modules/Sources/WordPressCore/Plugins/PluginService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,26 @@ public actor PluginService: PluginServiceProtocol {
return nil
}

public func togglePluginActivation(slug: PluginSlug) async throws {
let plugin = try await client.api.plugins.retrieveWithViewContext(pluginSlug: slug)
let newStatus: PluginStatus = plugin.data.status == .inactive ? (plugin.data.networkOnly ? .networkActive : .active) : .inactive
let newPlugin = try await client.api.plugins.update(pluginSlug: slug, params: .init(status: newStatus))
try await installedPluginDataStore.store([.init(plugin: newPlugin.data)])
public func updatePluginStatus(plugin: InstalledPlugin, activated: Bool) async throws -> InstalledPlugin {
let newStatus: PluginStatus = plugin.status == .inactive ? (plugin.networkOnly ? .networkActive : .active) : .inactive
let newPlugin = try await client.api.plugins.update(pluginSlug: plugin.slug, params: .init(status: newStatus))
let plugin = InstalledPlugin(plugin: newPlugin.data)
try await installedPluginDataStore.store([plugin])
return plugin
}

public func uninstalledPlugin(slug: PluginSlug) async throws {
let _ = try await client.api.plugins.delete(pluginSlug: slug)
try await installedPluginDataStore.delete(query: .slug(slug))
}

public func installPlugin(slug: PluginWpOrgDirectorySlug) async throws -> InstalledPlugin {
let plugin = try await client.api.plugins.create(params: .init(slug: slug, status: .inactive)).data
let installed = InstalledPlugin(plugin: plugin)
try await installedPluginDataStore.store([installed])
return installed
}

public func fetchPluginsDirectory(category: WordPressOrgApiPluginDirectoryCategory) async throws {
// Hard-code the pagination parameters for now. We can suface these parameters when the app needs pagination.
let plugins = try await wpOrgClient.browsePlugins(category: category, page: 1, pageSize: 10).plugins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ public protocol PluginServiceProtocol: Actor {

func resolveIconURL(of slug: PluginWpOrgDirectorySlug, plugin: PluginInformation?) async -> URL?

func togglePluginActivation(slug: PluginSlug) async throws
func updatePluginStatus(plugin: InstalledPlugin, activated: Bool) async throws -> InstalledPlugin

func uninstalledPlugin(slug: PluginSlug) async throws
func installPlugin(slug: PluginWpOrgDirectorySlug) async throws -> InstalledPlugin

func fetchPluginsDirectory(category: WordPressOrgApiPluginDirectoryCategory) async throws
func pluginDirectoryUpdates(query: CategorizedPluginInformationDataStoreQuery) async -> AsyncStream<Result<[CategorizedPluginInformation], Error>>
Expand Down
11 changes: 0 additions & 11 deletions WordPress/Classes/Plugins/Views/InstalledPluginsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,6 @@ final class InstalledPluginsListViewModel: ObservableObject {
}
}

func toggle(slug: PluginSlug) async {
self.updating.insert(slug)
defer { self.updating.remove(slug) }

do {
try await self.service.togglePluginActivation(slug: slug)
} catch {
DDLogError("Failed to update plugin: \(error)")
}
}

func uninstall(slug: PluginSlug) async {
self.updating.insert(slug)
defer { self.updating.remove(slug) }
Expand Down
94 changes: 83 additions & 11 deletions WordPress/Classes/Plugins/Views/PluginDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ struct PluginDetailsView: View {
return plugin.isActive
? .activated(plugin: plugin)
: .activate(plugin: plugin) {
// TODO
Task { await viewModel.updatePluginStatus(plugin, activated: true) }
}
} else {
return .install(slug: slug) {
// TODO
Task { await viewModel.install() }
}
}
}
Expand Down Expand Up @@ -87,7 +87,7 @@ struct PluginDetailsView: View {

actionButton
.view
.disabled(viewModel.isLoading || viewModel.isActivating || viewModel.isUninstalling)
.disabled(viewModel.isLoading || viewModel.isActivating || viewModel.isDeactivating || viewModel.isUninstalling || viewModel.isInstalling)
}
.listRowSeparator(.hidden)

Expand All @@ -98,7 +98,13 @@ struct PluginDetailsView: View {
.listSectionSeparator(.hidden)

if viewModel.isUninstalling {
uninstallingView()
inlineProgressView(title: Strings.uninstallingTitle, message: Strings.uninstallingMessage)
} else if viewModel.isInstalling {
inlineProgressView(title: Strings.installingTitle, message: Strings.installingMessage)
} else if viewModel.isActivating {
inlineProgressView(title: Strings.activatingTitle, message: Strings.activatingMessage)
} else if viewModel.isDeactivating {
inlineProgressView(title: Strings.deactivatingTitle, message: Strings.deactivatingMessage)
} else if let newVersion {
updateAvailableView(newVersion)
}
Expand Down Expand Up @@ -134,6 +140,14 @@ struct PluginDetailsView: View {
}
}

if let installed = viewModel.installed, installed.isActive {
Button {
Task { await viewModel.updatePluginStatus(installed, activated: false) }
} label: {
Label(Strings.deactivateButton, systemImage: "circle.slash")
}
}

if let url = wpOrgURL {
Section {
ShareLink(item: url)
Expand Down Expand Up @@ -214,14 +228,14 @@ struct PluginDetailsView: View {
}

@ViewBuilder
private func uninstallingView() -> some View {
private func inlineProgressView(title: String, message: String) -> some View {
HStack {
ProgressView()

VStack(alignment: .leading) {
Text(Strings.uninstallingTitle)
Text(title)
.font(.headline)
Text(Strings.uninstallingMessage)
Text(message)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Expand Down Expand Up @@ -383,10 +397,12 @@ final class WordPressPluginDetailViewModel: ObservableObject {

@Published private(set) var isLoading = false
@Published private(set) var isUninstalling = false
@Published private(set) var isInstalling = false
@Published private(set) var plugin: PluginInformation?
@Published private(set) var installed: InstalledPlugin?
@Published private(set) var error: String?
@Published private(set) var isActivating = false
@Published private(set) var isDeactivating = false

private var initialLoad = false

Expand Down Expand Up @@ -428,14 +444,15 @@ final class WordPressPluginDetailViewModel: ObservableObject {
}
}

func activate(_ plugin: InstalledPlugin) async {
isActivating = true
func updatePluginStatus(_ plugin: InstalledPlugin, activated: Bool) async {
let keyPath: ReferenceWritableKeyPath<WordPressPluginDetailViewModel, Bool> = activated ? \.isActivating : \.isDeactivating
self[keyPath: keyPath] = true
defer {
isActivating = false
self[keyPath: keyPath] = false
}

do {
try await service.togglePluginActivation(slug: plugin.slug)
self.installed = try await service.updatePluginStatus(plugin: plugin, activated: false)
} catch {
// TODO: Show an error notice
}
Expand All @@ -453,6 +470,19 @@ final class WordPressPluginDetailViewModel: ObservableObject {
// TODO: Show an error notice
}
}

func install() async {
isInstalling = true
defer {
isInstalling = false
}

do {
self.installed = try await service.installPlugin(slug: slug)
} catch {
// TODO: Show an error notice
}
}
}

private extension PluginInformation {
Expand Down Expand Up @@ -619,6 +649,12 @@ private enum Strings {
comment: "Button label to activate a plugin"
)

static let deactivateButton = NSLocalizedString(
"pluginDetails.deactivate.button",
value: "Deactivate",
comment: "Button label to deactivate a plugin"
)

static let activatedButton = NSLocalizedString(
"pluginDetails.activated.button",
value: "Activated",
Expand All @@ -636,4 +672,40 @@ private enum Strings {
value: "Please wait while the plugin is being removed...",
comment: "Message shown while a plugin is being uninstalled"
)

static let installingTitle = NSLocalizedString(
"pluginDetails.install.title",
value: "Install Plugin",
comment: "Title shown while a plugin is being installed"
)

static let installingMessage = NSLocalizedString(
"pluginDetails.install.message",
value: "Please wait while the plugin is being installed...",
comment: "Message shown while a plugin is being installed"
)

static let activatingTitle = NSLocalizedString(
"pluginDetails.activating.title",
value: "Activating Plugin",
comment: "Title shown while a plugin is being activated"
)

static let activatingMessage = NSLocalizedString(
"pluginDetails.activating.message",
value: "Please wait while the plugin is being activated...",
comment: "Message shown while a plugin is being activated"
)

static let deactivatingTitle = NSLocalizedString(
"pluginDetails.deactivating.title",
value: "Deactivating Plugin",
comment: "Title shown while a plugin is being deactivated"
)

static let deactivatingMessage = NSLocalizedString(
"pluginDetails.deactivating.message",
value: "Please wait while the plugin is being deactivated...",
comment: "Message shown while a plugin is being deactivated"
)
}
49 changes: 0 additions & 49 deletions WordPress/Classes/Plugins/Views/PluginListItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,62 +40,13 @@ struct PluginListItemView: View {

Spacer()
}
.contextMenu(menuItems: {
menuItems
})
.overlay(alignment: .topTrailing) {
Menu {
menuItems
} label: {
Image(systemName: "ellipsis")
.padding(4)
.frame(width: 44, height: 44, alignment: .topTrailing)
.contentShape(Rectangle())
}
.foregroundStyle(.secondary)
}
.sheet(isPresented: $isShowingSafariView) {
if let url = plugin.possibleWpOrgDirectoryURL {
SafariView(url: url)
}
}
}

@ViewBuilder
private var menuItems: some View {
Section {
if plugin.isActive {
Button(Strings.deactivate, systemImage: "bolt.slash") {
Task {
await viewModel.toggle(slug: plugin.slug)
}
}
} else {
Button(Strings.activate, systemImage: "bolt") {
Task {
await viewModel.toggle(slug: plugin.slug)
}
}
Button(Strings.delete, systemImage: "trash", role: .destructive) {
Task {
await viewModel.uninstall(slug: plugin.slug)
}
}
}
}
.disabled(isUpdating)

if plugin.possibleWpOrgDirectoryURL != nil {
Section {
Button {
isShowingSafariView = true
} label: {
Label(Strings.viewOnWordPressOrg, systemImage: "safari")
}
}
}
}

private enum Strings {
static func author(_ author: String) -> String {
let format = NSLocalizedString("sitePluginsList.item.author", value: "By %@", comment: "The plugin author displayed in the plugins list. The first argument is plugin author name")
Expand Down

0 comments on commit ad30f1e

Please sign in to comment.