Skip to content

Commit 818a202

Browse files
committed
feat: enrich plugin list rows with version, status badges, and download counts
1 parent 4aac87a commit 818a202

5 files changed

Lines changed: 118 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12-
- Redesigned Plugins settings tab with HSplitView master-detail layout: plugin list on the left, detail pane on the right, matching macOS conventions
12+
- Redesigned Plugins settings tab with HSplitView master-detail layout: plugin list on the left, detail pane on the right, matching macOS conventions. Plugin rows now show version, author/capability, and install status at a glance
13+
- Download counts in browse tab now always fetch latest from GitHub API (5-minute in-memory cooldown per session)
1314
- Replaced ~40 hardcoded `DatabaseType` switches across ~20 UI files with dynamic plugin property lookups via `PluginManager`, so third-party plugins get correct UI behavior (colors, labels, editor language, feature toggles) automatically
1415
- ConnectionFormView now fully dynamic: pgpass toggle, password visibility, and SSH/SSL tab visibility all driven by plugin metadata (`FieldSection`, `hidesPassword`, `supportsSSH`/`supportsSSL`) instead of hardcoded type checks
1516
- Replaced `AppState.isMongoDB`/`isRedis` booleans with `AppState.editorLanguage: EditorLanguage` for extensible editor language detection

