commit 10256585185fc1f3878d50ad5062f107fe2e5fba Author: robonen Date: Wed Apr 29 20:59:39 2026 +0700 Initial Commit diff --git a/crypto/Assets.xcassets/AccentColor.colorset/Contents.json b/crypto/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/crypto/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/crypto/Assets.xcassets/AppIcon.appiconset/Contents.json b/crypto/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/crypto/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/crypto/Assets.xcassets/Contents.json b/crypto/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/crypto/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/crypto/Assets.xcassets/WidgetBackground.colorset/Contents.json b/crypto/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/crypto/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/crypto/CryptoWidget.swift b/crypto/CryptoWidget.swift new file mode 100644 index 0000000..2bc5b52 --- /dev/null +++ b/crypto/CryptoWidget.swift @@ -0,0 +1,373 @@ +import WidgetKit +import SwiftUI + +// MARK: - Models + +struct SymbolThumb: Codable, Identifiable, Sendable { + var id: String { symbol } + let symbol: String + let open: Double + let high: Double + let low: Double + let close: Double + let chg: Double + let change: Double + let volume: Double + let turnover: Double + let quoteCurrencyName: String + let baseCurrency: String + let quoteCurrency: String + + var isPositive: Bool { chg >= 0 } + + var changePercent: String { + String(format: "%+.2f%%", chg * 100) + } + + var priceFormatted: String { + let fmt = NumberFormatter() + fmt.numberStyle = .decimal + fmt.minimumFractionDigits = 2 + if close >= 1000 { + fmt.maximumFractionDigits = 2 + } else if close >= 1 { + fmt.maximumFractionDigits = 4 + } else { + fmt.maximumFractionDigits = 6 + } + return fmt.string(from: NSNumber(value: close)) ?? String(format: "%.2f", close) + } +} + +// MARK: - API + +struct RapiraAPI { + static let url = URL(string: "https://api.rapira.net/market/symbol-thumb")! + + static func fetch() async throws -> [SymbolThumb] { + let (data, _) = try await URLSession.shared.data(from: url) + return try JSONDecoder().decode([SymbolThumb].self, from: data) + } + + static func fetchIcon(for currency: String) async -> Data? { + guard let url = URL(string: "https://cdn.rapira.net/media/crypto/\(currency).svg") else { return nil } + return try? await URLSession.shared.data(from: url).0 + } + + static func fetchIcons(for currencies: [String]) async -> [String: Data] { + await withTaskGroup(of: (String, Data?).self) { group in + for cur in Set(currencies) { + group.addTask { (cur, await fetchIcon(for: cur)) } + } + var result: [String: Data] = [:] + for await (cur, data) in group { + if let data { result[cur] = data } + } + return result + } + } +} + +// MARK: - Entry + +struct CryptoEntry: TimelineEntry { + let date: Date + let symbols: [SymbolThumb] + let selectedPairs: [String] + let icons: [String: Data] + + var filtered: [SymbolThumb] { + let order = Dictionary(uniqueKeysWithValues: selectedPairs.enumerated().map { ($1, $0) }) + return symbols + .filter { selectedPairs.contains($0.symbol) } + .sorted { (order[$0.symbol] ?? 99) < (order[$1.symbol] ?? 99) } + } +} + +// MARK: - Provider + +struct CryptoProvider: TimelineProvider { + func placeholder(in context: Context) -> CryptoEntry { + CryptoEntry(date: .now, symbols: Self.mock, selectedPairs: ["BTC/USDT", "ETH/USDT", "SOL/USDT"], icons: [:]) + } + + func getSnapshot(in context: Context, completion: @escaping (CryptoEntry) -> Void) { + Task { + let symbols = (try? await RapiraAPI.fetch()) ?? Self.mock + let pairs = loadPairs() + let filtered = symbols.filter { pairs.contains($0.symbol) } + let icons = await RapiraAPI.fetchIcons(for: filtered.map(\.quoteCurrency)) + completion(CryptoEntry(date: .now, symbols: symbols, selectedPairs: pairs, icons: icons)) + } + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + Task { + let symbols = (try? await RapiraAPI.fetch()) ?? Self.mock + let pairs = loadPairs() + let filtered = symbols.filter { pairs.contains($0.symbol) } + let icons = await RapiraAPI.fetchIcons(for: filtered.map(\.quoteCurrency)) + let entry = CryptoEntry(date: .now, symbols: symbols, selectedPairs: pairs, icons: icons) + let next = Calendar.current.date(byAdding: .minute, value: 5, to: .now)! + completion(Timeline(entries: [entry], policy: .after(next))) + } + } + + private func loadPairs() -> [String] { + UserDefaults(suiteName: "group.robonen.rapira-market-widget.crypto")? + .stringArray(forKey: "selectedPairs") ?? ["BTC/USDT", "ETH/USDT", "SOL/USDT"] + } + + static let mock: [SymbolThumb] = [ + SymbolThumb(symbol: "BTC/USDT", open: 71000, high: 71381.5, low: 68894.5, + close: 70324, chg: 0.0234, change: 1676, volume: 223.9, + turnover: 41930552, quoteCurrencyName: "Bitcoin", + baseCurrency: "USDT", quoteCurrency: "BTC"), + SymbolThumb(symbol: "ETH/USDT", open: 2152.9, high: 2177.4, low: 2105, + close: 2143.5, chg: -0.00446, change: -9.4, volume: 27226, + turnover: 119347435, quoteCurrencyName: "Ethereum", + baseCurrency: "USDT", quoteCurrency: "ETH"), + SymbolThumb(symbol: "SOL/USDT", open: 91.77, high: 92.29, low: 88.3, + close: 90.08, chg: -0.01913, change: -1.69, volume: 349968, + turnover: 71024390, quoteCurrencyName: "Solana", + baseCurrency: "USDT", quoteCurrency: "SOL"), + SymbolThumb(symbol: "TON/USDT", open: 1.31, high: 1.35, low: 1.29, + close: 1.3382, chg: 0.0223, change: 0.0282, volume: 5200000, + turnover: 6958640, quoteCurrencyName: "Toncoin", + baseCurrency: "USDT", quoteCurrency: "TON"), + ] +} + +// MARK: - Theme + +private enum Theme { + static let bgTop = Color(red: 0.08, green: 0.12, blue: 0.32) + static let bgBottom = Color(red: 0.03, green: 0.05, blue: 0.18) + static let label = Color(red: 0.45, green: 0.65, blue: 1.0) + static let positive = Color(red: 0.05, green: 0.82, blue: 0.42) + static let negative = Color(red: 1.0, green: 0.30, blue: 0.30) + static let divider = Color.white.opacity(0.08) + + static var bgGradient: LinearGradient { + LinearGradient( + colors: [bgTop, bgBottom], + startPoint: .top, + endPoint: .bottom + ) + } +} + +// MARK: - Coin Icon (Widget — uses prefetched Data) + +private struct WidgetCoinIcon: View { + let symbol: String + let data: Data? + let size: CGFloat + + var body: some View { + if let data, let img = NSImage(data: data) { + Image(nsImage: img) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size, height: size) + .clipShape(Circle()) + } else { + ZStack { + Circle() + .fill( + LinearGradient( + colors: [coinColor(symbol), coinColor(symbol).opacity(0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + Text(String(symbol.prefix(1))) + .font(.system(size: size * 0.42, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } + .frame(width: size, height: size) + } + } +} + +private func coinColor(_ s: String) -> Color { + switch s { + case "BTC": return .orange + case "ETH": return Color(red: 0.39, green: 0.49, blue: 0.94) + case "SOL": return Color(red: 0.56, green: 0.25, blue: 0.95) + case "BNB": return Color(red: 0.96, green: 0.72, blue: 0.04) + case "XRP": return Color(red: 0.14, green: 0.14, blue: 0.14) + case "TON": return Color(red: 0.04, green: 0.65, blue: 0.93) + default: return Color(red: 0.55, green: 0.55, blue: 0.60) + } +} + +// MARK: - Crypto Row + +private struct CryptoRow: View { + let symbol: SymbolThumb + let iconData: Data? + let compact: Bool + + var body: some View { + HStack(spacing: compact ? 8 : 10) { + WidgetCoinIcon(symbol: symbol.quoteCurrency, data: iconData, size: compact ? 26 : 30) + + VStack(alignment: .leading, spacing: 1) { + HStack(spacing: 2) { + Text(symbol.quoteCurrency) + .fontWeight(.bold) + Text("/") + .foregroundStyle(.white.opacity(0.35)) + Text(symbol.baseCurrency) + .fontWeight(.medium) + .foregroundStyle(.white.opacity(0.55)) + } + .font(.system(size: compact ? 12 : 13)) + .foregroundStyle(.white) + + Text(symbol.quoteCurrencyName) + .font(.system(size: compact ? 9 : 10)) + .foregroundStyle(.white.opacity(0.4)) + .lineLimit(1) + } + + Spacer(minLength: 4) + + Text(symbol.priceFormatted) + .font(.system(size: compact ? 13 : 14, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + .lineLimit(1) + .minimumScaleFactor(0.6) + + Text(symbol.changePercent) + .font(.system(size: compact ? 10 : 11, weight: .bold, design: .rounded)) + .foregroundStyle(symbol.isPositive ? Theme.positive : Theme.negative) + .frame(minWidth: compact ? 48 : 54, alignment: .trailing) + } + .padding(.horizontal, compact ? 12 : 14) + .padding(.vertical, compact ? 6 : 8) + } +} + +// MARK: - Small Widget + +struct SmallWidgetView: View { + let entry: CryptoEntry + + var body: some View { + if let pair = entry.filtered.first { + VStack(spacing: 4) { + Spacer(minLength: 0) + + WidgetCoinIcon(symbol: pair.quoteCurrency, data: entry.icons[pair.quoteCurrency], size: 36) + + HStack(spacing: 2) { + Text(pair.quoteCurrency) + .fontWeight(.bold) + Text("/") + .foregroundStyle(.white.opacity(0.35)) + Text(pair.baseCurrency) + .fontWeight(.medium) + .foregroundStyle(.white.opacity(0.55)) + } + .font(.system(size: 13)) + .foregroundStyle(.white) + + Text(pair.priceFormatted) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + .minimumScaleFactor(0.5) + .lineLimit(1) + + Text(pair.changePercent) + .font(.system(size: 13, weight: .bold, design: .rounded)) + .foregroundStyle(pair.isPositive ? Theme.positive : Theme.negative) + + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity) + .containerBackground(for: .widget) { Theme.bgGradient } + } + } +} + +// MARK: - Medium Widget + +struct MediumWidgetView: View { + let entry: CryptoEntry + + var body: some View { + VStack(spacing: 0) { + let items = Array(entry.filtered.prefix(3)) + ForEach(Array(items.enumerated()), id: \.element.id) { idx, sym in + CryptoRow(symbol: sym, iconData: entry.icons[sym.quoteCurrency], compact: true) + if idx < items.count - 1 { + Theme.divider.frame(height: 1).padding(.leading, 46) + } + } + } + .frame(maxHeight: .infinity) + .containerBackground(for: .widget) { Theme.bgGradient } + } +} + +// MARK: - Large Widget + +struct LargeWidgetView: View { + let entry: CryptoEntry + + var body: some View { + VStack(spacing: 0) { + let items = Array(entry.filtered.prefix(6)) + ForEach(Array(items.enumerated()), id: \.element.id) { idx, sym in + CryptoRow(symbol: sym, iconData: entry.icons[sym.quoteCurrency], compact: false) + if idx < items.count - 1 { + Theme.divider.frame(height: 1).padding(.leading, 54) + } + } + Spacer(minLength: 0) + } + .containerBackground(for: .widget) { Theme.bgGradient } + } +} + +// MARK: - Entry View + +struct CryptoWidgetEntryView: View { + var entry: CryptoEntry + @Environment(\.widgetFamily) private var family + + var body: some View { + switch family { + case .systemSmall: SmallWidgetView(entry: entry) + case .systemMedium: MediumWidgetView(entry: entry) + case .systemLarge: LargeWidgetView(entry: entry) + default: MediumWidgetView(entry: entry) + } + } +} + +// MARK: - Widget + +@main +struct CryptoWidget: Widget { + let kind = "CryptoWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: CryptoProvider()) { entry in + CryptoWidgetEntryView(entry: entry) + } + .configurationDisplayName("Rapira Crypto") + .description("Курсы криптовалют в реальном времени") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} + +// MARK: - Preview + +#Preview(as: .systemLarge) { + CryptoWidget() +} timeline: { + CryptoEntry(date: .now, symbols: CryptoProvider.mock, selectedPairs: ["BTC/USDT", "ETH/USDT", "SOL/USDT", "TON/USDT"], icons: [:]) +} \ No newline at end of file diff --git a/crypto/Info.plist b/crypto/Info.plist new file mode 100644 index 0000000..32ce55c --- /dev/null +++ b/crypto/Info.plist @@ -0,0 +1,16 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/cryptoExtension.entitlements b/cryptoExtension.entitlements new file mode 100644 index 0000000..af5e84f --- /dev/null +++ b/cryptoExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.robonen.rapira-market-widget.crypto + + + diff --git a/rapira-market-widget.xcodeproj/project.pbxproj b/rapira-market-widget.xcodeproj/project.pbxproj new file mode 100644 index 0000000..07391c9 --- /dev/null +++ b/rapira-market-widget.xcodeproj/project.pbxproj @@ -0,0 +1,563 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 48E90CC92F734F8800D6ABD4 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 48E90CC82F734F8800D6ABD4 /* WidgetKit.framework */; }; + 48E90CCB2F734F8800D6ABD4 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 48E90CCA2F734F8800D6ABD4 /* SwiftUI.framework */; }; + 48E90CD82F734F8900D6ABD4 /* cryptoExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 48E90CC62F734F8800D6ABD4 /* cryptoExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 48E90CD62F734F8900D6ABD4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 48E90CAC2F734DBF00D6ABD4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 48E90CC52F734F8800D6ABD4; + remoteInfo = cryptoExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 48E90CDD2F734F8900D6ABD4 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 48E90CD82F734F8900D6ABD4 /* cryptoExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 48E90CB42F734DBF00D6ABD4 /* rapira-market-widget.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "rapira-market-widget.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 48E90CC62F734F8800D6ABD4 /* cryptoExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = cryptoExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 48E90CC82F734F8800D6ABD4 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 48E90CCA2F734F8800D6ABD4 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 48E90CDE2F734FB600D6ABD4 /* cryptoExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = cryptoExtension.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 48E90CD92F734F8900D6ABD4 /* Exceptions for "crypto" folder in "cryptoExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 48E90CC52F734F8800D6ABD4 /* cryptoExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 48E90CB62F734DBF00D6ABD4 /* rapira-market-widget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "rapira-market-widget"; + sourceTree = ""; + }; + 48E90CCC2F734F8800D6ABD4 /* crypto */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 48E90CD92F734F8900D6ABD4 /* Exceptions for "crypto" folder in "cryptoExtension" target */, + ); + path = crypto; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 48E90CB12F734DBF00D6ABD4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48E90CC32F734F8800D6ABD4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 48E90CCB2F734F8800D6ABD4 /* SwiftUI.framework in Frameworks */, + 48E90CC92F734F8800D6ABD4 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 48E90CAB2F734DBF00D6ABD4 = { + isa = PBXGroup; + children = ( + 48E90CDE2F734FB600D6ABD4 /* cryptoExtension.entitlements */, + 48E90CB62F734DBF00D6ABD4 /* rapira-market-widget */, + 48E90CCC2F734F8800D6ABD4 /* crypto */, + 48E90CC72F734F8800D6ABD4 /* Frameworks */, + 48E90CB52F734DBF00D6ABD4 /* Products */, + ); + sourceTree = ""; + }; + 48E90CB52F734DBF00D6ABD4 /* Products */ = { + isa = PBXGroup; + children = ( + 48E90CB42F734DBF00D6ABD4 /* rapira-market-widget.app */, + 48E90CC62F734F8800D6ABD4 /* cryptoExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 48E90CC72F734F8800D6ABD4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 48E90CC82F734F8800D6ABD4 /* WidgetKit.framework */, + 48E90CCA2F734F8800D6ABD4 /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 48E90CB32F734DBF00D6ABD4 /* rapira-market-widget */ = { + isa = PBXNativeTarget; + buildConfigurationList = 48E90CBF2F734DC100D6ABD4 /* Build configuration list for PBXNativeTarget "rapira-market-widget" */; + buildPhases = ( + 48E90CB02F734DBF00D6ABD4 /* Sources */, + 48E90CB12F734DBF00D6ABD4 /* Frameworks */, + 48E90CB22F734DBF00D6ABD4 /* Resources */, + 48E90CDD2F734F8900D6ABD4 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 48E90CD72F734F8900D6ABD4 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 48E90CB62F734DBF00D6ABD4 /* rapira-market-widget */, + ); + name = "rapira-market-widget"; + packageProductDependencies = ( + ); + productName = "rapira-market-widget"; + productReference = 48E90CB42F734DBF00D6ABD4 /* rapira-market-widget.app */; + productType = "com.apple.product-type.application"; + }; + 48E90CC52F734F8800D6ABD4 /* cryptoExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 48E90CDA2F734F8900D6ABD4 /* Build configuration list for PBXNativeTarget "cryptoExtension" */; + buildPhases = ( + 48E90CC22F734F8800D6ABD4 /* Sources */, + 48E90CC32F734F8800D6ABD4 /* Frameworks */, + 48E90CC42F734F8800D6ABD4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 48E90CCC2F734F8800D6ABD4 /* crypto */, + ); + name = cryptoExtension; + packageProductDependencies = ( + ); + productName = cryptoExtension; + productReference = 48E90CC62F734F8800D6ABD4 /* cryptoExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 48E90CAC2F734DBF00D6ABD4 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2630; + LastUpgradeCheck = 2630; + TargetAttributes = { + 48E90CB32F734DBF00D6ABD4 = { + CreatedOnToolsVersion = 26.3; + }; + 48E90CC52F734F8800D6ABD4 = { + CreatedOnToolsVersion = 26.3; + }; + }; + }; + buildConfigurationList = 48E90CAF2F734DBF00D6ABD4 /* Build configuration list for PBXProject "rapira-market-widget" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 48E90CAB2F734DBF00D6ABD4; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 48E90CB52F734DBF00D6ABD4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 48E90CB32F734DBF00D6ABD4 /* rapira-market-widget */, + 48E90CC52F734F8800D6ABD4 /* cryptoExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 48E90CB22F734DBF00D6ABD4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48E90CC42F734F8800D6ABD4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 48E90CB02F734DBF00D6ABD4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48E90CC22F734F8800D6ABD4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 48E90CD72F734F8900D6ABD4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 48E90CC52F734F8800D6ABD4 /* cryptoExtension */; + targetProxy = 48E90CD62F734F8900D6ABD4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 48E90CBD2F734DC100D6ABD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = 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_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = 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_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = WA8SWY233K; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 48E90CBE2F734DC100D6ABD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = 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_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = 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_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = WA8SWY233K; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 48E90CC02F734DC100D6ABD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "rapira-market-widget/rapira-market-widget.entitlements"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = WA8SWY233K; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "robonen.rapira-market-widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 48E90CC12F734DC100D6ABD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "rapira-market-widget/rapira-market-widget.entitlements"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = WA8SWY233K; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "robonen.rapira-market-widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 48E90CDB2F734F8900D6ABD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = cryptoExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = WA8SWY233K; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = crypto/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = crypto; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "robonen.rapira-market-widget.crypto"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 48E90CDC2F734F8900D6ABD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = cryptoExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = WA8SWY233K; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = crypto/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = crypto; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "robonen.rapira-market-widget.crypto"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 48E90CAF2F734DBF00D6ABD4 /* Build configuration list for PBXProject "rapira-market-widget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 48E90CBD2F734DC100D6ABD4 /* Debug */, + 48E90CBE2F734DC100D6ABD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 48E90CBF2F734DC100D6ABD4 /* Build configuration list for PBXNativeTarget "rapira-market-widget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 48E90CC02F734DC100D6ABD4 /* Debug */, + 48E90CC12F734DC100D6ABD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 48E90CDA2F734F8900D6ABD4 /* Build configuration list for PBXNativeTarget "cryptoExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 48E90CDB2F734F8900D6ABD4 /* Debug */, + 48E90CDC2F734F8900D6ABD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 48E90CAC2F734DBF00D6ABD4 /* Project object */; +} diff --git a/rapira-market-widget.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/rapira-market-widget.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/rapira-market-widget.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/rapira-market-widget.xcodeproj/xcuserdata/robonen.xcuserdatad/xcschemes/xcschememanagement.plist b/rapira-market-widget.xcodeproj/xcuserdata/robonen.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..fbc5e61 --- /dev/null +++ b/rapira-market-widget.xcodeproj/xcuserdata/robonen.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + cryptoExtension.xcscheme_^#shared#^_ + + orderHint + 0 + + rapira-market-widget.xcscheme_^#shared#^_ + + orderHint + 1 + + + + diff --git a/rapira-market-widget/Assets.xcassets/AccentColor.colorset/Contents.json b/rapira-market-widget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/rapira-market-widget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/rapira-market-widget/Assets.xcassets/AppIcon.appiconset/Contents.json b/rapira-market-widget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/rapira-market-widget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/rapira-market-widget/Assets.xcassets/Contents.json b/rapira-market-widget/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/rapira-market-widget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/rapira-market-widget/CryptoWidgetSettings.swift b/rapira-market-widget/CryptoWidgetSettings.swift new file mode 100644 index 0000000..09c49b7 --- /dev/null +++ b/rapira-market-widget/CryptoWidgetSettings.swift @@ -0,0 +1,206 @@ +import SwiftUI +import WidgetKit + +private let appGreen = Color(red: 0.20, green: 0.78, blue: 0.35) +private let appRed = Color(red: 1.0, green: 0.23, blue: 0.19) + +// MARK: - Settings View + +struct CryptoWidgetSettingsView: View { + @State private var allSymbols: [SymbolThumb] = [] + @State private var selectedPairs: Set = [] + @State private var searchText = "" + @State private var isLoading = false + @State private var saved = false + + private let maxSelection = 6 + + private var filtered: [SymbolThumb] { + if searchText.isEmpty { return allSymbols } + let q = searchText.lowercased() + return allSymbols.filter { + $0.symbol.lowercased().contains(q) || + $0.quoteCurrencyName.lowercased().contains(q) + } + } + + var body: some View { + VStack(spacing: 0) { + // Header + HStack { + Text("Rapira Crypto") + .font(.system(size: 16, weight: .bold)) + Text("\(selectedPairs.count)/\(maxSelection)") + .font(.system(size: 12, weight: .medium, design: .rounded)) + .foregroundStyle(.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(.quaternary, in: Capsule()) + + Spacer() + + Button { + savePairs() + } label: { + HStack(spacing: 4) { + if saved { + Image(systemName: "checkmark") + .font(.system(size: 11, weight: .bold)) + Text("Сохранено") + } else { + Text("Сохранить") + } + } + .font(.system(size: 12, weight: .semibold)) + } + .buttonStyle(.borderedProminent) + .tint(saved ? .green : .accentColor) + .controlSize(.small) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + // Search + HStack(spacing: 6) { + Image(systemName: "magnifyingglass") + .foregroundStyle(.tertiary) + .font(.system(size: 12)) + TextField("Поиск пары...", text: $searchText) + .textFieldStyle(.plain) + .font(.system(size: 13)) + } + .padding(8) + .background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + .padding(.horizontal, 16) + .padding(.bottom, 8) + + Divider() + + // List + if isLoading { + Spacer() + ProgressView() + .scaleEffect(0.8) + Spacer() + } else { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(filtered) { sym in + AppPairRow( + symbol: sym, + isSelected: selectedPairs.contains(sym.symbol), + canSelect: selectedPairs.count < maxSelection || selectedPairs.contains(sym.symbol) + ) { + togglePair(sym.symbol) + } + Divider().padding(.leading, 56).opacity(0.4) + } + } + .padding(.vertical, 4) + } + } + } + .frame(width: 420, height: 560) + .background(Color(.windowBackgroundColor)) + .onChange(of: selectedPairs) { _, _ in + saved = false + } + .task { + loadSavedPairs() + await loadData() + } + } + + // MARK: - Actions + + private func togglePair(_ pair: String) { + if selectedPairs.contains(pair) { + selectedPairs.remove(pair) + } else if selectedPairs.count < maxSelection { + selectedPairs.insert(pair) + } + } + + private func savePairs() { + let defaults = UserDefaults(suiteName: "group.robonen.rapira-market-widget.crypto") + defaults?.set(Array(selectedPairs), forKey: "selectedPairs") + WidgetCenter.shared.reloadAllTimelines() + withAnimation(.easeInOut(duration: 0.2)) { saved = true } + } + + private func loadSavedPairs() { + let defaults = UserDefaults(suiteName: "group.robonen.rapira-market-widget.crypto") + if let s = defaults?.stringArray(forKey: "selectedPairs") { + selectedPairs = Set(s) + } else { + selectedPairs = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] + } + } + + private func loadData() async { + isLoading = true + defer { isLoading = false } + guard let data = try? await RapiraAPI.fetch() else { return } + allSymbols = data + .filter { $0.baseCurrency == "USDT" } + .sorted { $0.turnover > $1.turnover } + } +} + +// MARK: - Pair Row + +private struct AppPairRow: View { + let symbol: SymbolThumb + let isSelected: Bool + let canSelect: Bool + let onToggle: () -> Void + + var body: some View { + Button(action: onToggle) { + HStack(spacing: 10) { + // Selection dot + Circle() + .fill(isSelected ? Color.accentColor : Color.clear) + .frame(width: 8, height: 8) + .overlay(Circle().strokeBorder(isSelected ? Color.clear : Color.secondary.opacity(0.3), lineWidth: 1)) + + CoinIcon(symbol: symbol.quoteCurrency, size: 30) + + VStack(alignment: .leading, spacing: 1) { + Text(symbol.quoteCurrency) + .font(.system(size: 13, weight: .semibold)) + Text(symbol.quoteCurrencyName) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .lineLimit(1) + } + + Spacer(minLength: 4) + + Text(symbol.priceFormatted) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineLimit(1) + .minimumScaleFactor(0.7) + + Text(symbol.changePercent) + .font(.system(size: 11, weight: .semibold, design: .rounded)) + .foregroundStyle(.white) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background(symbol.isPositive ? appGreen : appRed, in: RoundedRectangle(cornerRadius: 5, style: .continuous)) + } + .padding(.horizontal, 16) + .padding(.vertical, 7) + .contentShape(Rectangle()) + .opacity((!canSelect && !isSelected) ? 0.35 : 1.0) + } + .buttonStyle(.plain) + .disabled(!canSelect && !isSelected) + } +} + +// MARK: - Preview + +#Preview { + CryptoWidgetSettingsView() +} \ No newline at end of file diff --git a/rapira-market-widget/RapiraMarketWidgetApp.swift b/rapira-market-widget/RapiraMarketWidgetApp.swift new file mode 100644 index 0000000..2bbccae --- /dev/null +++ b/rapira-market-widget/RapiraMarketWidgetApp.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@main +struct RapiraMarketWidgetApp: App { + var body: some Scene { + Window("Rapira Crypto", id: "main") { + CryptoWidgetSettingsView() + } + .windowResizability(.contentSize) + } +} diff --git a/rapira-market-widget/SharedModels.swift b/rapira-market-widget/SharedModels.swift new file mode 100644 index 0000000..fe6c9ab --- /dev/null +++ b/rapira-market-widget/SharedModels.swift @@ -0,0 +1,95 @@ +import SwiftUI + +// MARK: - Models (shared with widget extension) + +struct SymbolThumb: Codable, Identifiable, Sendable { + var id: String { symbol } + let symbol: String + let open: Double + let high: Double + let low: Double + let close: Double + let chg: Double + let change: Double + let volume: Double + let turnover: Double + let quoteCurrencyName: String + let baseCurrency: String + let quoteCurrency: String + + var isPositive: Bool { chg >= 0 } + + var changePercent: String { + String(format: "%+.2f%%", chg * 100) + } + + var priceFormatted: String { + let fmt = NumberFormatter() + fmt.numberStyle = .decimal + fmt.minimumFractionDigits = 2 + if close >= 1000 { + fmt.maximumFractionDigits = 2 + } else if close >= 1 { + fmt.maximumFractionDigits = 4 + } else { + fmt.maximumFractionDigits = 6 + } + return fmt.string(from: NSNumber(value: close)) ?? String(format: "%.2f", close) + } +} + +// MARK: - API + +struct RapiraAPI { + static let url = URL(string: "https://api.rapira.net/market/symbol-thumb")! + + static func fetch() async throws -> [SymbolThumb] { + let (data, _) = try await URLSession.shared.data(from: url) + return try JSONDecoder().decode([SymbolThumb].self, from: data) + } +} + +// MARK: - Coin Icon (App — async CDN loading) + +struct CoinIcon: View { + let symbol: String + var size: CGFloat = 32 + + @State private var image: NSImage? + + var body: some View { + Group { + if let image { + Image(nsImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(Circle()) + } else { + ZStack { + Circle().fill(fallbackColor.opacity(0.15)) + Text(String(symbol.prefix(1))) + .font(.system(size: size * 0.4, weight: .bold, design: .rounded)) + .foregroundStyle(fallbackColor) + } + } + } + .frame(width: size, height: size) + .task(id: symbol) { + guard let url = URL(string: "https://cdn.rapira.net/media/crypto/\(symbol).svg") else { return } + guard let (data, _) = try? await URLSession.shared.data(from: url), + let img = NSImage(data: data) else { return } + image = img + } + } + + private var fallbackColor: Color { + switch symbol { + case "BTC": return .orange + case "ETH": return Color(red: 0.39, green: 0.49, blue: 0.94) + case "SOL": return Color(red: 0.56, green: 0.25, blue: 0.95) + case "BNB": return Color(red: 0.96, green: 0.72, blue: 0.04) + case "TON": return Color(red: 0.04, green: 0.65, blue: 0.93) + default: return Color(red: 0.55, green: 0.55, blue: 0.60) + } + } +} diff --git a/rapira-market-widget/rapira-market-widget.entitlements b/rapira-market-widget/rapira-market-widget.entitlements new file mode 100644 index 0000000..af5e84f --- /dev/null +++ b/rapira-market-widget/rapira-market-widget.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.robonen.rapira-market-widget.crypto + + +