From e3af5054048832065ad3564432e158e7121e6b5b Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Fri, 15 May 2026 21:22:16 +0200 Subject: [PATCH] feat: add bitcoin facts widgets --- Bitkit.xcodeproj/project.pbxproj | 52 +++++- .../icons/bitcoin.imageset}/Contents.json | 2 +- .../icons/bitcoin.imageset/bitcoin.pdf | Bin 0 -> 6147 bytes Bitkit/Components/Widgets/FactsWidget.swift | 87 +++++----- Bitkit/MainNavView.swift | 2 + .../BitcoinFacts.swift} | 31 +--- Bitkit/Services/MigrationsService.swift | 15 -- Bitkit/Styles/Colors.swift | 1 + Bitkit/Utilities/WidgetsBackupConverter.swift | 26 +-- .../ViewModels/Widgets/FactsViewModel.swift | 11 +- Bitkit/ViewModels/WidgetsViewModel.swift | 16 +- .../Widgets/FactsWidgetPreviewView.swift | 161 ++++++++++++++++++ Bitkit/Views/Widgets/WidgetDetailView.swift | 4 +- Bitkit/Views/Widgets/WidgetEditLogic.swift | 36 +--- Bitkit/Views/Widgets/WidgetEditModels.swift | 32 +--- Bitkit/Views/Widgets/WidgetEditView.swift | 3 - .../bitcoin.imageset/Contents.json | 15 ++ .../bitcoin.imageset/bitcoin.pdf | Bin 0 -> 6031 bytes .../Assets.xcassets/btc.imageset/btc.pdf | Bin 11533 -> 0 bytes BitkitWidget/BitkitWidget.swift | 1 + BitkitWidget/FactsHomeScreenWidget.swift | 141 +++++++++++++++ 21 files changed, 442 insertions(+), 194 deletions(-) rename {BitkitWidget/Assets.xcassets/btc.imageset => Bitkit/Assets.xcassets/icons/bitcoin.imageset}/Contents.json (78%) create mode 100644 Bitkit/Assets.xcassets/icons/bitcoin.imageset/bitcoin.pdf rename Bitkit/{Services/Widgets/FactsService.swift => Models/BitcoinFacts.swift} (81%) create mode 100644 Bitkit/Views/Widgets/FactsWidgetPreviewView.swift create mode 100644 BitkitWidget/Assets.xcassets/bitcoin.imageset/Contents.json create mode 100644 BitkitWidget/Assets.xcassets/bitcoin.imageset/bitcoin.pdf delete mode 100644 BitkitWidget/Assets.xcassets/btc.imageset/btc.pdf create mode 100644 BitkitWidget/FactsHomeScreenWidget.swift diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 703b0c05a..e7c80f00c 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 4A319B512E8F24F2002B9AC9 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A319B502E8F24F2002B9AC9 /* WidgetKit.framework */; }; 4A319B532E8F24F2002B9AC9 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A319B522E8F24F2002B9AC9 /* SwiftUI.framework */; }; 4A319B622E8F24F4002B9AC9 /* BitkitWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4A319B4F2E8F24F2002B9AC9 /* BitkitWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4A319B702E8F2600002B9AC9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4A319B712E8F2600002B9AC9 /* Localizable.strings */; }; 4AAB08CA2E1FE77600BA63DF /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4AAB08C92E1FE77600BA63DF /* Lottie */; }; 961058E32C355B5500E1F1D8 /* BitkitNotification.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 961058DC2C355B5500E1F1D8 /* BitkitNotification.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 968FDF162DFAFE230053CD7F /* LDKNode in Frameworks */ = {isa = PBXBuildFile; productRef = 9613018B2C5022D700878183 /* LDKNode */; }; @@ -79,6 +80,21 @@ 4A319B502E8F24F2002B9AC9 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 4A319B522E8F24F2002B9AC9 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 4A319B6E2E8F25F6002B9AC9 /* BitkitWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BitkitWidgetExtension.entitlements; sourceTree = ""; }; + 4A319B722E8F2600002B9AC9 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = Bitkit/Resources/Localization/ar.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B732E8F2600002B9AC9 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = Bitkit/Resources/Localization/ca.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B742E8F2600002B9AC9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = Bitkit/Resources/Localization/cs.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B752E8F2600002B9AC9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = Bitkit/Resources/Localization/de.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B762E8F2600002B9AC9 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = Bitkit/Resources/Localization/el.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B772E8F2600002B9AC9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Bitkit/Resources/Localization/en.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B782E8F2600002B9AC9 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "Bitkit/Resources/Localization/es-419.lproj/Localizable.strings"; sourceTree = SOURCE_ROOT; }; + 4A319B792E8F2600002B9AC9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Bitkit/Resources/Localization/es.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B7A2E8F2600002B9AC9 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Bitkit/Resources/Localization/fr.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B7B2E8F2600002B9AC9 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = Bitkit/Resources/Localization/it.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B7C2E8F2600002B9AC9 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Bitkit/Resources/Localization/nl.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B7D2E8F2600002B9AC9 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = Bitkit/Resources/Localization/pl.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B7E2E8F2600002B9AC9 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings"; sourceTree = SOURCE_ROOT; }; + 4A319B7F2E8F2600002B9AC9 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = Bitkit/Resources/Localization/pt.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 4A319B802E8F2600002B9AC9 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Bitkit/Resources/Localization/ru.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; 961058DC2C355B5500E1F1D8 /* BitkitNotification.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BitkitNotification.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 96FE1F612C2DE6AA006D0C8B /* Bitkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bitkit.app; sourceTree = BUILT_PRODUCTS_DIR; }; 96FE1F722C2DE6AC006D0C8B /* BitkitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BitkitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -177,6 +193,7 @@ Models/BlocksWidgetData.swift, Models/BlocksWidgetFields.swift, Models/BlocksWidgetOptions.swift, + Models/BitcoinFacts.swift, Models/NewsWidgetData.swift, Models/NewsWidgetOptions.swift, Models/PriceWidgetData.swift, @@ -274,6 +291,7 @@ 96A44F562CEF5F5400FBACFF /* BitkitUITests */, 96A44F5C2CEF5F5800FBACFF /* BitkitNotification */, 4A319B542E8F24F2002B9AC9 /* BitkitWidget */, + 4A319B712E8F2600002B9AC9 /* Localizable.strings */, 96FE1F622C2DE6AA006D0C8B /* Products */, 961058EC2C35798C00E1F1D8 /* Frameworks */, ); @@ -496,6 +514,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4A319B702E8F2600002B9AC9 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -529,6 +548,31 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXVariantGroup section */ + 4A319B712E8F2600002B9AC9 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 4A319B722E8F2600002B9AC9 /* ar */, + 4A319B732E8F2600002B9AC9 /* ca */, + 4A319B742E8F2600002B9AC9 /* cs */, + 4A319B752E8F2600002B9AC9 /* de */, + 4A319B762E8F2600002B9AC9 /* el */, + 4A319B772E8F2600002B9AC9 /* en */, + 4A319B782E8F2600002B9AC9 /* es-419 */, + 4A319B792E8F2600002B9AC9 /* es */, + 4A319B7A2E8F2600002B9AC9 /* fr */, + 4A319B7B2E8F2600002B9AC9 /* it */, + 4A319B7C2E8F2600002B9AC9 /* nl */, + 4A319B7D2E8F2600002B9AC9 /* pl */, + 4A319B7E2E8F2600002B9AC9 /* pt-BR */, + 4A319B7F2E8F2600002B9AC9 /* pt */, + 4A319B802E8F2600002B9AC9 /* ru */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin PBXShellScriptBuildPhase section */ 96EMBED0012026012000FRAME /* Remove Static Framework Stubs */ = { isa = PBXShellScriptBuildPhase; @@ -621,7 +665,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = BitkitWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 187; DEVELOPMENT_TEAM = KYH47R284B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BitkitWidget/Info.plist; @@ -633,7 +677,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = "to.bitkit.widget"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -654,7 +698,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = BitkitWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 187; DEVELOPMENT_TEAM = KYH47R284B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BitkitWidget/Info.plist; @@ -666,7 +710,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = "to.bitkit.widget"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/BitkitWidget/Assets.xcassets/btc.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/bitcoin.imageset/Contents.json similarity index 78% rename from BitkitWidget/Assets.xcassets/btc.imageset/Contents.json rename to Bitkit/Assets.xcassets/icons/bitcoin.imageset/Contents.json index 50f875c92..8a6583348 100644 --- a/BitkitWidget/Assets.xcassets/btc.imageset/Contents.json +++ b/Bitkit/Assets.xcassets/icons/bitcoin.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "btc.pdf", + "filename" : "bitcoin.pdf", "idiom" : "universal" } ], diff --git a/Bitkit/Assets.xcassets/icons/bitcoin.imageset/bitcoin.pdf b/Bitkit/Assets.xcassets/icons/bitcoin.imageset/bitcoin.pdf new file mode 100644 index 0000000000000000000000000000000000000000..caaae04e51b6553e476e406da137a35ee3670892 GIT binary patch literal 6147 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 PrVND&?Axb*%;0|jDIlA0 literal 0 HcmV?d00001 diff --git a/Bitkit/Components/Widgets/FactsWidget.swift b/Bitkit/Components/Widgets/FactsWidget.swift index 2944c35f1..b4f43db9f 100644 --- a/Bitkit/Components/Widgets/FactsWidget.swift +++ b/Bitkit/Components/Widgets/FactsWidget.swift @@ -1,45 +1,17 @@ import SwiftUI -/// Options for configuring the FactsWidget -struct FactsWidgetOptions: Codable, Equatable { - var showSource: Bool = true -} - struct FactsWidget: View { - /// Configuration options for the widget - var options: FactsWidgetOptions = .init() - - /// Flag indicating if the widget is in editing mode var isEditing: Bool = false - - /// Callback to signal when editing should end var onEditingEnd: (() -> Void)? - /// View model for handling facts data @StateObject private var viewModel = FactsViewModel.shared - /// Initialize the widget - init( - options: FactsWidgetOptions = FactsWidgetOptions(), - isEditing: Bool = false, - onEditingEnd: (() -> Void)? = nil - ) { - self.options = options - self.isEditing = isEditing - self.onEditingEnd = onEditingEnd - } - - /// Initialize with a custom view model (for previews) init( - viewModel: FactsViewModel, - options: FactsWidgetOptions = FactsWidgetOptions(), isEditing: Bool = false, onEditingEnd: (() -> Void)? = nil ) { - self.options = options self.isEditing = isEditing self.onEditingEnd = onEditingEnd - _viewModel = StateObject(wrappedValue: viewModel) } var body: some View { @@ -48,30 +20,57 @@ struct FactsWidget: View { isEditing: isEditing, onEditingEnd: onEditingEnd ) { - VStack(spacing: 0) { - TitleText(viewModel.fact) - .lineLimit(2) - .frame(maxWidth: .infinity, alignment: .leading) + FactsWidgetWideContent(fact: viewModel.fact) + } + } +} - if options.showSource { - WidgetContentBuilder.sourceRow(source: "synonym.to") - } - } +struct FactsWidgetWideContent: View { + let fact: String + + var body: some View { + HStack(alignment: .top, spacing: 32) { + TitleText(fact) + .lineLimit(4) + .frame(maxWidth: .infinity, alignment: .leading) + + BitcoinLogo() } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct FactsWidgetCompactContent: View { + let fact: String + + var body: some View { + BodyMSBText(fact) + .lineLimit(4) + .frame(maxWidth: .infinity, alignment: .leading) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .overlay(alignment: .bottomTrailing) { + BitcoinLogo() + } + .padding(16) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(Color.gray6) + .cornerRadius(16) + } +} + +private struct BitcoinLogo: View { + var body: some View { + Image("bitcoin") + .resizable() + .frame(width: 32, height: 32) } } #Preview { VStack(spacing: 16) { FactsWidget() - - FactsWidget( - options: FactsWidgetOptions(showSource: false) - ) - - FactsWidget( - isEditing: true - ) + FactsWidget(isEditing: true) } .padding() .background(Color.black) diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 136ee5ff1..023305007 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -445,6 +445,8 @@ struct MainNavView: View { NewsWidgetPreviewView() case .blocks: BlocksWidgetPreviewView() + case .facts: + FactsWidgetPreviewView() default: WidgetDetailView(id: widgetType) } diff --git a/Bitkit/Services/Widgets/FactsService.swift b/Bitkit/Models/BitcoinFacts.swift similarity index 81% rename from Bitkit/Services/Widgets/FactsService.swift rename to Bitkit/Models/BitcoinFacts.swift index 5ccfddca3..48d5fca8c 100644 --- a/Bitkit/Services/Widgets/FactsService.swift +++ b/Bitkit/Models/BitcoinFacts.swift @@ -1,26 +1,7 @@ import Foundation -/// Service for managing Bitcoin facts -class FactsService { - static let shared = FactsService() - - private init() {} - - /// Returns a random Bitcoin fact - /// - Returns: A Bitcoin fact string - func getRandomFact() -> String { - return facts.randomElement()! - } - - /// Returns all available Bitcoin facts - /// - Returns: Array of Bitcoin facts - func getAllFacts() -> [String] { - return facts - } - - // MARK: - Private Properties - - private let facts = [ +enum BitcoinFacts { + static let all = [ "Satoshi Nakamoto mined more than 1M Bitcoin.", "You don't need permission to use Bitcoin.", "You don't need a bank account to use Bitcoin.", @@ -36,7 +17,7 @@ class FactsService { "The largest transaction was 500,000 bitcoin.", "Bitcoin is legal tender in El Salvador.", "Not your keys, not your coins.", - "’Bitcoin’ is the network, ‘bitcoin’ is the currency.", + "'Bitcoin' is the network, 'bitcoin' is the currency.", "Bitcoin was not the first digital currency.", "Bitcoin was first created with 31,000 lines of code.", "Bitcoin does not have a CEO.", @@ -48,10 +29,10 @@ class FactsService { "The identity of Bitcoin's inventor is unknown.", "If you lose your keys, you lose your coins.", "Bitcoins don't grow on trees.", - "There can only be 21 million bitcoins. ", + "There can only be 21 million bitcoins.", "Bitcoins are created when a block is mined.", "One bitcoin is 100,000,000 satoshis.", - "The smallest unit of Bitcoin is a “satoshi.”", + "The smallest unit of Bitcoin is a \"satoshi.\"", "Bitcoins live on the blockchain, not in wallets.", "You can hold keys, but you cannot hold bitcoin.", "Private keys allow you to sign transactions.", @@ -84,7 +65,7 @@ class FactsService { "The genesis block reward is not spendable.", "You can count 1 day of blocks on 2 hands.", "There are enough sats for everyone.", - "More computing power ≠ more bitcoin.", + "More computing power != more bitcoin.", "Bitcoin doesn't need your personal info.", "Satoshi considered calling it Netcoin.", ] diff --git a/Bitkit/Services/MigrationsService.swift b/Bitkit/Services/MigrationsService.swift index 43a0a8370..c3a2dd786 100644 --- a/Bitkit/Services/MigrationsService.swift +++ b/Bitkit/Services/MigrationsService.swift @@ -270,10 +270,6 @@ private struct MigrationNewsWidgetOptions: Codable { var showSource: Bool } -private struct MigrationFactsWidgetOptions: Codable { - var showSource: Bool -} - // MARK: - RN Migration Keys enum RNKeychainKey { @@ -1961,17 +1957,6 @@ extension MigrationsService { } } - let factsPrefs = (widgetsDict["factsPreferences"] as? [String: Any]) - ?? (widgetsDict["facts"] as? [String: Any]) - if let prefs = factsPrefs { - let options = MigrationFactsWidgetOptions( - showSource: getBool(from: prefs, key: "showSource", defaultValue: false) - ) - if let data = try? JSONEncoder().encode(options) { - result["facts"] = data - } - } - return result } } diff --git a/Bitkit/Styles/Colors.swift b/Bitkit/Styles/Colors.swift index e3b0533b0..389087b76 100644 --- a/Bitkit/Styles/Colors.swift +++ b/Bitkit/Styles/Colors.swift @@ -10,6 +10,7 @@ extension Color { static let redAccent = Color(hex: 0xE95164) static let yellowAccent = Color(hex: 0xFFD200) static let pubkyGreen = Color(hex: 0xBEFF00) + static let bitcoin = Color(hex: 0xF7931A) // MARK: - Base diff --git a/Bitkit/Utilities/WidgetsBackupConverter.swift b/Bitkit/Utilities/WidgetsBackupConverter.swift index 7783d31f8..706e6660c 100644 --- a/Bitkit/Utilities/WidgetsBackupConverter.swift +++ b/Bitkit/Utilities/WidgetsBackupConverter.swift @@ -7,7 +7,6 @@ enum WidgetsBackupConverter { var widgetsArray: [[String: Any]] = [] var blocksPreferences: [String: Any]? var newsPreferences: [String: Any]? - var factsPreferences: [String: Any]? var weatherPreferences: [String: Any]? var pricePreferences: [String: Any]? @@ -42,12 +41,6 @@ enum WidgetsBackupConverter { "showSource": options.showSource, ] } - case .facts: - if let options = try? JSONDecoder().decode(FactsWidgetOptions.self, from: optionsData) { - factsPreferences = [ - "showSource": options.showSource, - ] - } case .weather: if let options = try? JSONDecoder().decode(WeatherWidgetOptions.self, from: optionsData) { weatherPreferences = [ @@ -66,7 +59,7 @@ enum WidgetsBackupConverter { "period": androidPeriod, ] } - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: break } } @@ -75,7 +68,6 @@ enum WidgetsBackupConverter { return [ "widgets": widgetsArray, "headlinePreferences": newsPreferences ?? getDefaultNewsPreferences(), - "factsPreferences": factsPreferences ?? getDefaultFactsPreferences(), "blocksPreferences": blocksPreferences ?? getDefaultBlocksPreferences(), "weatherPreferences": weatherPreferences ?? getDefaultWeatherPreferences(), "pricePreferences": pricePreferences ?? getDefaultPricePreferences(), @@ -140,13 +132,6 @@ enum WidgetsBackupConverter { ) optionsData = try? JSONEncoder().encode(iosOptions) } - case .facts: - if let prefs = jsonDict["factsPreferences"] as? [String: Any] { - let iosOptions = FactsWidgetOptions( - showSource: prefs["showSource"] as? Bool ?? false - ) - optionsData = try? JSONEncoder().encode(iosOptions) - } case .weather: if let prefs = jsonDict["weatherPreferences"] as? [String: Any] { let iosOptions = WeatherWidgetOptions( @@ -176,7 +161,7 @@ enum WidgetsBackupConverter { ) optionsData = try? JSONEncoder().encode(iosOptions) } - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: break } @@ -209,13 +194,6 @@ enum WidgetsBackupConverter { ] } - private static func getDefaultFactsPreferences() -> [String: Any] { - let defaults = FactsWidgetOptions() - return [ - "showSource": defaults.showSource, - ] - } - private static func getDefaultWeatherPreferences() -> [String: Any] { let defaults = WeatherWidgetOptions() return [ diff --git a/Bitkit/ViewModels/Widgets/FactsViewModel.swift b/Bitkit/ViewModels/Widgets/FactsViewModel.swift index dbf10fb2e..4852746e8 100644 --- a/Bitkit/ViewModels/Widgets/FactsViewModel.swift +++ b/Bitkit/ViewModels/Widgets/FactsViewModel.swift @@ -7,19 +7,18 @@ class FactsViewModel: ObservableObject { @Published var fact: String = "" - private let factsService = FactsService.shared private var refreshTimer: Timer? private let refreshInterval: TimeInterval = 2 * 60 // 2 minutes /// Private initializer for the singleton instance private init() { - fact = factsService.getRandomFact() + fact = randomFact() startRefreshTimer() } /// Public initializer for previews and testing init(preview: Bool = true) { - fact = factsService.getRandomFact() + fact = randomFact() } deinit { @@ -29,8 +28,12 @@ class FactsViewModel: ObservableObject { private func startRefreshTimer() { refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in Task { @MainActor [weak self] in - self?.fact = self?.factsService.getRandomFact() ?? "" + self?.fact = self?.randomFact() ?? "" } } } + + private func randomFact() -> String { + BitcoinFacts.all.randomElement()! + } } diff --git a/Bitkit/ViewModels/WidgetsViewModel.swift b/Bitkit/ViewModels/WidgetsViewModel.swift index 73020328f..31b3929c0 100644 --- a/Bitkit/ViewModels/WidgetsViewModel.swift +++ b/Bitkit/ViewModels/WidgetsViewModel.swift @@ -12,12 +12,10 @@ protocol WidgetOptionsProtocol: Codable, Equatable { /// Default options for each widget type func getDefaultOptions(for type: WidgetType) -> Any { switch type { - case .suggestions, .calculator: + case .suggestions, .calculator, .facts: return EmptyWidgetOptions() case .blocks: return BlocksWidgetOptions() - case .facts: - return FactsWidgetOptions() case .news: return NewsWidgetOptions() case .weather: @@ -74,11 +72,7 @@ struct Widget: Identifiable { case .calculator: CalculatorWidget(isEditing: isEditing, onEditingEnd: onEditingEnd) case .facts: - FactsWidget( - options: widgetsViewModel.getOptions(for: type, as: FactsWidgetOptions.self), - isEditing: isEditing, - onEditingEnd: onEditingEnd - ) + FactsWidget(isEditing: isEditing, onEditingEnd: onEditingEnd) case .news: NewsWidget( options: widgetsViewModel.getOptions(for: type, as: NewsWidgetOptions.self), @@ -275,16 +269,12 @@ class WidgetsViewModel: ObservableObject { /// Check if widget has custom options (different from default) func hasCustomOptions(for type: WidgetType) -> Bool { switch type { - case .suggestions, .calculator: + case .suggestions, .calculator, .facts: return false case .blocks: let current: BlocksWidgetOptions = getOptions(for: type, as: BlocksWidgetOptions.self) let defaultOptions = BlocksWidgetOptions() return current != defaultOptions - case .facts: - let current: FactsWidgetOptions = getOptions(for: type, as: FactsWidgetOptions.self) - let defaultOptions = FactsWidgetOptions() - return current != defaultOptions case .news: let current: NewsWidgetOptions = getOptions(for: type, as: NewsWidgetOptions.self) let defaultOptions = NewsWidgetOptions() diff --git a/Bitkit/Views/Widgets/FactsWidgetPreviewView.swift b/Bitkit/Views/Widgets/FactsWidgetPreviewView.swift new file mode 100644 index 000000000..109d3c45f --- /dev/null +++ b/Bitkit/Views/Widgets/FactsWidgetPreviewView.swift @@ -0,0 +1,161 @@ +import SwiftUI + +/// Preview screen for the Bitcoin Facts widget. +struct FactsWidgetPreviewView: View { + @EnvironmentObject private var navigation: NavigationViewModel + @EnvironmentObject private var widgets: WidgetsViewModel + + @StateObject private var viewModel = FactsViewModel.shared + + @State private var carouselPage: Int = 0 + @State private var showDeleteAlert = false + + private let widgetType: WidgetType = .facts + + private var widgetName: String { + t("widgets__facts__name") + } + + private var widgetDescription: String { + t("widgets__facts__description") + } + + private var isWidgetSaved: Bool { + widgets.isWidgetSaved(widgetType) + } + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + NavigationBar(title: widgetName, showMenuButton: false) + + BodyMText(widgetDescription, textColor: .textSecondary) + + VStack(spacing: 16) { + carousel + + sizeLabel + + pageIndicator + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + buttonsRow + } + .navigationBarHidden(true) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .bottomSafeAreaPadding() + .alert( + t("widgets__delete__title"), + isPresented: $showDeleteAlert, + actions: { + Button(t("common__cancel"), role: .cancel) { showDeleteAlert = false } + Button(t("common__delete_yes"), role: .destructive) { onDelete() } + }, + message: { + Text(t("widgets__delete__description", variables: ["name": widgetName])) + } + ) + } + + private var carousel: some View { + TabView(selection: $carouselPage) { + compactPage.tag(0) + widePage.tag(1) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(maxHeight: .infinity) + } + + private var compactPage: some View { + VStack { + Spacer(minLength: 0) + FactsWidgetCompactContent(fact: viewModel.fact) + .frame(width: 163, height: 192) + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity) + } + + private var widePage: some View { + VStack { + Spacer(minLength: 0) + FactsWidgetWideContent(fact: viewModel.fact) + .padding(16) + .background(Color.gray6) + .cornerRadius(16) + .frame(maxWidth: .infinity) + Spacer(minLength: 0) + } + } + + private var sizeLabel: some View { + HStack { + Spacer() + CaptionMText( + carouselPage == 0 + ? t("widgets__widget__size_small") + : t("widgets__widget__size_wide"), + textColor: .textSecondary + ) + .textCase(.uppercase) + Spacer() + } + } + + private var pageIndicator: some View { + HStack(spacing: 8) { + Spacer() + ForEach(0 ..< 2, id: \.self) { index in + Circle() + .fill(carouselPage == index ? Color.white : Color.white.opacity(0.32)) + .frame(width: 8, height: 8) + } + Spacer() + } + } + + private var buttonsRow: some View { + HStack(spacing: 16) { + if isWidgetSaved { + CustomButton( + title: t("common__delete"), + variant: .secondary, + size: .large, + shouldExpand: true + ) { + showDeleteAlert = true + } + .accessibilityIdentifier("WidgetDelete") + } + + CustomButton( + title: t("widgets__widget__save_widget"), + variant: .primary, + size: .large, + shouldExpand: true, + action: onSave + ) + .accessibilityIdentifier("WidgetSave") + } + } + + private func onSave() { + widgets.saveWidget(widgetType) + navigation.reset() + } + + private func onDelete() { + widgets.deleteWidget(widgetType) + navigation.reset() + } +} + +#Preview { + NavigationStack { + FactsWidgetPreviewView() + .environmentObject(NavigationViewModel()) + .environmentObject(WidgetsViewModel()) + } + .preferredColorScheme(.dark) +} diff --git a/Bitkit/Views/Widgets/WidgetDetailView.swift b/Bitkit/Views/Widgets/WidgetDetailView.swift index 7bab64f76..362e5f2f4 100644 --- a/Bitkit/Views/Widgets/WidgetDetailView.swift +++ b/Bitkit/Views/Widgets/WidgetDetailView.swift @@ -30,9 +30,9 @@ struct WidgetDetailView: View { /// Check if widget has customization options private var hasOptions: Bool { switch id { - case .blocks, .facts, .news, .price, .weather: + case .blocks, .news, .price, .weather: return true - case .suggestions, .calculator: + case .suggestions, .calculator, .facts: return false } } diff --git a/Bitkit/Views/Widgets/WidgetEditLogic.swift b/Bitkit/Views/Widgets/WidgetEditLogic.swift index b530ff4f0..7e50e2f14 100644 --- a/Bitkit/Views/Widgets/WidgetEditLogic.swift +++ b/Bitkit/Views/Widgets/WidgetEditLogic.swift @@ -5,7 +5,6 @@ import SwiftUI @MainActor class WidgetEditLogic: ObservableObject { @Published var blocksOptions = BlocksWidgetOptions() - @Published var factsOptions = FactsWidgetOptions() @Published var newsOptions = NewsWidgetOptions() @Published var weatherOptions = WeatherWidgetOptions() @Published var priceOptions = PriceWidgetOptions() @@ -24,9 +23,9 @@ class WidgetEditLogic: ObservableObject { var hasOptions: Bool { switch widgetType { - case .facts, .blocks, .news, .price, .weather: + case .blocks, .news, .price, .weather: return true - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: return false } } @@ -42,16 +41,13 @@ class WidgetEditLogic: ObservableObject { || blocksOptions.fees case .news: return newsOptions.showTitle || newsOptions.showSource || newsOptions.showDate - case .facts: - // Facts widget's static title is always shown, so it always has an enabled option - return true case .weather: // Weather widget has multiple options, check if any are enabled return weatherOptions.showStatus || weatherOptions.showText || weatherOptions.showMedian || weatherOptions.showNextBlockFee case .price: // Price widget always has a selected pair (single-select). return true - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: return false } } @@ -61,9 +57,6 @@ class WidgetEditLogic: ObservableObject { case .blocks: let defaultOptions = BlocksWidgetOptions() return blocksOptions != defaultOptions - case .facts: - let defaultOptions = FactsWidgetOptions() - return factsOptions != defaultOptions case .news: let defaultOptions = NewsWidgetOptions() return newsOptions != defaultOptions @@ -73,7 +66,7 @@ class WidgetEditLogic: ObservableObject { case .price: let defaultOptions = PriceWidgetOptions() return priceOptions != defaultOptions - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: return false } } @@ -108,13 +101,6 @@ class WidgetEditLogic: ObservableObject { default: break } - case .facts: - switch item.key { - case "showSource": - factsOptions.showSource.toggle() - default: - break - } case .news: switch item.key { case "showDate": @@ -154,7 +140,7 @@ class WidgetEditLogic: ObservableObject { default: break } - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: break } onStateChange?() @@ -179,15 +165,13 @@ class WidgetEditLogic: ObservableObject { switch widgetType { case .blocks: blocksOptions = widgetsViewModel.getOptions(for: widgetType, as: BlocksWidgetOptions.self) - case .facts: - factsOptions = widgetsViewModel.getOptions(for: widgetType, as: FactsWidgetOptions.self) case .news: newsOptions = widgetsViewModel.getOptions(for: widgetType, as: NewsWidgetOptions.self) case .weather: weatherOptions = widgetsViewModel.getOptions(for: widgetType, as: WeatherWidgetOptions.self) case .price: priceOptions = widgetsViewModel.getOptions(for: widgetType, as: PriceWidgetOptions.self) - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: break } } @@ -196,15 +180,13 @@ class WidgetEditLogic: ObservableObject { switch widgetType { case .blocks: blocksOptions = BlocksWidgetOptions() - case .facts: - factsOptions = FactsWidgetOptions() case .news: newsOptions = NewsWidgetOptions() case .weather: weatherOptions = WeatherWidgetOptions() case .price: priceOptions = PriceWidgetOptions() - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: break } onStateChange?() @@ -214,15 +196,13 @@ class WidgetEditLogic: ObservableObject { switch widgetType { case .blocks: widgetsViewModel.saveOptions(blocksOptions, for: widgetType) - case .facts: - widgetsViewModel.saveOptions(factsOptions, for: widgetType) case .news: widgetsViewModel.saveOptions(newsOptions, for: widgetType) case .weather: widgetsViewModel.saveOptions(weatherOptions, for: widgetType) case .price: widgetsViewModel.saveOptions(priceOptions, for: widgetType) - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: break } } diff --git a/Bitkit/Views/Widgets/WidgetEditModels.swift b/Bitkit/Views/Widgets/WidgetEditModels.swift index 5a347f9c1..cffbb22cd 100644 --- a/Bitkit/Views/Widgets/WidgetEditModels.swift +++ b/Bitkit/Views/Widgets/WidgetEditModels.swift @@ -110,32 +110,6 @@ enum WidgetEditItemFactory { return items } - @MainActor - static func getFactsItems(factsViewModel: FactsViewModel, factsOptions: FactsWidgetOptions) -> [WidgetEditItem] { - var items: [WidgetEditItem] = [] - - items.append( - WidgetEditItem( - key: "showTitle", - type: .staticItem, - titleView: AnyView(TitleText(factsViewModel.fact)), - isChecked: true - ) - ) - - items.append( - WidgetEditItem( - key: "showSource", - type: .toggleItem, - title: t("widgets__widget__source"), - valueView: AnyView(BodySSBText("mempool.space", textColor: .textSecondary)), - isChecked: factsOptions.showSource - ) - ) - - return items - } - @MainActor static func getNewsItems( newsViewModel: NewsViewModel, @@ -365,12 +339,10 @@ enum WidgetEditItemFactory { static func getItems( for widgetType: WidgetType, blocksViewModel: BlocksViewModel, - factsViewModel: FactsViewModel, newsViewModel: NewsViewModel, priceDataByPeriod: [GraphPeriod: [PriceData]] = [:], weatherViewModel: WeatherViewModel, blocksOptions: BlocksWidgetOptions, - factsOptions: FactsWidgetOptions, newsOptions: NewsWidgetOptions, priceOptions: PriceWidgetOptions, weatherOptions: WeatherWidgetOptions @@ -378,15 +350,13 @@ enum WidgetEditItemFactory { switch widgetType { case .blocks: return getBlocksItems(blocksViewModel: blocksViewModel, blocksOptions: blocksOptions) - case .facts: - return getFactsItems(factsViewModel: factsViewModel, factsOptions: factsOptions) case .news: return getNewsItems(newsViewModel: newsViewModel, newsOptions: newsOptions) case .price: return getPriceItems(priceOptions: priceOptions, priceDataByPeriod: priceDataByPeriod) case .weather: return getWeatherItems(weatherViewModel: weatherViewModel, weatherOptions: weatherOptions) - case .calculator, .suggestions: + case .calculator, .suggestions, .facts: return [] } } diff --git a/Bitkit/Views/Widgets/WidgetEditView.swift b/Bitkit/Views/Widgets/WidgetEditView.swift index d7714f5d2..b07374d4a 100644 --- a/Bitkit/Views/Widgets/WidgetEditView.swift +++ b/Bitkit/Views/Widgets/WidgetEditView.swift @@ -15,7 +15,6 @@ struct WidgetEditView: View { // View models for getting actual content @StateObject private var blocksViewModel = BlocksViewModel.shared - @StateObject private var factsViewModel = FactsViewModel.shared @StateObject private var newsViewModel = NewsViewModel.shared @StateObject private var priceViewModel = PriceViewModel.shared @StateObject private var weatherViewModel = WeatherViewModel.shared @@ -34,12 +33,10 @@ struct WidgetEditView: View { return WidgetEditItemFactory.getItems( for: id, blocksViewModel: blocksViewModel, - factsViewModel: factsViewModel, newsViewModel: newsViewModel, priceDataByPeriod: priceViewModel.dataByPeriod, weatherViewModel: weatherViewModel, blocksOptions: editLogic.blocksOptions, - factsOptions: editLogic.factsOptions, newsOptions: editLogic.newsOptions, priceOptions: editLogic.priceOptions, weatherOptions: editLogic.weatherOptions diff --git a/BitkitWidget/Assets.xcassets/bitcoin.imageset/Contents.json b/BitkitWidget/Assets.xcassets/bitcoin.imageset/Contents.json new file mode 100644 index 000000000..a64f61927 --- /dev/null +++ b/BitkitWidget/Assets.xcassets/bitcoin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bitcoin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/BitkitWidget/Assets.xcassets/bitcoin.imageset/bitcoin.pdf b/BitkitWidget/Assets.xcassets/bitcoin.imageset/bitcoin.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8085e5f487b0c98fefe28b59f67473616cc37838 GIT binary patch literal 6031 zcma)Ac|25K*e8kRM_LtMlswGp{s+Kcz%vln24n5oB zCFp$IeF^Ri9F#Ll78ge(`mh&({SiT?A&7(}(VU;6;ftfvb&y(!Wn*@P|I-8x2&NR6 zgNVf>1P6;b43I%P0-z)X-{vzMe1qx*2Gt0saMYC5pzmx{Wf6tS$(rFqgCB$|Ev_haA^T58Ak`J(g^S;;sIMz0}dy`gb00u zBTPu(XCnYlAwW#vvsl|GwAwsk4Oii4nRktuR-8n{ZfFy(r36gHC~$M{udX_2mBdwa zQsMIPFw&Lv30%0}@RC)7t@aO0JU>WC*`J$_53eAab z5xW7urr_U1k?0?tD{)6&u6YCea^f7B_VwxZUsE!AH8vfTSn(G5{4}D7Cj^DGpk;O2{-aQ4dW(- z@%XM9c_{WhTyHH*@{!j=-qmp4Q+mR0xk`>}VY#NleqG+Wl^b<=jcS<6W!}5t(Y>%z zZgM!clEAgY0u?Lmdc{+Y!TI@Y;Bwbi5e-+J6(R5-x9_VHJFtS+Fdi$gQE2t{)pxnu zS8dXJg2mYLw(9E|ez+Xnh{_Qv6N=H>(CFGIy@mH2PBd?|Sap)phN&=~UMzFdR-x>g z;a+E^n)9lCHQBxH-z9^2wuNPVvtWwyVt1T}?csZ(hqRHmhs(h>MXt|8XO7uq<#^fi z*<~ZqZlt`8lBZ)*j*SIQ8Z6 z6wb7;MSZKapvOttBi=rBb)=$Dz}f&<0QZ|Xv6_&F=f1Tw#VUk!V2H;bdM!q!N5#J% zou$nR=pQ?*=PS8#Ju{*>LefAmGsZVs$s}dF-1?YPijT2wl771j&%D*EunaZ}Hk6E~ zo5x-o+`&!!=f)ZOMS7yD!Hk)sWNO0x0neg{iqzn=PV=h0M-4Nv0QJmnR%L>8omkzGx`HHEX;-;W$KOe_*H-91!XC?dX!G_O zHq+M{XNEfvpX-^wB`4*IQx4O1^kQG>dh2>@{lTd0^Ec_1LkZ0?n~Ynp7Uej9LVF2L zDl^WU%o1sOd)Mdy_ZR5{2%kOs&D$I?mY9S04SrM<+4E+(;6a=*2+=bDx3((d|-M-|+}K)!0ip;VYjaV^T$1E*`j8b&+_H4}~CFd)@H7@4eG2(Uak6 zB9*B+Sywarv!$ADG!M02XhJm~%1pi5k@M3mE~oZY zbj$9h;pWulH?LiDALu`DnLEb)D2#aau~;ktQ3{q}Pm*KgmrL_AJBF{GgK_IbDEVgI_khOPJ!Pes)Nw1nGP zk!@!zO|&!jfTCCTSVE1#CvkvCU_EPmH+ z|F-jx(f)xup5s?u8yEPzApIzb^!eIm^ChYM%kAN!pKf0dBZ?@uGkhBZPWk1~x41Q@ z=s!1j?_hf7YIeVSt$%Ht^^~<+wu*JAhDzGGw7nW}2=syHuFiLuuZ-WE=(in|WMq{; zE#1aQEE_8I>&)#+FB`r;-s#Yk01KlF%&ua&$o_Ywa2dsv^K_P*-*Z@nBQI817T;w#=^2K zER#Z|hMLGX zukQ;g=zKtxZ&%bn1orQF1em)euvlai8mSq1Fr$vYte4C<3J^e*q-R#*-NeuKsTK-V`A@7WW@%_{NK@&kEGq(ga#7f9p+CCN* zdTqEX>u>kQz4FEV)Y_8oWK3aVDT+8u!8l@`U?}%aP`p{K&$|*d%en%NgrqyC%TPQc z$77{5n>2Yfj|3>RGko8?33mBCx#H&W=Hm?~I;)3uo#wDpHJ?XHyDE*Rj3cKHPxDU; zj4(QmTyZL79vb~vmG$$Y#mAjLc2cGb+CuYRl}yxe_i@*A6WbDsUWd={O>dobPRHtC z-|cU5x?yww=Gd?8-I>2@Mk2*xJ-5lKj}7JYeyo)1$$jrt^QjsC7Js2Oy)~kBVzzF! z@Rnm`$>k}Q-#fpdsNM}-j5Q2~G|F1IwPrG5Cd#fUn>;r#Y3I#zH8a;HtFTv9T>z_X)TD8${h&(+u(#_&dE^ryWPrXrl^+UBo z^35-A(r#TUe_7E@t@&*-ZdIOY^`Pz7>sQ`A9l@-3j>A5+9X=t^KQ(5$W)R(@Wwb%X zpCd8H0G^QW-K)L|8Vi|!9UY13*;_R?qyIkq!aD^aq|ms8td?9z-e~DeRQ)-V8q32n?#8*C zU|{xBvToO01!fZxHFaPjXsV!E&5IKIpg5!hC9eAG`=Mv$7p8wmywXYgUN`%8Tz0l) z{6bzsX#?PDS#>u=@cWD}8t|Xe6moks<=*rqU;N7#5ww7J*=F5XDRbc#;AL#$#WcBf zO@__t&N`R$lP@IBrS6T==GSBSbJG%)7;B~I2NGQvP4eR;)OT&dWT*pNCouM6vuV%k7hB6-zJ{h?r)a4&bKFCi@4imP(ReE3LOPM$gOS_=TU*4F#F; zDYsng<>Xq{MAnQ5NleasXZ7>AHX!1Jdgdl4r}sY4dElZuJ25)?!7#Zq!{WDg@U(x> z&wNLps=@Z*-$}ziYC{nZw1WeW9{uQ2Tk@si)$8u!e8jfF=E_T&qBoVRN9^qHK7Eqf z-FINzB>Gy&=oN$U$CJTfC2czX!~&`;Yj`lT%)T>_QZx}k6p~{_`JQrMJ<~*c7PMq1 zTq~~V>T|9%$c^-{>3Vs4IL~pU2b=J7NBt3izAjE)lWMW9;I-e2B3Acl3GB<48p-K9 zbM>_man6AkPxIL~^|>oK%j)c@zlY5@-F2ey^djrZ)Y|X)o;BimS~=^TY2l?wC%XjXQJcaeRI3D zA!43t`L}<{b`=K2A zo^I^Uw$eKy(U*|83-vk7WrJ4y9{*{Z;H_@3%7fA}GW!-sPYooVp+X`!t+SdP1O4 zwK;8!|9NIipIc-ftoD8=DJaib*S8(~&7F@|3Ib&wMPoU(3bW6QVJl2f3Tb?*4Y?=@qyZuIF-IUS$_onMvof0_r$0B8dUr3@t3R?GS zyY;HAFNaIM-4`3WH3CBj-m8w_p!|#wq88 zt%f~|qXOd6CzC03kZpm5b8m*TH7<0nYyldDLB#_!IJ5&TpjH5Zh%=xtp?kTa68rh{ zqN2P2{drqZ@yE0UTXAkQus=3GvZGTOcsj=p+QO9|-0jG8s9gkyHnjO2YSE=XEd0D+6^`R5CKsdH?L*ubJiobYlyf|zV66v&pP zUj6wOg5qU`qk5Z@-6`z95oiGZ^?+*tKvOuWwa}r2vomfUbR+@+02NCG+ySuF0%Zq8 zW<;d5cT429 zA;B=RzmOL(bycuZ|HjZ@#{7k0Q2&xsL92k(wOmdGjs6cg3|Inx$*KLDPZf;;^J2N2 zs@lJ8R#n4*tGZkcrKI#Pn^8)be`82>aKrj*u8IYrRBcqB^ zMxvdS)Rd7*NQ{!Q?0+EajRFDXEgZ6!Q^^QBVh{i=3Aj!)I#|%`MM9x4DA>-O#(Pcv E2fv|X=>Px# literal 0 HcmV?d00001 diff --git a/BitkitWidget/Assets.xcassets/btc.imageset/btc.pdf b/BitkitWidget/Assets.xcassets/btc.imageset/btc.pdf deleted file mode 100644 index c40a91328362b5fb2c56574a782e4ff114855f4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11533 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 TL7*)<6bgYNfP#XmdTRdxv>wJd diff --git a/BitkitWidget/BitkitWidget.swift b/BitkitWidget/BitkitWidget.swift index 48d2778c4..dfd5eb8f9 100644 --- a/BitkitWidget/BitkitWidget.swift +++ b/BitkitWidget/BitkitWidget.swift @@ -7,5 +7,6 @@ struct BitkitWidgetBundle: WidgetBundle { BitkitPriceWidget() BitkitNewsWidget() BitkitBlocksWidget() + BitkitFactsWidget() } } diff --git a/BitkitWidget/FactsHomeScreenWidget.swift b/BitkitWidget/FactsHomeScreenWidget.swift new file mode 100644 index 000000000..c51b1381d --- /dev/null +++ b/BitkitWidget/FactsHomeScreenWidget.swift @@ -0,0 +1,141 @@ +import SwiftUI +import WidgetKit + +// MARK: - Entry + +struct FactsWidgetEntry: TimelineEntry { + let date: Date + let fact: String +} + +// MARK: - Timeline Provider + +struct FactsWidgetProvider: TimelineProvider { + private static let refreshInterval: TimeInterval = 2 * 60 + + func placeholder(in _: Context) -> FactsWidgetEntry { + FactsWidgetEntry(date: Date(), fact: BitcoinFacts.all[0]) + } + + func getSnapshot(in _: Context, completion: @escaping (FactsWidgetEntry) -> Void) { + completion(entry(at: Date())) + } + + func getTimeline(in _: Context, completion: @escaping (Timeline) -> Void) { + let now = Date() + let nextRefresh = now.addingTimeInterval(Self.refreshInterval) + completion(Timeline(entries: [entry(at: now)], policy: .after(nextRefresh))) + } + + private func entry(at date: Date) -> FactsWidgetEntry { + let bucket = Int(date.timeIntervalSince1970 / Self.refreshInterval) + let index = abs(bucket) % BitcoinFacts.all.count + return FactsWidgetEntry(date: date, fact: BitcoinFacts.all[index]) + } +} + +// MARK: - View + +struct FactsHomeScreenWidgetEntryView: View { + @Environment(\.widgetFamily) var widgetFamily + @Environment(\.widgetRenderingMode) var widgetRenderingMode + + var entry: FactsWidgetProvider.Entry + + var body: some View { + content + .containerBackground(for: .widget) { backgroundView } + } + + @ViewBuilder + private var content: some View { + switch widgetFamily { + case .systemSmall: + compactLayout + default: + wideLayout + } + } + + private var compactLayout: some View { + Text(entry.fact) + .font(Fonts.semiBold(size: 17)) + .foregroundColor(textColor) + .lineLimit(4) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .overlay(alignment: .bottomTrailing) { + bitcoinLogo + } + .widgetAccentable() + } + + private var wideLayout: some View { + HStack(alignment: .top, spacing: 32) { + Text(entry.fact) + .font(Fonts.bold(size: 22)) + .foregroundColor(textColor) + .lineLimit(4) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity, alignment: .topLeading) + .widgetAccentable() + + bitcoinLogo + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + private var bitcoinLogo: some View { + Group { + if widgetRenderingMode == .fullColor { + ZStack { + Circle() + .fill(Color.bitcoin) + + bitcoinGlyph + .foregroundColor(.white) + } + } else { + ZStack { + Circle() + .fill(Color.white) + + bitcoinGlyph + .blendMode(.destinationOut) + } + .compositingGroup() + } + } + .frame(width: 32, height: 32) + } + + private var bitcoinGlyph: some View { + Image("bitcoin") + .resizable() + .renderingMode(.template) + } + + private var backgroundView: some View { + widgetRenderingMode == .fullColor ? Color.gray6 : Color.clear + } + + private var textColor: Color { + widgetRenderingMode == .fullColor ? .white : .primary + } +} + +// MARK: - Widget Configuration + +struct BitkitFactsWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration( + kind: "BitkitFactsWidget", + provider: FactsWidgetProvider() + ) { entry in + FactsHomeScreenWidgetEntryView(entry: entry) + } + .configurationDisplayName("widgets__facts__name") + .description("widgets__facts__description") + .supportedFamilies([.systemSmall, .systemMedium]) + } +}