TablePro/Core/Plugins/Registry/DownloadCountService.swift

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ final class DownloadCountService {
1111
static let shared = DownloadCountService()
1212

1313
private var counts: [String: Int] = [:]
14+
private var lastFetchDate: Date?
15+
private static let cooldown: TimeInterval = 300 // 5 minutes
1416
private static let logger = Logger(subsystem: "com.TablePro", category: "DownloadCountService")
1517

16-
private static let cacheKey = "downloadCountsCache"
17-
private static let cacheDateKey = "downloadCountsCacheDate"
18-
private static let cacheTTL: TimeInterval = 3_600 // 1 hour
19-
2018
// swiftlint:disable:next force_unwrapping
2119
private static let releasesURL = URL(string: "https://api.github.com/repos/datlechin/TablePro/releases?per_page=100")!
2220

@@ -27,8 +25,6 @@ final class DownloadCountService {
2725
config.timeoutIntervalForRequest = 15
2826
config.timeoutIntervalForResource = 30
2927
self.session = URLSession(configuration: config)
30-
31-
loadCache()
3228
}
3329

3430
// MARK: - Public
@@ -40,8 +36,7 @@ final class DownloadCountService {
4036
func fetchCounts(for manifest: RegistryManifest?) async {
4137
guard let manifest else { return }
4238

43-
if isCacheValid() {
44-
Self.logger.debug("Using cached download counts")
39+
if let lastFetchDate, Date().timeIntervalSince(lastFetchDate) < Self.cooldown {
4540
return
4641
}
4742

@@ -60,7 +55,7 @@ final class DownloadCountService {
6055
}
6156

6257
counts = totals
63-
saveCache(totals)
58+
lastFetchDate = Date()
6459
Self.logger.info("Fetched download counts for \(totals.count) plugin(s)")
6560
} catch {
6661
Self.logger.error("Failed to fetch download counts: \(error.localizedDescription)")
@@ -102,31 +97,6 @@ final class DownloadCountService {
10297
return map
10398
}
10499

105-
// MARK: - Cache
106-
107-
private func isCacheValid() -> Bool {
108-
guard let cacheDate = UserDefaults.standard.object(forKey: Self.cacheDateKey) as? Date else {
109-
return false
110-
}
111-
return Date().timeIntervalSince(cacheDate) < Self.cacheTTL
112-
}
113-
114-
private func loadCache() {
115-
guard isCacheValid(),
116-
let data = UserDefaults.standard.data(forKey: Self.cacheKey),
117-
let cached = try? JSONDecoder().decode([String: Int].self, from: data) else {
118-
counts = [:]
119-
return
120-
}
121-
counts = cached
122-
}
123-
124-
private func saveCache(_ totals: [String: Int]) {
125-
if let data = try? JSONEncoder().encode(totals) {
126-
UserDefaults.standard.set(data, forKey: Self.cacheKey)
127-
UserDefaults.standard.set(Date(), forKey: Self.cacheDateKey)
128-
}
129-
}
130100
}
131101

132102
// MARK: - GitHub API Models

TablePro/Resources/Localizable.xcstrings

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,16 @@
274274
}
275275
}
276276
},
277+
"%@ %@" : {
278+
"localizations" : {
279+
"en" : {
280+
"stringUnit" : {
281+
"state" : "new",
282+
"value" : "%1$@ %2$@"
283+
}
284+
}
285+
}
286+
},
277287
"%@ cannot be empty" : {
278288
"localizations" : {
279289
"vi" : {
@@ -7636,6 +7646,9 @@
76367646
}
76377647
}
76387648
}
7649+
},
7650+
"Filter..." : {
7651+
76397652
},
76407653
"Filters" : {
76417654
"localizations" : {
@@ -14124,6 +14137,7 @@
1412414137
}
1412514138
},
1412614139
"Search plugins..." : {
14140+
"extractionState" : "stale",
1412714141
"localizations" : {
1412814142
"vi" : {
1412914143
"stringUnit" : {
@@ -17156,7 +17170,6 @@
1715617170
}
1715717171
},
1715817172
"User" : {
17159-
"extractionState" : "stale",
1716017173
"localizations" : {
1716117174
"vi" : {
1716217175
"stringUnit" : {
@@ -17237,7 +17250,6 @@
1723717250
}
1723817251
},
1723917252
"v%@" : {
17240-
"extractionState" : "stale",
1724117253
"localizations" : {
1724217254
"vi" : {
1724317255
"stringUnit" : {

TablePro/Views/Settings/Plugins/BrowsePluginsView.swift

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,81 @@ struct BrowsePluginsView: View {
111111

112112
@ViewBuilder
113113
private func browseRow(_ plugin: RegistryPlugin) -> some View {
114-
HStack(spacing: 6) {
114+
HStack(spacing: 8) {
115115
pluginIcon(plugin.iconName ?? "puzzlepiece")
116-
.frame(width: 16)
116+
.font(.title3)
117+
.frame(width: 24, height: 24)
118+
.foregroundStyle(.secondary)
119+
120+
VStack(alignment: .leading, spacing: 2) {
121+
HStack(spacing: 4) {
122+
Text(plugin.name)
123+
.lineLimit(1)
124+
if plugin.isVerified {
125+
Image(systemName: "checkmark.seal.fill")
126+
.foregroundStyle(.blue)
127+
.font(.caption2)
128+
}
129+
}
130+
131+
HStack(spacing: 4) {
132+
Text("v\(plugin.version)")
133+
Text("·")
134+
Text(plugin.author.name)
135+
.lineLimit(1)
136+
if let count = downloadCountService.downloadCount(for: plugin.id) {
137+
Text("·")
138+
Text("\(Image(systemName: "arrow.down.circle")) \(formattedCount(count))")
139+
}
140+
}
141+
.font(.caption)
117142
.foregroundStyle(.secondary)
118-
Text(plugin.name)
119-
.lineLimit(1)
120-
if plugin.isVerified {
121-
Image(systemName: "checkmark.seal.fill")
122-
.foregroundStyle(.blue)
123-
.font(.caption2)
124143
}
144+
145+
Spacer()
146+
147+
rowStatusBadge(for: plugin)
148+
}
149+
.padding(.vertical, 2)
150+
}
151+
152+
// MARK: - Row Status Badge
153+
154+
@ViewBuilder
155+
private func rowStatusBadge(for plugin: RegistryPlugin) -> some View {
156+
if isPluginInstalled(plugin.id) {
157+
Text("Installed")
158+
.font(.caption2)
159+
.foregroundStyle(.secondary)
160+
} else if let progress = installTracker.state(for: plugin.id) {
161+
switch progress.phase {
162+
case .downloading(let fraction):
163+
ProgressView(value: fraction)
164+
.frame(width: 40)
165+
.progressViewStyle(.linear)
166+
case .installing:
167+
ProgressView()
168+
.controlSize(.mini)
169+
case .completed:
170+
Image(systemName: "checkmark.circle.fill")
171+
.foregroundStyle(.green)
172+
.font(.caption)
173+
case .failed:
174+
Button("Retry") { installPlugin(plugin) }
175+
.controlSize(.mini)
176+
}
177+
} else {
178+
Button("Install") { installPlugin(plugin) }
179+
.buttonStyle(.bordered)
180+
.controlSize(.mini)
181+
}
182+
}
183+
184+
private func formattedCount(_ count: Int) -> String {
185+
if count >= 1000 {
186+
return String(format: "%.1fk", Double(count) / 1000.0)
125187
}
188+
return "\(count)"
126189
}
127190

128191
// MARK: - Detail

TablePro/Views/Settings/Plugins/InstalledPluginsView.swift

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ struct InstalledPluginsView: View {
3232
pluginList
3333
.frame(minWidth: 200, idealWidth: 240, maxWidth: 280)
3434

35-
3635
detailPane
3736
.frame(minWidth: 340)
3837
}
@@ -146,14 +145,35 @@ struct InstalledPluginsView: View {
146145

147146
@ViewBuilder
148147
private func pluginRow(_ plugin: PluginEntry) -> some View {
149-
HStack(spacing: 6) {
148+
HStack(spacing: 8) {
150149
pluginIcon(plugin.iconName)
151-
.frame(width: 16)
152-
.foregroundStyle(plugin.isEnabled ? .primary : .tertiary)
153-
Text(plugin.name)
154-
.lineLimit(1)
155-
.foregroundStyle(plugin.isEnabled ? .primary : .secondary)
150+
.font(.title3)
151+
.frame(width: 24, height: 24)
152+
.foregroundStyle(plugin.isEnabled ? .secondary : .tertiary)
153+
154+
VStack(alignment: .leading, spacing: 2) {
155+
Text(plugin.name)
156+
.lineLimit(1)
157+
.foregroundStyle(plugin.isEnabled ? .primary : .secondary)
158+
159+
HStack(spacing: 4) {
160+
Text("v\(plugin.version)")
161+
if let capability = plugin.capabilities.first {
162+
Text("·")
163+
Text(capability.displayName)
164+
}
165+
}
166+
.font(.caption)
167+
.foregroundStyle(.secondary)
168+
}
169+
170+
Spacer()
171+
172+
Text(plugin.source == .builtIn ? String(localized: "Built-in") : String(localized: "User"))
173+
.font(.caption2)
174+
.foregroundStyle(.secondary)
156175
}
176+
.padding(.vertical, 2)
157177
}
158178

159179
@ViewBuilder

0 commit comments

Comments
 (0)