add swiftlint, add more german translation, add fetchExploreResults to JSController, clenaup skeleton cells, ...

This commit is contained in:
Dominic Drees 2025-04-27 11:51:13 +02:00
parent ca15d27456
commit aed5630c4c
9 changed files with 493 additions and 51 deletions

120
.swiftlint.yml Normal file
View file

@ -0,0 +1,120 @@
# Directory and file filters
included:
- Plugins
- Source
- Tests
- Package.swift
excluded:
- Tests/BuiltInRulesTests/Resources
- Tests/FrameworkTests/Resources
# Enabled/disabled rules
analyzer_rules:
- unused_declaration
- unused_import
opt_in_rules:
- all
disabled_rules:
- anonymous_argument_in_multiline_closure
- async_without_await
- conditional_returns_on_newline
- contrasted_opening_brace
- convenience_type
- discouraged_optional_collection
- explicit_acl
- explicit_enum_raw_value
- explicit_top_level_acl
- explicit_type_interface
- file_types_order
- force_unwrapping
- function_default_parameter_at_end
- indentation_width
- missing_docs
- multiline_arguments
- multiline_arguments_brackets
- multiline_function_chains
- multiline_parameters_brackets
- no_extension_access_modifier
- no_grouping_extension
- no_magic_numbers
- one_declaration_per_file
- prefer_key_path # Re-enable once we are on Swift 6.
- prefer_nimble
- prefixed_toplevel_constant
- required_deinit
- sorted_enum_cases
- strict_fileprivate
- switch_case_on_newline
- todo
- trailing_closure
- type_contents_order
- vertical_whitespace_between_cases
# Configurations
attributes:
always_on_line_above:
- "@ConfigurationElement"
- "@OptionGroup"
- "@RuleConfigurationDescriptionBuilder"
balanced_xctest_lifecycle: &unit_test_configuration
test_parent_classes:
- SwiftLintTestCase
- XCTestCase
closure_body_length:
warning: 50
error: 100
empty_xctest_method: *unit_test_configuration
file_name:
excluded:
- Exports.swift
- GeneratedTests.swift
- Macros.swift
- Reporters+Register.swift
- Rules+Register.swift
- Rules+Template.swift
- RuleConfigurationMacros.swift
- SwiftSyntax+SwiftLint.swift
- TestHelpers.swift
final_test_case: *unit_test_configuration
function_body_length: 60
identifier_name:
excluded:
- id
large_tuple: 3
number_separator:
minimum_length: 5
redundant_type_annotation:
consider_default_literal_types_redundant: true
single_test_class: *unit_test_configuration
trailing_comma:
mandatory_comma: true
type_body_length: 400
unneeded_override:
affect_initializers: true
unused_import:
always_keep_imports:
- SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes
- SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused
# Custom rules
custom_rules:
rule_id:
included: Source/SwiftLintBuiltInRules/Rules/.+/\w+\.swift
name: Rule ID
message: Rule IDs must be all lowercase, snake case and not end with `rule`
regex: ^\s+identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*")
severity: error
fatal_error:
name: Fatal Error
excluded: "Tests/*"
message: Prefer using `queuedFatalError` over `fatalError` to avoid leaking compiler host machine paths.
regex: \bfatalError\b
match_kinds:
- identifier
severity: error
rule_test_function:
included: Tests/SwiftLintFrameworkTests/RulesTests.swift
name: Rule Test Function
message: Rule Test Function mustn't end with `rule`
regex: func\s*test\w+(r|R)ule\(\)
severity: error

View file

