From 731add8c807f472a774fd4bb637fe9c081d6b9fd Mon Sep 17 00:00:00 2001 From: Jordan Koch Date: Thu, 18 Jul 2019 10:49:28 -0400 Subject: [PATCH] initial --- .gitignore | 13 + Arxius-Bridging-Header.h | 1 + Arxius.swift | 224 +++++++++++++ Arxius.xcodeproj/project.pbxproj | 305 ++++++++++++++++++ .../xcshareddata/xcschemes/Arxius.xcscheme | 80 +++++ Extensions.swift | 37 +++ Info.plist | 22 ++ README.md | 21 ++ 8 files changed, 703 insertions(+) create mode 100755 .gitignore create mode 100755 Arxius-Bridging-Header.h create mode 100755 Arxius.swift create mode 100755 Arxius.xcodeproj/project.pbxproj create mode 100644 Arxius.xcodeproj/xcshareddata/xcschemes/Arxius.xcscheme create mode 100755 Extensions.swift create mode 100755 Info.plist create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..217a369 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*~ +*~.* +.DS_Store +.LSOverride +*.mode1v3 +*.mode2v3 +*.pbxuser +*.perspectivev3 +*.pyc +.tmp +build/ +xcuserdata +*.xcworkspace diff --git a/Arxius-Bridging-Header.h b/Arxius-Bridging-Header.h new file mode 100755 index 0000000..7013c1c --- /dev/null +++ b/Arxius-Bridging-Header.h @@ -0,0 +1 @@ +#import "Textual.h" diff --git a/Arxius.swift b/Arxius.swift new file mode 100755 index 0000000..38a7959 --- /dev/null +++ b/Arxius.swift @@ -0,0 +1,224 @@ +import Foundation + +@objc +class Plugin: NSObject, THOPluginProtocol { + var apikey = "" + var theClient:IRCClient? = nil + var currentAlbum = "" + var uploadQueue:[URL] = [] + var isUploading = false + + var task:URLSessionDataTask? + + @objc + let subscribedUserInputCommands = ["arxiuskey", "upload", "cancel"] + + @objc func pluginLoadedIntoMemory() { + if (apikey.count < 1) { + if let key = TPCPreferencesUserDefaults.shared().value(forKey: "arxiusAPIKey") { + apikey = key as! String + } + } + } + + @objc + func userInputCommandInvoked(on client: IRCClient, command commandString: String, messageString: String) { + theClient = client + + performBlock(onMainThread: { + switch commandString { + case "ARXIUSKEY": + if (messageString.count == 32) { + apikey = messageString + TPCPreferencesUserDefaults.shared().setValue(apikey, forKey: "arxiusAPIKey") + debug("Your API key is set") + } + else if (messageString.count == 0 && apikey.count > 0) { + apikey = "" + TPCPreferencesUserDefaults.shared().removeObject(forKey: "arxiusAPIKey") + debug("Your API key is unset") + } + else if (messageString.count == 0) { + debug("Enter your API key like this: /arxiuskey abcd1234abcd1234abcd1234abcd1234") + } + else { + debug("That's not a valid API key") + } + + case "UPLOAD": + let dialog = NSOpenPanel(); + + dialog.title = "Select files to upload"; + dialog.showsResizeIndicator = true; + dialog.showsHiddenFiles = false; + dialog.canChooseDirectories = false; + dialog.canCreateDirectories = true; + dialog.allowsMultipleSelection = true; + + if (dialog.runModal() == NSApplication.ModalResponse.OK) { + let files = dialog.urls + + if (files.count > 0) { + for file in files { + let path = file.absoluteURL + uploadQueue.append(path) + } + + makeAlbumIfNeeded() + } + } + + case "CANCEL": + task?.cancel() + uploadQueue = [] + debug("Upload cancelled") + + break + + default: + break + } + }) + } + + func debug(_ message: Any) { + self.theClient!.printDebugInformation("\u{02}[Arxius]\u{02} \(message)", in: self.theClient!.lastSelectedChannel!) + } + + func startUploading() { + uploadFiles() + } + + func uploadFiles() { + if (uploadQueue.count > 0) { + upload() + } + else { + currentAlbum = "" + } + } + + func upload() { + let file = uploadQueue[0] + uploadQueue.remove(at: 0) + + let url = URL(string: "https://arxius.io/")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + var parameters: [String:String] = [:] + + if (apikey.count > 0) { + parameters["apikey"] = apikey + } + + if (currentAlbum.count > 0) { + parameters["album"] = currentAlbum + } + + var last = false + if (uploadQueue.count < 1) { + last = true + parameters["last"] = "true" + } + + let boundary = "Boundary-\(UUID().uuidString)" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + do { + let fileData = try Data(contentsOf: file) + + request.httpBody = createBody(parameters: parameters, boundary: boundary, data: fileData, mimeType: "application/octet-stream", filename: file.lastPathComponent) + + task = URLSession.shared.dataTask(with: request) { data, response, error in + guard let data = data, let response = response as? HTTPURLResponse, error == nil else { + return + } + + guard (200 ... 299) ~= response.statusCode else { + return + } + + if (last) { + if let link = String(data: data, encoding: .utf8) { + self.debug("Upload complete: \(link)") + } + } + } + task!.resume() + + debug("Uploading: \(file.path)") + + uploadFiles() + } + catch {} + } + + func makeAlbumIfNeeded() { + if (uploadQueue.count > 1) { + let url = URL(string: "https://arxius.io/api/create/album")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + var parameters: [String:String] = [:] + + if (apikey.count > 0) { + parameters["apikey"] = apikey + } + + request.httpBody = parameters.percentEscaped().data(using: .utf8) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + guard let data = data, let response = response as? HTTPURLResponse, error == nil else { + return + } + + guard (200 ... 299) ~= response.statusCode else { + return + } + + do { + let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! [String: Any] + + if (json.index(forKey: "album") != nil) { + let album = json["album"] as! String + self.currentAlbum = album + self.debug("Created album: \(album)") + + self.startUploading() + } + else { + self.debug("Error creating album") + } + } catch {} + } + task.resume() + } + else { + self.startUploading() + } + } + + func createBody(parameters: [String: String], + boundary: String, + data: Data, + mimeType: String, + filename: String) -> Data { + let body = NSMutableData() + + let boundaryPrefix = "--\(boundary)\r\n" + + for (key, value) in parameters { + body.appendString(boundaryPrefix) + body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") + body.appendString("\(value)\r\n") + } + + body.appendString(boundaryPrefix) + body.appendString("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n") + body.appendString("Content-Type: \(mimeType)\r\n\r\n") + body.append(data) + body.appendString("\r\n") + body.appendString("--".appending(boundary.appending("--"))) + + return body as Data + } +} diff --git a/Arxius.xcodeproj/project.pbxproj b/Arxius.xcodeproj/project.pbxproj new file mode 100755 index 0000000..c2979bc --- /dev/null +++ b/Arxius.xcodeproj/project.pbxproj @@ -0,0 +1,305 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 07E88365214F2BAE001982D7 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E88364214F2BAE001982D7 /* Extensions.swift */; }; + 4C38AD881946BB2C00B4A7AB /* Arxius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C38AD871946BB2C00B4A7AB /* Arxius.swift */; }; + 4C51BE9412D0471600E79CEB /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C51BE9312D0471600E79CEB /* Cocoa.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 07E88364214F2BAE001982D7 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 4C38AD861946BB2B00B4A7AB /* Arxius-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Arxius-Bridging-Header.h"; sourceTree = ""; }; + 4C38AD871946BB2C00B4A7AB /* Arxius.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Arxius.swift; sourceTree = ""; }; + 4C51BE9312D0471600E79CEB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 8D576316048677EA00EA77CD /* Arxius.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Arxius.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + 8D576317048677EA00EA77CD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D576313048677EA00EA77CD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C51BE9412D0471600E79CEB /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 089C166AFE841209C02AAC07 /* PreferencePaneExample */ = { + isa = PBXGroup; + children = ( + 08FB77AFFE84173DC02AAC07 /* Source */, + 089C167CFE841241C02AAC07 /* Resources */, + 089C1671FE841209C02AAC07 /* Frameworks */, + 19C28FB6FE9D52B211CA2CBB /* Products */, + ); + name = PreferencePaneExample; + sourceTree = ""; + }; + 089C1671FE841209C02AAC07 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4C51BE9312D0471600E79CEB /* Cocoa.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 089C167CFE841241C02AAC07 /* Resources */ = { + isa = PBXGroup; + children = ( + 4C51BE9912D0472300E79CEB /* Documents */, + ); + name = Resources; + sourceTree = ""; + }; + 08FB77AFFE84173DC02AAC07 /* Source */ = { + isa = PBXGroup; + children = ( + 07E88364214F2BAE001982D7 /* Extensions.swift */, + 4C38AD871946BB2C00B4A7AB /* Arxius.swift */, + 4C38AD861946BB2B00B4A7AB /* Arxius-Bridging-Header.h */, + ); + name = Source; + sourceTree = ""; + }; + 19C28FB6FE9D52B211CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8D576316048677EA00EA77CD /* Arxius.bundle */, + ); + name = Products; + sourceTree = ""; + }; + 4C51BE9912D0472300E79CEB /* Documents */ = { + isa = PBXGroup; + children = ( + 8D576317048677EA00EA77CD /* Info.plist */, + ); + name = Documents; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D57630D048677EA00EA77CD /* Arxius */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Arxius" */; + buildPhases = ( + 8D57630F048677EA00EA77CD /* Resources */, + 8D576311048677EA00EA77CD /* Sources */, + 8D576313048677EA00EA77CD /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Arxius; + productInstallPath = "$(HOME)/Library/Bundles"; + productName = PreferencePaneExample; + productReference = 8D576316048677EA00EA77CD /* Arxius.bundle */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 089C1669FE841209C02AAC07 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 1000; + TargetAttributes = { + 8D57630D048677EA00EA77CD = { + LastSwiftMigration = 1000; + }; + }; + }; + buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Arxius" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + Base, + ); + mainGroup = 089C166AFE841209C02AAC07 /* PreferencePaneExample */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D57630D048677EA00EA77CD /* Arxius */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D57630F048677EA00EA77CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D576311048677EA00EA77CD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C38AD881946BB2C00B4A7AB /* Arxius.swift in Sources */, + 07E88365214F2BAE001982D7 /* Extensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1DEB911B08733D790010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = /Applications/Textual.app/Contents/MacOS/Textual; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Frameworks/**\""; + HEADER_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Headers/**\""; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = "com.jordankoch.textual-arxius"; + PRODUCT_NAME = Arxius; + SWIFT_OBJC_BRIDGING_HEADER = "Arxius-Bridging-Header.h"; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + 1DEB911C08733D790010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = /Applications/Textual.app/Contents/MacOS/Textual; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Frameworks/**\""; + HEADER_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Headers/**\""; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = "com.jordankoch.textual-arxius"; + PRODUCT_NAME = Arxius; + SWIFT_OBJC_BRIDGING_HEADER = "Arxius-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; + 1DEB911F08733D790010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 1DEB912008733D790010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Arxius" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB911B08733D790010E9CD /* Debug */, + 1DEB911C08733D790010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Arxius" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB911F08733D790010E9CD /* Debug */, + 1DEB912008733D790010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 089C1669FE841209C02AAC07 /* Project object */; +} diff --git a/Arxius.xcodeproj/xcshareddata/xcschemes/Arxius.xcscheme b/Arxius.xcodeproj/xcshareddata/xcschemes/Arxius.xcscheme new file mode 100644 index 0000000..42f94d4 --- /dev/null +++ b/Arxius.xcodeproj/xcshareddata/xcschemes/Arxius.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Extensions.swift b/Extensions.swift new file mode 100755 index 0000000..237c20e --- /dev/null +++ b/Extensions.swift @@ -0,0 +1,37 @@ +// +// Extensions.swift +// Arxius +// +// Created by Jordan Koch on 9/16/18. +// + +import Foundation + +extension Dictionary { + func percentEscaped() -> String { + return map { (key, value) in + let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" + let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "" + return escapedKey + "=" + escapedValue + } + .joined(separator: "&") + } +} + +extension CharacterSet { + static let urlQueryValueAllowed: CharacterSet = { + let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let subDelimitersToEncode = "!$&'()*+,;=" + + var allowed = CharacterSet.urlQueryAllowed + allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") + return allowed + }() +} + +extension NSMutableData { + func appendString(_ string: String) { + let data = string.data(using: String.Encoding.utf8, allowLossyConversion: false) + append(data!) + } +} diff --git a/Info.plist b/Info.plist new file mode 100755 index 0000000..db7db66 --- /dev/null +++ b/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleVersion + 1.0.1 + MinimumTextualVersion + 6.0.0 + NSPrincipalClass + $(PRODUCT_NAME).Plugin + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..18b2a87 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +## Download + +https://arxius.io/textual + +## Install + +Unzip & double click `Arxius.bundle` + +## Usage + +Upload a file + +`/upload` + +Set your API key + +`/arxiuskey abcd1234abcd1234abcd1234abcd1234` + +Unset your API key + +`/arxiuskey` \ No newline at end of file