@ -1109,6 +1109,16 @@
}
}
},
"No Content Available" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kein Inhalt verfügbar"
}
}
}
},
"No data received" : {
"localizations" : {
"de" : {
@ -1689,6 +1699,16 @@
}
}
},
"Try updating the Module" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Versuche das Modul zu updaten"
}
}
}
},
"Two Finger Hold for Pause" : {
"localizations" : {
"de" : {

View file

@ -4,7 +4,7 @@
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.de.devsforge.sulfur.fork</string>
<string>iCloud.de.devsforge.sulfur</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
@ -12,7 +12,7 @@
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.de.devsforge.sulfur.fork</string>
<string>iCloud.de.devsforge.sulfur</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>

View file

@ -0,0 +1,133 @@
//
// JSController-Search.swift
// Sulfur
//
// Created by Dominic on 24.04.25.
//
import JavaScriptCore
// TODO: implement and test
extension JSController {
func fetchExploreResults(module: ScrapingModule, completion: @escaping ([ExploreItem]) -> Void) {
/*let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
guard let url = URL(string: searchUrl) else {
completion([])
return
}
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error {
Logger.shared.log("Network error: \(error)",type: "Error")
DispatchQueue.main.async { completion([]) }
return
}
guard let data = data, let html = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to decode HTML",type: "Error")
DispatchQueue.main.async { completion([]) }
return
}
Logger.shared.log(html,type: "HTMLStrings")
if let parseFunction = self.context.objectForKeyedSubscript("searchResults"),
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
let resultItems = results.map { item in
SearchItem(
title: item["title"] ?? "",
imageUrl: item["image"] ?? "",
href: item["href"] ?? ""
)
}
DispatchQueue.main.async {
completion(resultItems)
}
} else {
Logger.shared.log("Failed to parse results",type: "Error")
DispatchQueue.main.async { completion([]) }
}
}.resume()
*/
}
func fetchJsExploreResults(module: ScrapingModule, completion: @escaping ([ExploreItem]) -> Void) {
/*
if let exception = context.exception {
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
completion([])
return
}
guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else {
Logger.shared.log("No JavaScript function searchResults found",type: "Error")
completion([])
return
}
let promiseValue = searchResultsFunction.call(withArguments: [keyword])
guard let promise = promiseValue else {
Logger.shared.log("searchResults did not return a Promise",type: "Error")
completion([])
return
}
let thenBlock: @convention(block) (JSValue) -> Void = { result in
Logger.shared.log(result.toString(),type: "HTMLStrings")
if let jsonString = result.toString(),
let data = jsonString.data(using: .utf8) {
do {
if let array = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] {
let resultItems = array.compactMap { item -> SearchItem? in
guard let title = item["title"] as? String,
let imageUrl = item["image"] as? String,
let href = item["href"] as? String else {
Logger.shared.log("Missing or invalid data in search result item: \(item)", type: "Error")
return nil
}
return SearchItem(title: title, imageUrl: imageUrl, href: href)
}
DispatchQueue.main.async {
completion(resultItems)
}
} else {
Logger.shared.log("Failed to parse JSON",type: "Error")
DispatchQueue.main.async {
completion([])
}
}
} catch {
Logger.shared.log("JSON parsing error: \(error)",type: "Error")
DispatchQueue.main.async {
completion([])
}
}
} else {
Logger.shared.log("Result is not a string",type: "Error")
DispatchQueue.main.async {
completion([])
}
}
}
let catchBlock: @convention(block) (JSValue) -> Void = { error in
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))",type: "Error")
DispatchQueue.main.async {
completion([])
}
}
let thenFunction = JSValue(object: thenBlock, in: context)
let catchFunction = JSValue(object: catchBlock, in: context)
promise.invokeMethod("then", withArguments: [thenFunction as Any])
promise.invokeMethod("catch", withArguments: [catchFunction as Any])
*/
}
}

View file

@ -7,38 +7,30 @@
import SwiftUI
struct HomeSkeletonCell: View {
enum SkeletonCellType {
// unused !? ( legacy code from HomeSkeletonCell )
case home
case search
case explore
}
struct SkeletonCell: View {
let type: SkeletonCellType
let cellWidth: CGFloat
var body: some View {
VStack {
VStack(alignment: type == .home ? .center : .leading, spacing: type == .home ? 0 : 8) {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: cellWidth, height: cellWidth * 1.5)
.cornerRadius(10)
.shimmering()
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray.opacity(0.3))
.frame(width: cellWidth, height: 20)
.padding(.top, 4)
.shimmering()
}
}
}
struct SearchSkeletonCell: View {
let cellWidth: CGFloat
var body: some View {
VStack(alignment: .leading, spacing: 8) {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: cellWidth, height: cellWidth * 1.5)
.shimmering()
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray.opacity(0.3))
.frame(width: cellWidth, height: 20)
.padding(.top, type == .home ? 4 : 0)
.shimmering()
}
}

View file

@ -1,35 +1,52 @@
//
// LibraryView.swift
// Sora
// Sulfur
//
// Created by Francesco on 05/01/25.
// Created by Dominic on 24.04.25.
//
import SwiftUI
import Kingfisher
struct ExploreView: View {
@EnvironmentObject private var moduleManager: ModuleManager
@EnvironmentObject private var profileStore: ProfileStore
struct ExploreItem: Identifiable {
let id = UUID()
let title: String
let imageUrl: String
let href: String
}
@AppStorage("selectedModuleId") private var selectedModuleId: String?
struct ExploreView: View {
@AppStorage("hideEmptySections") private var hideEmptySections: Bool?
@AppStorage("selectedModuleId") private var selectedModuleId: String?
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
@StateObject private var jsController = JSController()
@EnvironmentObject private var moduleManager: ModuleManager
@EnvironmentObject private var profileStore: ProfileStore
@Environment(\.verticalSizeClass) var verticalSizeClass
@State private var exploreItems: [ExploreItem] = []
@State private var selectedExploreItem: ExploreItem?
@State private var hasNoResults = false
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
@State private var isModuleSelectorPresented = false
@State private var showProfileSettings = false
@State private var isLoading = false
private var selectedModule: ScrapingModule? {
guard let id = selectedModuleId else { return nil }
return moduleManager.modules.first { $0.id.uuidString == id }
}
private let columns = [
GridItem(.adaptive(minimum: 150), spacing: 12)
private var loadingMessages: [String] = [
"Exploring the depths...",
"Looking for results...",
"Fetching data...",
"Please wait...",
"Almost there..."
]
private var columnsCount: Int {
if UIDevice.current.userInterfaceIdiom == .pad {
let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height
@ -38,7 +55,7 @@ struct ExploreView: View {
return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait
}
}
private var cellWidth: CGFloat {
let keyWindow = UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow }) }
@ -49,15 +66,83 @@ struct ExploreView: View {
let availableWidth = safeWidth - totalSpacing
return availableWidth / CGFloat(columnsCount)
}
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
//TODO: add explore content views
let columnsCount = determineColumns()
VStack(spacing: 0) {
if !(hideEmptySections ?? false) && selectedModule == nil {
VStack(spacing: 8) {
Image(systemName: "questionmark.app")
.font(.largeTitle)
.foregroundColor(.secondary)
Text("No Module Selected")
.font(.headline)
Text("Please select a module from settings")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.background(Color(.systemBackground))
}
if isLoading {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) {
ForEach(0..<columnsCount*4, id: \.self) { _ in
SkeletonCell(type: .explore, cellWidth: cellWidth)
}
}
.padding(.top)
.padding()
} else if hasNoResults {
VStack(spacing: 8) {
Image(systemName: "star")
.font(.largeTitle)
.foregroundColor(.secondary)
Text("No Content Available")
.font(.headline)
Text("Try updating the Module")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.padding(.top)
} else {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) {
ForEach(exploreItems) { item in
NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule!)) {
VStack {
KFImage(URL(string: item.imageUrl))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: cellWidth * 3 / 2)
.frame(maxWidth: cellWidth)
.cornerRadius(10)
.clipped()
Text(item.title)
.font(.subheadline)
.foregroundColor(Color.primary)
.padding([.leading, .bottom], 8)
.lineLimit(1)
}
}
}
.onAppear {
updateOrientation()
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
updateOrientation()
}
}
.padding(.top)
.padding()
}
}
.padding(.vertical, 20)
NavigationLink(
destination: SettingsViewProfile(),
isActive: $showProfileSettings,
@ -66,6 +151,7 @@ struct ExploreView: View {
.hidden()
}
.navigationTitle("Explore")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Menu {
@ -100,6 +186,7 @@ struct ExploreView: View {
)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
ForEach(getModuleLanguageGroups(), id: \.self) { language in
@ -143,22 +230,65 @@ struct ExploreView: View {
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.onAppear {
updateOrientation()
//TODO: fetch explore content
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
updateOrientation()
}
.navigationViewStyle(StackNavigationViewStyle())
.onChange(of: selectedModuleId) { _ in
fetchData()
}
}
private func fetchData() {
Logger.shared.log("Fetching Explore Data", type: "General")
guard let module = selectedModule else {
exploreItems = []
hasNoResults = false
return
}
isLoading = true
hasNoResults = false
exploreItems = []
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
Task {
do {
let jsContent = try moduleManager.getModuleContent(module)
jsController.loadScript(jsContent)
if module.metadata.asyncJS == true {
jsController.fetchJsExploreResults(module: module) { items in
exploreItems = items
hasNoResults = items.isEmpty
isLoading = false
}
} else {
jsController.fetchExploreResults(module: module) { items in
exploreItems = items
hasNoResults = items.isEmpty
isLoading = false
}
}
} catch {
Logger.shared.log("Error loading module: \(error)", type: "Error")
isLoading = false
hasNoResults = true
}
}
}
}
private func updateOrientation() {
DispatchQueue.main.async {
isLandscape = UIDevice.current.orientation.isLandscape
}
}
private func determineColumns() -> Int {
if UIDevice.current.userInterfaceIdiom == .pad {
return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait
} else {
return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait
}
}
private func cleanLanguageName(_ language: String?) -> String {
guard let language = language else { return "Unknown" }

View file

@ -111,7 +111,7 @@ struct SearchView: View {
if isSearching {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) {
ForEach(0..<columnsCount*4, id: \.self) { _ in
SearchSkeletonCell(cellWidth: cellWidth)
SkeletonCell(type: .search, cellWidth: cellWidth)
}
}
.padding(.top)

View file

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
120D3C722DBA40AB0093D596 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 120D3C712DBA40A40093D596 /* .swiftlint.yml */; };
120D3C732DBA40AB0093D596 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 120D3C712DBA40A40093D596 /* .swiftlint.yml */; };
126C428D2DB99627006BC27D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 126C428C2DB99627006BC27D /* Localizable.xcstrings */; };
126C428E2DB99627006BC27D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 126C428C2DB99627006BC27D /* Localizable.xcstrings */; };
132E351D2D959DDB0007800E /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351C2D959DDB0007800E /* Drops */; };
@ -17,6 +19,7 @@
/* Begin PBXFileReference section */
120764652DB6F6E0003621E9 /* SulfurTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SulfurTV.app; sourceTree = BUILT_PRODUCTS_DIR; };
120D3C712DBA40A40093D596 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
126C428A2DB9921C006BC27D /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
126C428C2DB99627006BC27D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -77,6 +80,7 @@
133D7C612D2BE2500075467E = {
isa = PBXGroup;
children = (
120D3C712DBA40A40093D596 /* .swiftlint.yml */,
126C428C2DB99627006BC27D /* Localizable.xcstrings */,
126C42F62DB9AA97006BC27D /* Sora */,
120764662DB6F6E0003621E9 /* SulfurTV */,
@ -108,6 +112,7 @@
buildRules = (
);
dependencies = (
120D3C702DBA3DF90093D596 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
120764662DB6F6E0003621E9 /* SulfurTV */,
@ -130,6 +135,7 @@
buildRules = (
);
dependencies = (
120D3C6E2DBA3DF30093D596 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
126C42F62DB9AA97006BC27D /* Sora */,
@ -178,6 +184,7 @@
132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */,
132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */,
13B77E172DA44F8300126FDF /* XCRemoteSwiftPackageReference "MarqueeLabel" */,
120D3C6C2DBA3D790093D596 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
);
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
projectDirPath = "";
@ -194,6 +201,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
120D3C722DBA40AB0093D596 /* .swiftlint.yml in Resources */,
126C428E2DB99627006BC27D /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -202,6 +210,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
120D3C732DBA40AB0093D596 /* .swiftlint.yml in Resources */,
126C428D2DB99627006BC27D /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -225,6 +234,17 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
120D3C6E2DBA3DF30093D596 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 120D3C6D2DBA3DF30093D596 /* SwiftLintBuildToolPlugin */;
};
120D3C702DBA3DF90093D596 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 120D3C6F2DBA3DF90093D596 /* SwiftLintBuildToolPlugin */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
1207646D2DB6F6E1003621E9 /* Debug */ = {
isa = XCBuildConfiguration;
@ -555,6 +575,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
120D3C6C2DBA3D790093D596 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins";
requirement = {
branch = main;
kind = branch;
};
};
132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/omaralbeik/Drops.git";
@ -590,6 +618,16 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
120D3C6D2DBA3DF30093D596 /* SwiftLintBuildToolPlugin */ = {
isa = XCSwiftPackageProductDependency;
package = 120D3C6C2DBA3D790093D596 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
productName = "plugin:SwiftLintBuildToolPlugin";
};
120D3C6F2DBA3DF90093D596 /* SwiftLintBuildToolPlugin */ = {
isa = XCSwiftPackageProductDependency;
package = 120D3C6C2DBA3D790093D596 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
productName = "plugin:SwiftLintBuildToolPlugin";
};
132E351C2D959DDB0007800E /* Drops */ = {
isa = XCSwiftPackageProductDependency;
package = 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */;

View file

@ -1,5 +1,5 @@
{
"originHash" : "e772caa8d6a8793d24bf04e3d77695cd5ac695f3605d2b657e40115caedf8863",
"originHash" : "c4909124df3eb22bfcc539fb1f3936eb79c309605d2f34c5b02efa1fe3f48447",
"pins" : [
{
"identity" : "drops",
@ -45,6 +45,15 @@
"branch" : "master",
"revision" : "18e4787f4dc1c26d2d581c4bc9aeae34686eeeae"
}
},
{
"identity" : "swiftlintplugins",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
"state" : {
"branch" : "main",
"revision" : "8545ddf4de043e6f2051c5cf204f39ef778ebf6b"
}
}
],
"version" : 3