diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index b01a6db17..8cbd5f0bb 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -109,6 +109,44 @@ ); target = 4A319B4E2E8F24F2002B9AC9 /* BitkitWidgetExtension */; }; + 4A319B672E8F24F5002B9AC9 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Components/Widgets/BlocksWidgetContent.swift, + Components/Widgets/FactsWidgetContent.swift, + Components/Widgets/NewsWidgetContent.swift, + Components/Widgets/PriceWidgetContent.swift, + Components/Widgets/WeatherWidgetContent.swift, + Components/Widgets/WidgetPalette.swift, + Constants/WidgetEnv.swift, + "Fonts/InterTight-Black.ttf", + "Fonts/InterTight-Bold.ttf", + "Fonts/InterTight-ExtraBold.ttf", + "Fonts/InterTight-Medium.ttf", + "Fonts/InterTight-Regular.ttf", + "Fonts/InterTight-SemiBold.ttf", + Models/BitcoinFacts.swift, + Models/BlocksWidgetData.swift, + Models/BlocksWidgetFields.swift, + Models/BlocksWidgetOptions.swift, + Models/NewsWidgetData.swift, + Models/NewsWidgetOptions.swift, + Models/PriceWidgetData.swift, + Models/PriceWidgetOptions.swift, + Models/WeatherWidgetData.swift, + Models/WeatherWidgetOptions.swift, + Services/Widgets/BlocksHomeScreenWidgetOptionsStore.swift, + Services/Widgets/MempoolWeatherAPI.swift, + Services/Widgets/NewsHomeScreenWidgetOptionsStore.swift, + Services/Widgets/PriceHomeScreenWidgetOptionsStore.swift, + Services/Widgets/WeatherHomeScreenWidgetOptionsStore.swift, + Styles/Colors.swift, + Styles/Fonts.swift, + Styles/TextStyle.swift, + Utilities/LocalizeHelpers.swift, + ); + target = 4A319B4E2E8F24F2002B9AC9 /* BitkitWidgetExtension */; + }; 96A44F3D2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -123,8 +161,8 @@ Extensions/HexBytes.swift, "Extensions/LDKNode+AddressType.swift", Extensions/PaymentDetails.swift, - Models/BlocktankNotificationType.swift, Models/BlocksWidgetOptions.swift, + Models/BlocktankNotificationType.swift, Models/LnPeer.swift, Models/PubkyPublicKeyFormat.swift, Models/Toast.swift, @@ -180,39 +218,6 @@ ); target = 961058DB2C355B5500E1F1D8 /* BitkitNotification */; }; - 4A319B672E8F24F5002B9AC9 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Fonts/InterTight-Black.ttf, - Fonts/InterTight-Bold.ttf, - Fonts/InterTight-ExtraBold.ttf, - Fonts/InterTight-Medium.ttf, - Fonts/InterTight-Regular.ttf, - Constants/WidgetEnv.swift, - Fonts/InterTight-SemiBold.ttf, - Components/Widgets/WeatherWidgetContent.swift, - Models/BlocksWidgetData.swift, - Models/BlocksWidgetFields.swift, - Models/BlocksWidgetOptions.swift, - Models/BitcoinFacts.swift, - Models/NewsWidgetData.swift, - Models/NewsWidgetOptions.swift, - Models/PriceWidgetData.swift, - Models/PriceWidgetOptions.swift, - Models/WeatherWidgetData.swift, - Models/WeatherWidgetOptions.swift, - Services/Widgets/BlocksHomeScreenWidgetOptionsStore.swift, - Services/Widgets/MempoolWeatherAPI.swift, - Services/Widgets/NewsHomeScreenWidgetOptionsStore.swift, - Services/Widgets/PriceHomeScreenWidgetOptionsStore.swift, - Services/Widgets/WeatherHomeScreenWidgetOptionsStore.swift, - Styles/Colors.swift, - Styles/Fonts.swift, - Styles/TextStyle.swift, - Utilities/LocalizeHelpers.swift, - ); - target = 4A319B4E2E8F24F2002B9AC9 /* BitkitWidgetExtension */; - }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -554,31 +559,6 @@ }; /* 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; @@ -663,6 +643,31 @@ }; /* End PBXTargetDependency 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 XCBuildConfiguration section */ 4A319B632E8F24F4002B9AC9 /* Debug */ = { isa = XCBuildConfiguration; @@ -684,7 +689,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 2.2.1; - PRODUCT_BUNDLE_IDENTIFIER = "to.bitkit.widget"; + PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -717,7 +722,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 2.2.1; - PRODUCT_BUNDLE_IDENTIFIER = "to.bitkit.widget"; + PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1143,7 +1148,7 @@ repositoryURL = "https://github.com/pubky/paykit-rs"; requirement = { kind = exactVersion; - version = 0.1.0-rc5; + version = "0.1.0-rc5"; }; }; 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = { diff --git a/BitkitWidget/Assets.xcassets/bitcoin.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/bitcoin-symbol.imageset/Contents.json similarity index 82% rename from BitkitWidget/Assets.xcassets/bitcoin.imageset/Contents.json rename to Bitkit/Assets.xcassets/icons/bitcoin-symbol.imageset/Contents.json index a64f61927..27cf96d8c 100644 --- a/BitkitWidget/Assets.xcassets/bitcoin.imageset/Contents.json +++ b/Bitkit/Assets.xcassets/icons/bitcoin-symbol.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "bitcoin.pdf", + "filename" : "bitcoin-symbol.pdf", "idiom" : "universal" } ], diff --git a/BitkitWidget/Assets.xcassets/bitcoin.imageset/bitcoin.pdf b/Bitkit/Assets.xcassets/icons/bitcoin-symbol.imageset/bitcoin-symbol.pdf similarity index 100% rename from BitkitWidget/Assets.xcassets/bitcoin.imageset/bitcoin.pdf rename to Bitkit/Assets.xcassets/icons/bitcoin-symbol.imageset/bitcoin-symbol.pdf diff --git a/Bitkit/Components/Widgets/BlocksWidget.swift b/Bitkit/Components/Widgets/BlocksWidget.swift index 3344d0ae5..faae7a4d0 100644 --- a/Bitkit/Components/Widgets/BlocksWidget.swift +++ b/Bitkit/Components/Widgets/BlocksWidget.swift @@ -42,78 +42,11 @@ struct BlocksWidget: View { WidgetContentBuilder.errorView(t("widgets__blocks__error")) } else if let data = viewModel.blockData { BlocksWidgetWideContent(data: data, options: options) + .frame(height: BlocksWidgetWideContent.inAppContentHeight) } } } -// MARK: - Wide layout (in-app + 343-wide carousel page + .systemMedium / .systemLarge OS widget) - -struct BlocksWidgetWideContent: View { - let data: CachedBlock - let options: BlocksWidgetOptions - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - ForEach(options.enabledFields, id: \.self) { field in - BlocksWidgetWideRow(field: field, value: field.value(from: data)) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } -} - -private struct BlocksWidgetWideRow: View { - let field: BlocksWidgetField - let value: String - - var body: some View { - HStack(alignment: .center, spacing: 8) { - Image(field.iconName) - .resizable() - .renderingMode(.template) - .foregroundColor(.brandAccent) - .frame(width: 20, height: 20) - - BodyMText(field.label, textColor: .white80) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) - - BodyMSBText(value) - .lineLimit(1) - .truncationMode(.middle) - } - } -} - -// MARK: - Compact layout (small carousel preview + 163×192 OS small widget) - -struct BlocksWidgetCompactContent: View { - let data: CachedBlock - let options: BlocksWidgetOptions - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - ForEach(options.enabledFields, id: \.self) { field in - HStack(alignment: .center, spacing: 8) { - Image(field.iconName) - .resizable() - .renderingMode(.template) - .foregroundColor(.brandAccent) - .frame(width: 20, height: 20) - - BodySSBText(field.value(from: data)) - .lineLimit(1) - .truncationMode(.middle) - } - } - } - .padding(16) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background(Color.gray6) - .cornerRadius(16) - } -} - #Preview { BlocksWidget() .padding() diff --git a/Bitkit/Components/Widgets/BlocksWidgetContent.swift b/Bitkit/Components/Widgets/BlocksWidgetContent.swift new file mode 100644 index 000000000..1a06f9612 --- /dev/null +++ b/Bitkit/Components/Widgets/BlocksWidgetContent.swift @@ -0,0 +1,95 @@ +import SwiftUI +import WidgetKit + +// Shared Bitcoin Blocks widget content, reused by the in-app feed, the carousel preview, and the +// home-screen WidgetKit extension. Colors adapt to `widgetRenderingMode` via ``WidgetPalette``. + +// MARK: - Wide layout (in-app + 343-wide carousel page + .systemMedium / .systemLarge OS widget) + +struct BlocksWidgetWideContent: View { + static let inAppContentHeight: CGFloat = 124 + + let data: CachedBlock + let options: BlocksWidgetOptions + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(options.enabledFields.enumerated()), id: \.element) { index, field in + if index > 0 { + Spacer(minLength: 8) + } + BlocksWidgetWideRow(field: field, value: field.value(from: data), palette: palette) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} + +private struct BlocksWidgetWideRow: View { + let field: BlocksWidgetField + let value: String + let palette: WidgetPalette + + var body: some View { + HStack(alignment: .center, spacing: 8) { + BlocksWidgetIcon(field: field, palette: palette) + + BodyMText(field.label, textColor: palette.label) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + BodyMSBText(value, textColor: palette.title) + .lineLimit(1) + .truncationMode(.middle) + .widgetAccentable() + } + } +} + +// MARK: - Compact layout (small carousel preview + .systemSmall OS widget) + +struct BlocksWidgetCompactContent: View { + let data: CachedBlock + let options: BlocksWidgetOptions + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(options.enabledFields.enumerated()), id: \.element) { index, field in + if index > 0 { + Spacer(minLength: 8) + } + HStack(alignment: .center, spacing: 8) { + BlocksWidgetIcon(field: field, palette: palette) + + BodySSBText(field.value(from: data), textColor: palette.title) + .lineLimit(1) + .truncationMode(.middle) + .widgetAccentable() + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} + +// MARK: - Shared row icon + +private struct BlocksWidgetIcon: View { + let field: BlocksWidgetField + let palette: WidgetPalette + + var body: some View { + Image(field.iconName) + .resizable() + .renderingMode(.template) + .foregroundColor(palette.accent) + .frame(width: 20, height: 20) + .widgetAccentable() + } +} diff --git a/Bitkit/Components/Widgets/FactsWidget.swift b/Bitkit/Components/Widgets/FactsWidget.swift index b4f43db9f..f2c8f5652 100644 --- a/Bitkit/Components/Widgets/FactsWidget.swift +++ b/Bitkit/Components/Widgets/FactsWidget.swift @@ -25,48 +25,6 @@ struct FactsWidget: View { } } -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() diff --git a/Bitkit/Components/Widgets/FactsWidgetContent.swift b/Bitkit/Components/Widgets/FactsWidgetContent.swift new file mode 100644 index 000000000..35eb73c51 --- /dev/null +++ b/Bitkit/Components/Widgets/FactsWidgetContent.swift @@ -0,0 +1,85 @@ +import SwiftUI +import WidgetKit + +// Shared Bitcoin Facts widget content, reused by the in-app feed, the carousel preview, and the +// home-screen WidgetKit extension. Colors and the Bitcoin badge adapt to `widgetRenderingMode` +// via ``WidgetPalette``. Card chrome is supplied by the caller. + +// MARK: - Wide layout (in-app + 343-wide carousel page + .systemMedium OS widget) + +struct FactsWidgetWideContent: View { + let fact: String + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + HStack(alignment: .top, spacing: 32) { + TitleText(fact, textColor: palette.title) + .lineLimit(4) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity, alignment: .leading) + .widgetAccentable() + + BitcoinLogo() + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +// MARK: - Compact layout (small carousel preview + .systemSmall OS widget) + +struct FactsWidgetCompactContent: View { + let fact: String + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + BodyMSBText(fact, textColor: palette.title) + .lineLimit(4) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .widgetAccentable() + .overlay(alignment: .bottomTrailing) { + BitcoinLogo() + } + } +} + +// MARK: - Bitcoin badge + +struct BitcoinLogo: View { + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + Group { + if renderingMode == .fullColor { + ZStack { + Circle() + .fill(Color.bitcoin) + + glyph + .foregroundColor(.white) + } + } else { + ZStack { + Circle() + .fill(Color.white) + + glyph + .blendMode(.destinationOut) + } + .compositingGroup() + } + } + .frame(width: 32, height: 32) + .widgetAccentable() + } + + private var glyph: some View { + Image("bitcoin-symbol") + .resizable() + .renderingMode(.template) + } +} diff --git a/Bitkit/Components/Widgets/NewsWidget.swift b/Bitkit/Components/Widgets/NewsWidget.swift index b111e1492..1ba8a3f4f 100644 --- a/Bitkit/Components/Widgets/NewsWidget.swift +++ b/Bitkit/Components/Widgets/NewsWidget.swift @@ -44,75 +44,17 @@ struct NewsWidget: View { } else if viewModel.error != nil { WidgetContentBuilder.errorView(t("widgets__news__error")) } else if let data = viewModel.widgetData { - NewsWidgetWideContent(data: data, options: options) + NewsWidgetWideContent( + title: data.title, + publisher: data.publisher, + timeAgo: data.timeAgo, + options: options + ) + .frame(height: NewsWidgetWideContent.inAppContentHeight) } } } -// MARK: - Wide layout (in-app + 343-wide carousel page) - -struct NewsWidgetWideContent: View { - let data: WidgetData - let options: NewsWidgetOptions - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - if options.showTitle { - TitleText(data.title) - .lineLimit(4) - .frame(maxWidth: .infinity, alignment: .leading) - } - - if options.showSource || options.showDate { - HStack(alignment: .center, spacing: 8) { - if options.showSource { - BodySSBText(data.publisher, textColor: .brandAccent) - .lineLimit(1) - } - Spacer(minLength: 0) - if options.showDate { - BodySSBText(data.timeAgo, textColor: .textSecondary) - .lineLimit(1) - } - } - .frame(maxWidth: .infinity) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } -} - -// MARK: - Compact layout (small carousel preview + 163×192 OS widget) - -struct NewsWidgetCompactContent: View { - let data: WidgetData - let options: NewsWidgetOptions - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - if options.showTitle { - TitleText(data.title) - .lineLimit(4) - .frame(maxWidth: .infinity, alignment: .leading) - } - - Spacer(minLength: 8) - - if options.showDate { - HStack { - Spacer(minLength: 0) - BodySSBText(data.timeAgo, textColor: .textSecondary) - .lineLimit(1) - } - } - } - .padding(16) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background(Color.gray6) - .cornerRadius(16) - } -} - #Preview { NewsWidget() .padding() diff --git a/Bitkit/Components/Widgets/NewsWidgetContent.swift b/Bitkit/Components/Widgets/NewsWidgetContent.swift new file mode 100644 index 000000000..df5ed088f --- /dev/null +++ b/Bitkit/Components/Widgets/NewsWidgetContent.swift @@ -0,0 +1,81 @@ +import SwiftUI +import WidgetKit + +// MARK: - Wide layout (in-app + 343-wide carousel page + .systemMedium OS widget) + +struct NewsWidgetWideContent: View { + static let inAppContentHeight: CGFloat = 86 + + let title: String + let publisher: String + let timeAgo: String + let options: NewsWidgetOptions + var titleLineLimit: Int = 2 + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + VStack(alignment: .leading, spacing: 0) { + if options.showTitle { + TitleText(title, textColor: palette.title) + .lineLimit(titleLineLimit) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity, alignment: .leading) + .widgetAccentable() + } + + Spacer(minLength: 0) + + if options.showSource || options.showDate { + HStack(alignment: .center, spacing: 8) { + if options.showSource { + BodySSBText(publisher, textColor: palette.accent) + .lineLimit(1) + } + Spacer(minLength: 0) + if options.showDate { + BodySSBText(timeAgo, textColor: palette.secondary) + .lineLimit(1) + } + } + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} + +// MARK: - Compact layout (small carousel preview + .systemSmall OS widget) + +struct NewsWidgetCompactContent: View { + let title: String + let timeAgo: String + let options: NewsWidgetOptions + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + VStack(alignment: .leading, spacing: 0) { + if options.showTitle { + TitleText(title, textColor: palette.title) + .lineLimit(4) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity, alignment: .leading) + .widgetAccentable() + } + + Spacer(minLength: 8) + + if options.showDate { + HStack { + Spacer(minLength: 0) + BodySSBText(timeAgo, textColor: palette.secondary) + .lineLimit(1) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index eb75c65fc..da82ac30e 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -1,4 +1,3 @@ -import Charts import SwiftUI /// Displays Bitcoin price for the user's selected trading pair and timeframe. @@ -55,135 +54,6 @@ struct PriceWidget: View { } } -// MARK: - Wide layout (in-app + carousel page) - -struct PriceWidgetWideContent: View { - let data: PriceData - let period: GraphPeriod - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - VStack(alignment: .leading, spacing: 4) { - HStack(alignment: .center, spacing: 16) { - CaptionMText("\(data.name) \(period.rawValue)", textColor: .textSecondary) - .textCase(.uppercase) - .frame(maxWidth: .infinity, alignment: .leading) - - TitleText( - data.change.formatted, - textColor: data.change.isPositive ? .greenAccent : .redAccent - ) - .lineLimit(1) - .accessibilityIdentifier("price_card_pair_change_\(data.name)") - } - .accessibilityIdentifier("PriceWidgetRow-\(data.name)") - - Text(data.price) - .font(Fonts.bold(size: 34)) - .foregroundColor(.textPrimary) - .lineLimit(1) - .minimumScaleFactor(0.7) - .frame(maxWidth: .infinity, alignment: .leading) - .accessibilityIdentifier("price_card_pair_price_\(data.name)") - } - - PriceChart(values: data.pastValues, isPositive: data.change.isPositive) - .frame(height: 48) - .accessibilityIdentifier("price_card_chart") - } - .frame(maxWidth: .infinity, alignment: .leading) - } -} - -// MARK: - Compact layout (small carousel preview only) - -struct PriceWidgetCompactContent: View { - let data: PriceData - let period: GraphPeriod - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 0) { - CaptionMText(data.name, textColor: .textSecondary) - .textCase(.uppercase) - Spacer(minLength: 0) - CaptionMText(period.rawValue, textColor: .textSecondary) - .textCase(.uppercase) - } - .accessibilityIdentifier("price_card_small_pair_row_\(data.name)") - - Text(data.price) - .font(Fonts.bold(size: 22)) - .foregroundColor(.textPrimary) - .lineLimit(1) - .minimumScaleFactor(0.7) - .accessibilityIdentifier("price_card_small_pair_price_\(data.name)") - - BodySSBText( - data.change.formatted, - textColor: data.change.isPositive ? .greenAccent : .redAccent - ) - .lineLimit(1) - .accessibilityIdentifier("price_card_small_pair_change_\(data.name)") - } - - PriceChart(values: data.pastValues, isPositive: data.change.isPositive) - .frame(height: 64) - .accessibilityIdentifier("price_card_small_chart") - } - .padding(16) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background(Color.gray6) - .cornerRadius(16) - } -} - -// MARK: - Chart - -struct PriceChart: View { - let values: [Double] - let isPositive: Bool - - private let lineWidth: CGFloat = 1.3 - - private var normalizedValues: [Double] { - guard values.count > 1 else { return values } - - let minValue = values.min() ?? 0 - let maxValue = values.max() ?? 0 - let range = maxValue - minValue - - guard range > 0 else { return values.map { _ in 0.5 } } - - return values.map { value in - let normalized = (value - minValue) / range - return 0.15 + (normalized * 0.7) - } - } - - private var lineColor: Color { - isPositive ? .greenAccent : .redAccent - } - - var body: some View { - Chart { - ForEach(Array(normalizedValues.enumerated()), id: \.offset) { index, value in - LineMark( - x: .value("Index", index), - y: .value("Price", value) - ) - .foregroundStyle(lineColor) - .lineStyle(StrokeStyle(lineWidth: lineWidth)) - .interpolationMethod(.catmullRom) - } - } - .chartXAxis(.hidden) - .chartYAxis(.hidden) - .chartYScale(domain: 0.1 ... 0.9) - } -} - #Preview { PriceWidget() .padding() diff --git a/Bitkit/Components/Widgets/PriceWidgetContent.swift b/Bitkit/Components/Widgets/PriceWidgetContent.swift new file mode 100644 index 000000000..3bfea47ca --- /dev/null +++ b/Bitkit/Components/Widgets/PriceWidgetContent.swift @@ -0,0 +1,151 @@ +import Charts +import SwiftUI +import WidgetKit + +// Shared Bitcoin Price widget content, reused by the in-app feed, the carousel preview, and the +// home-screen WidgetKit extension. Colors adapt to `widgetRenderingMode` via ``WidgetPalette``. +// +// Card chrome (padding/background/corner) is supplied by the caller, not here. + +// MARK: - Wide layout (in-app + 343-wide carousel page + .systemMedium OS widget) + +struct PriceWidgetWideContent: View { + let data: PriceData + let period: GraphPeriod + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 16) { + CaptionMText("\(data.name) \(period.rawValue)", textColor: palette.secondary) + .textCase(.uppercase) + .frame(maxWidth: .infinity, alignment: .leading) + + TitleText( + data.change.formatted, + textColor: palette.data(data.change.isPositive ? .greenAccent : .redAccent) + ) + .lineLimit(1) + .widgetAccentable() + .accessibilityIdentifier("price_card_pair_change_\(data.name)") + } + .accessibilityIdentifier("PriceWidgetRow-\(data.name)") + + Text(data.price) + .font(Fonts.bold(size: 34)) + .foregroundColor(palette.title) + .lineLimit(1) + .minimumScaleFactor(0.7) + .frame(maxWidth: .infinity, alignment: .leading) + .widgetAccentable() + .accessibilityIdentifier("price_card_pair_price_\(data.name)") + } + + PriceChart(values: data.pastValues, isPositive: data.change.isPositive, renderingMode: renderingMode) + .frame(height: 48) + .widgetAccentable() + .accessibilityIdentifier("price_card_chart") + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +// MARK: - Compact layout (small carousel preview + .systemSmall OS widget) + +struct PriceWidgetCompactContent: View { + let data: PriceData + let period: GraphPeriod + + @Environment(\.widgetRenderingMode) private var renderingMode + + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 0) { + CaptionMText(data.name, textColor: palette.secondary) + .textCase(.uppercase) + Spacer(minLength: 0) + CaptionMText(period.rawValue, textColor: palette.secondary) + .textCase(.uppercase) + } + .accessibilityIdentifier("price_card_small_pair_row_\(data.name)") + + Text(data.price) + .font(Fonts.bold(size: 22)) + .foregroundColor(palette.title) + .lineLimit(1) + .minimumScaleFactor(0.7) + .widgetAccentable() + .accessibilityIdentifier("price_card_small_pair_price_\(data.name)") + + BodySSBText( + data.change.formatted, + textColor: palette.data(data.change.isPositive ? .greenAccent : .redAccent) + ) + .lineLimit(1) + .widgetAccentable() + .accessibilityIdentifier("price_card_small_pair_change_\(data.name)") + } + + Spacer(minLength: 8) + + PriceChart(values: data.pastValues, isPositive: data.change.isPositive, renderingMode: renderingMode) + .frame(height: 64) + .widgetAccentable() + .accessibilityIdentifier("price_card_small_chart") + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} + +// MARK: - Chart + +struct PriceChart: View { + let values: [Double] + let isPositive: Bool + /// Defaults to `.fullColor` so non-widget callers render the colored line. + var renderingMode: WidgetRenderingMode = .fullColor + + private let lineWidth: CGFloat = 1.3 + + private var normalizedValues: [Double] { + guard values.count > 1 else { return values } + + let minValue = values.min() ?? 0 + let maxValue = values.max() ?? 0 + let range = maxValue - minValue + + guard range > 0 else { return values.map { _ in 0.5 } } + + return values.map { value in + let normalized = (value - minValue) / range + return 0.15 + (normalized * 0.7) + } + } + + private var lineColor: Color { + guard renderingMode == .fullColor else { return .primary } + return isPositive ? .greenAccent : .redAccent + } + + var body: some View { + Chart { + ForEach(Array(normalizedValues.enumerated()), id: \.offset) { index, value in + LineMark( + x: .value("Index", index), + y: .value("Price", value) + ) + .foregroundStyle(lineColor) + .lineStyle(StrokeStyle(lineWidth: lineWidth)) + .interpolationMethod(.catmullRom) + } + } + .chartXAxis(.hidden) + .chartYAxis(.hidden) + .chartYScale(domain: 0.1 ... 0.9) + } +} diff --git a/Bitkit/Components/Widgets/WeatherWidgetContent.swift b/Bitkit/Components/Widgets/WeatherWidgetContent.swift index 793e8958e..82673aa5f 100644 --- a/Bitkit/Components/Widgets/WeatherWidgetContent.swift +++ b/Bitkit/Components/Widgets/WeatherWidgetContent.swift @@ -44,12 +44,15 @@ struct WeatherFeeMetric: View { /// in the space-constrained compact widget. var compactLabel: Bool = false + @Environment(\.widgetRenderingMode) private var renderingMode + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) VStack(alignment: .leading, spacing: 4) { - labelView + labelView(palette: palette) Text(value) .font(Fonts.bold(size: valueSize)) - .foregroundColor(valueColor) + .foregroundColor(palette.data(valueColor)) .kerning(-1) .lineLimit(1) .minimumScaleFactor(0.9) @@ -58,14 +61,14 @@ struct WeatherFeeMetric: View { } @ViewBuilder - private var labelView: some View { + private func labelView(palette: WidgetPalette) -> some View { if compactLabel { - FootnoteText(label, textColor: .white64) + FootnoteText(label, textColor: palette.secondary) .textCase(.uppercase) .lineLimit(1) .minimumScaleFactor(0.7) } else { - CaptionMText(label, textColor: .white64) + CaptionMText(label, textColor: palette.secondary) .textCase(.uppercase) .lineLimit(1) } @@ -79,12 +82,15 @@ struct WeatherWidgetWideContent: View { let conditionDescription: String let metricLabel: String + @Environment(\.widgetRenderingMode) private var renderingMode + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) HStack(alignment: .top, spacing: 16) { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 4) { - SubtitleText(conditionTitle, textColor: .white) - BodySText(conditionDescription, textColor: .white80) + SubtitleText(conditionTitle, textColor: palette.title) + BodySText(conditionDescription, textColor: palette.label) .lineLimit(2) .fixedSize(horizontal: false, vertical: true) } @@ -112,14 +118,17 @@ struct WeatherWidgetCompactContent: View { let conditionTitle: String let metricLabel: String + @Environment(\.widgetRenderingMode) private var renderingMode + var body: some View { + let palette = WidgetPalette(renderingMode: renderingMode) VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading) { Text(data.condition.icon) .font(.system(size: 58)) .minimumScaleFactor(0.85) .widgetAccentable() - SubtitleText(conditionTitle, textColor: .white) + SubtitleText(conditionTitle, textColor: palette.title) .lineLimit(1) .minimumScaleFactor(0.6) } diff --git a/Bitkit/Components/Widgets/WidgetPalette.swift b/Bitkit/Components/Widgets/WidgetPalette.swift new file mode 100644 index 000000000..9c98e029a --- /dev/null +++ b/Bitkit/Components/Widgets/WidgetPalette.swift @@ -0,0 +1,48 @@ +import SwiftUI +import WidgetKit + +/// Centralizes the color convention shared between the in-app widget feed and the home-screen +/// WidgetKit extension, so a single content view renders correctly in full color **and** in +/// tinted/monochrome (Smart Stack) rendering. +/// +/// Outside of a widget, `widgetRenderingMode` defaults to `.fullColor`, so in-app widgets get the +/// dark-theme palette automatically. In tinted/accented modes colors collapse to `.primary`/ +/// `.secondary` so WidgetKit can recolor them, and the container background becomes clear. +struct WidgetPalette { + let renderingMode: WidgetRenderingMode + + var isFullColor: Bool { + renderingMode == .fullColor + } + + /// Titles and primary values. + var title: Color { + isFullColor ? .white : .primary + } + + /// Strong secondary labels (e.g. Blocks field labels, weather description). + var label: Color { + isFullColor ? .white80 : .secondary + } + + /// Subtle metadata (price pair/period, news date, metric caption). + var secondary: Color { + isFullColor ? .white64 : .secondary + } + + /// Icons and accent text (block icons, news source). + var accent: Color { + isFullColor ? .brandAccent : .primary + } + + /// Container background — clear in tinted mode so the wallpaper shows through. + var background: Color { + isFullColor ? .gray6 : .clear + } + + /// Data-driven colors (green/red change, weather condition). Falls back to `.primary` in + /// tinted mode so the system can recolor consistently. + func data(_ color: Color) -> Color { + isFullColor ? color : .primary + } +} diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 872c04695..b99282662 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -28,3 +28,34 @@ "wallet__drawer__support" = "الدعم"; "wallet__recipient_contact" = "جهة الاتصال"; "wallet__activity_contact" = "جهة الاتصال"; +"widgets__onboarding__title" = "مرحبًا،\nالأدوات"; +"widgets__onboarding__description" = "استمتع بموجزات لامركزية من خدمات الويب المفضلة لديك، بإضافة أدوات ممتعة ومفيدة إلى محفظة Bitkit."; +"widgets__widget__nav_title" = "الأداة"; +"widgets__widget__edit" = "موجز الأداة"; +"widgets__widget__edit_default" = "افتراضي"; +"widgets__widget__edit_custom" = "مخصص"; +"widgets__widget__edit_description" = "يرجى اختيار الحقول التي تريد عرضها في أداة {name}."; +"widgets__widget__source" = "المصدر"; +"widgets__add" = "إضافة أداة"; +"widgets__delete__title" = "حذف الأداة؟"; +"widgets__delete__description" = "هل أنت متأكد من رغبتك في حذف '{name}' من أدواتك؟"; +"widgets__price__name" = "سعر Bitcoin"; +"widgets__price__description" = "تحقق من أحدث أسعار صرف Bitcoin لمجموعة متنوعة من العملات."; +"widgets__news__name" = "عناوين Bitcoin"; +"widgets__news__description" = "اقرأ أحدث وأفضل عناوين Bitcoin من مصادر إخبارية متنوعة."; +"widgets__blocks__name" = "كتل Bitcoin"; +"widgets__blocks__description" = "افحص إحصائيات متنوعة عن كتل Bitcoin المُعدّنة حديثًا."; +"widgets__facts__name" = "حقائق Bitcoin"; +"widgets__facts__description" = "اكتشف حقائق ممتعة عن Bitcoin، في كل مرة تفتح محفظتك."; +"widgets__calculator__name" = "حاسبة Bitcoin"; +"widgets__calculator__description" = "حوّل مبالغ ₿ إلى {fiatSymbol} أو العكس."; +"widgets__weather__name" = "طقس Bitcoin"; +"widgets__weather__description" = "اكتشف متى يكون الوقت مناسبًا للتعامل على سلسلة Bitcoin."; +"widgets__weather__condition__good__title" = "ظروف مواتية"; +"widgets__weather__condition__good__description" = "كل شيء واضح. الآن وقت جيد للتعامل على السلسلة."; +"widgets__weather__condition__average__title" = "ظروف متوسطة"; +"widgets__weather__condition__average__description" = "معدل الكتلة التالية قريب من المتوسطات الشهرية."; +"widgets__weather__condition__poor__title" = "ظروف سيئة"; +"widgets__weather__condition__poor__description" = "إذا لم تكن في عجلة للتعامل، قد يكون من الأفضل الانتظار قليلاً."; +"widgets__weather__current_fee" = "متوسط الرسوم الحالي"; +"widgets__weather__next_block" = "تضمين الكتلة التالية"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 4df41828d..168f21d1d 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -978,7 +978,6 @@ "wallet__balance_hidden_message" = "Llisca el saldo de la teva cartera per revelar-lo de nou."; "wallet__balance_unit_switched_title" = "Canviat a {unit}"; "wallet__balance_unit_switched_message" = "Toca el saldo de la teva cartera per tornar-lo a {unit}."; -"widgets__widgets" = "Ginys"; "widgets__onboarding__title" = "Hola,\nGinys"; "widgets__onboarding__description" = "Gaudeix de fonts descentralitzades dels teus serveis web preferits afegint ginys divertits i útils a la teva cartera Bitkit."; "widgets__widget__nav_title" = "Giny"; @@ -991,12 +990,20 @@ "widgets__delete__title" = "Eliminar giny?"; "widgets__delete__description" = "Estàs segur que vols eliminar \'{name}\' dels teus ginys?"; "widgets__price__name" = "Preu de Bitcoin"; +"widgets__price__description" = "Comprova els últims tipus de canvi de Bitcoin per a diverses monedes fiduciàries."; "widgets__price__error" = "No s\'han pogut obtenir les dades de preu"; "widgets__news__name" = "Capçaleres de Bitcoin"; +"widgets__news__description" = "Llegeix les últimes i millors notícies de Bitcoin de diversos llocs de notícies."; "widgets__news__error" = "No s\'han pogut obtenir les últimes notícies"; "widgets__blocks__name" = "Blocs de Bitcoin"; +"widgets__blocks__description" = "Examina diverses estadístiques dels blocs de Bitcoin recentment minats."; "widgets__blocks__error" = "No s\'han pogut obtenir les dades dels blocs"; "widgets__facts__name" = "Bitcoin Fets"; +"widgets__facts__description" = "Descobreix fets interessants sobre Bitcoin cada vegada que obres la cartera."; +"widgets__calculator__name" = "Calculadora de Bitcoin"; +"widgets__calculator__description" = "Converteix quantitats de ₿ a {fiatSymbol} o viceversa."; +"widgets__weather__name" = "Temps de Bitcoin"; +"widgets__weather__description" = "Descobreix quan és un bon moment per fer transaccions a la blockchain de Bitcoin."; "widgets__weather__condition__good__title" = "Condicions favorables"; "widgets__weather__condition__good__description" = "Ara seria un bon moment per fer transaccions a la blockchain."; "widgets__weather__condition__average__title" = "Condicions mitjanes"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index ab02e1b99..a7af52dd4 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -1136,7 +1136,6 @@ "wallet__receive_insufficient_text" = "Nedostatečná přijímací kapacita pro příjem této částky přes lightning."; "wallet__receive_foreground_title" = "Udržovat Bitkit na popředí"; "wallet__receive_foreground_msg" = "Při přepínání mezi aplikacemi může dojít k selhání plateb na váš disponibilní zůstatek."; -"widgets__widgets" = "Widgety"; "widgets__onboarding__title" = "Dobrý den,\nWidgety"; "widgets__onboarding__description" = "Užívejte si decentralizované kanály ze svých oblíbených webových služeb přidáním zábavných a užitečných widgetů do peněženky Bitkit."; "widgets__widget__nav_title" = "Widget"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index 869273695..2eee50823 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -1132,7 +1132,6 @@ "wallet__receive_insufficient_text" = "Unzureichende Empfangskapazität, um diesen Betrag über Lightning zu empfangen."; "wallet__receive_foreground_title" = "Bitkit im Vordergrund halten"; "wallet__receive_foreground_msg" = "Zahlungen auf Ihr Guthaben können fehlschlagen, wenn Sie zwischen Apps wechseln."; -"widgets__widgets" = "Widgets"; "widgets__onboarding__title" = "Hallo,\nWidgets"; "widgets__onboarding__description" = "Genieße dezentrale Feeds von deinen bevorzugten Webdiensten, indem du deinem Bitkit-Wallet lustige und nützliche Widgets hinzufügst."; "widgets__widget__nav_title" = "Widget"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index 8c7654fbe..e78fdd7a7 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -839,7 +839,6 @@ "wallet__balance_hidden_message" = "Σύρετε το υπόλοιπο του πορτοφολιού σας για να το αποκαλύψετε ξανά."; "wallet__balance_unit_switched_title" = "Αλλαγή σε {unit}"; "wallet__balance_unit_switched_message" = "Πατήστε στο υπόλοιπο του πορτοφολιού σας για να το αλλάξετε πίσω σε {unit}."; -"widgets__widgets" = "Widgets"; "widgets__onboarding__title" = "Γεια σας,\nWidgets"; "widgets__onboarding__description" = "Απολαύστε αποκεντρωμένες ροές από τις αγαπημένες σας υπηρεσίες web, προσθέτοντας διασκεδαστικά και χρήσιμα widgets στο πορτοφόλι Bitkit σας."; "widgets__nav_title" = "Widgets"; @@ -852,9 +851,21 @@ "widgets__add" = "Προσθήκη Widget"; "widgets__delete__title" = "Διαγραφή Widget;"; "widgets__delete__description" = "Είστε σίγουροι ότι θέλετε να διαγράψετε το '{name}' από τα widgets σας;"; +"widgets__price__name" = "Τιμή Bitcoin"; +"widgets__price__description" = "Έλεγξε τις τελευταίες συναλλαγματικές ισοτιμίες Bitcoin για διάφορα fiat νομίσματα."; "widgets__price__error" = "Δεν ήταν δυνατή η λήψη δεδομένων τιμής"; +"widgets__news__name" = "Bitcoin Headlines"; +"widgets__news__description" = "Διάβασε τους τελευταίους τίτλους Bitcoin από διάφορες ειδησεογραφικές σελίδες."; "widgets__news__error" = "Δεν ήταν δυνατή η λήψη τελευταίων ειδήσεων"; +"widgets__blocks__name" = "Bitcoin Blocks"; +"widgets__blocks__description" = "Εξέτασε διάφορα στατιστικά για τα νεοεξορυχθέντα Bitcoin Blocks."; "widgets__blocks__error" = "Δεν ήταν δυνατή η λήψη δεδομένων blocks"; +"widgets__facts__name" = "Bitcoin Facts"; +"widgets__facts__description" = "Ανακάλυψε διασκεδαστικά γεγονότα για το Bitcoin, κάθε φορά που ανοίγεις το πορτοφόλι σου."; +"widgets__calculator__name" = "Αριθμομηχανή Bitcoin"; +"widgets__calculator__description" = "Μετατροπή ποσών ₿ σε {fiatSymbol} ή αντίστροφα."; +"widgets__weather__name" = "Bitcoin Weather"; +"widgets__weather__description" = "Μάθε πότε είναι καλή στιγμή για συναλλαγή στο Bitcoin blockchain."; "widgets__weather__condition__good__title" = "Ευνοϊκές Συνθήκες"; "widgets__weather__condition__good__description" = "Τώρα θα ήταν καλή στιγμή για συναλλαγές στο blockchain."; "widgets__weather__condition__average__title" = "Μέσες Συνθήκες"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 4adc42912..7de19e7bf 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -1375,7 +1375,6 @@ "wallet__receive_insufficient_text" = "Insufficient receiving capacity to receive this amount over Lightning."; "wallet__receive_foreground_title" = "Keep Bitkit In Foreground"; "wallet__receive_foreground_msg" = "Payments to your spending balance might fail if you switch between apps."; -"widgets__widgets" = "Widgets"; "widgets__onboarding__swipe" = "Swipe down\nto find your\nwidgets"; "widgets__onboarding__title" = "Hello,\nWidgets"; "widgets__onboarding__description" = "Enjoy decentralized feeds from your favorite web services, by adding fun and useful widgets to your Bitkit wallet."; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index 6b6a61b7b..4fbc07158 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -1147,7 +1147,6 @@ "wallet__receive_insufficient_text" = "Capacidad de recepción insuficiente para recibir esta cantidad a través de Lightning."; "wallet__receive_foreground_title" = "Mantener Bitkit en primer plano"; "wallet__receive_foreground_msg" = "Los pagos a su saldo de gastos pueden fallar si cambia de una aplicación a otra."; -"widgets__widgets" = "Widgets"; "widgets__onboarding__title" = "Hola,\nWidgets"; "widgets__onboarding__description" = "Disfruta feeds descentralizados de tus servicios web favoritos añadiendo widgets útiles y divertidos a tu wallet de Bitkit."; "widgets__nav_title" = "Widgets"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index 98a3f8cd1..8421df428 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -1006,7 +1006,6 @@ "wallet__balance_unit_switched_title" = "Cambió a {unit}"; "wallet__balance_unit_switched_message" = "Toca tu saldo del monedero para cambiarlo de nuevo a {unit}."; "wallet__ldk_sync_error_title" = "Error de Sincronización de Lightning"; -"widgets__widgets" = "Widgets"; "widgets__onboarding__title" = "Hola,\nWidgets"; "widgets__onboarding__description" = "Disfruta de fuentes descentralizadas de tus servicios web favoritos, añadiendo widgets divertidos y útiles a tu monedero Bitkit."; "widgets__nav_title" = "Widgets"; @@ -1020,12 +1019,20 @@ "widgets__delete__title" = "¿Borrar Widget?"; "widgets__delete__description" = "¿Está seguro de querer borrar '{name}' de sus widgets?"; "widgets__price__name" = "Precio de Bitcoin"; +"widgets__price__description" = "Consulta los últimos tipos de cambio de Bitcoin para varias monedas fiduciarias."; "widgets__price__error" = "No se pudieron obtener los datos de precio"; "widgets__news__name" = "Titulares Bitcoin"; +"widgets__news__description" = "Lee los últimos y mejores titulares de Bitcoin de varios sitios de noticias."; "widgets__news__error" = "No se pudieron obtener las últimas noticias"; "widgets__blocks__name" = "Bloques Bitcoin"; +"widgets__blocks__description" = "Examina diversas estadísticas sobre los bloques de Bitcoin recién minados."; "widgets__blocks__error" = "No se pudieron obtener los datos de bloques"; "widgets__facts__name" = "Hechos Bitcoin"; +"widgets__facts__description" = "Descubre datos curiosos sobre Bitcoin, cada vez que abras tu monedero."; +"widgets__calculator__name" = "Calculadora Bitcoin"; +"widgets__calculator__description" = "Convierte cantidades de ₿ a {fiatSymbol} o viceversa."; +"widgets__weather__name" = "Tiempo Bitcoin"; +"widgets__weather__description" = "Descubre cuándo es un buen momento para transaccionar en la blockchain de Bitcoin."; "widgets__weather__condition__good__title" = "Condiciones Favorables"; "widgets__weather__condition__good__description" = "Ahora sería un buen momento para realizar transacciones en la blockchain."; "widgets__weather__condition__average__title" = "Condiciones Promedio"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index 733882182..458df3548 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -1161,7 +1161,6 @@ "wallet__receive_insufficient_text" = "Capacité insuffisante pour recevoir ce montant en Lightning."; "wallet__receive_foreground_title" = "Garder Bitkit au premier plan"; "wallet__receive_foreground_msg" = "Les paiements sur votre solde du compte courant peuvent échouer si vous passez d\'une application à l\'autre."; -"widgets__widgets" = "Widgets"; "widgets__onboarding__title" = "Bonjour, \nWidgets"; "widgets__onboarding__description" = "Profitez des flux décentralisés de vos services web préférés en ajoutant des widgets amusants et utiles à votre portefeuille Bitkit."; "widgets__nav_title" = "Widgets"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index 0b1186efd..49854cdbd 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -1115,7 +1115,6 @@ "wallet__ldk_sync_error_title" = "Errore di sincronizzazione Lightning"; "wallet__receive_insufficient_title" = "Saldo di ricezione insufficiente."; "wallet__receive_insufficient_text" = "Capacità di ricezione insufficiente per ricevere questo importo tramite Lightning."; -"widgets__widgets" = "Widget"; "widgets__onboarding__title" = "Ciao,\nWidgets"; "widgets__onboarding__description" = "Goditi i feed decentralizzati dai tuoi servizi web preferiti, aggiungendo widget divertenti e utili al tuo portafoglio Bitkit."; "widgets__nav_title" = "Widget"; @@ -1129,12 +1128,20 @@ "widgets__delete__title" = "Eliminare Widget?"; "widgets__delete__description" = "Sei sicuro di volere eliminare '{name}' dai tuoi widget?"; "widgets__price__name" = "Prezzo Bitcoin"; +"widgets__price__description" = "Controlla i tassi di cambio Bitcoin piu' recenti per varie valute fiat."; "widgets__price__error" = "Impossibile ottenere i dati del prezzo"; "widgets__news__name" = "Titoli di Testa di Bitcoin"; +"widgets__news__description" = "Leggi le ultime notizie su Bitcoin da vari siti di informazione."; "widgets__news__error" = "Impossibile ottenere le ultime notizie"; "widgets__blocks__name" = "Blocchi Bitcoin"; +"widgets__blocks__description" = "Esamina varie statistiche sui blocchi Bitcoin appena estratti."; "widgets__blocks__error" = "Impossibile ottenere i dati dei blocchi"; "widgets__facts__name" = "Fatti su Bitcoin"; +"widgets__facts__description" = "Scopri fatti divertenti su Bitcoin ogni volta che apri il wallet."; +"widgets__calculator__name" = "Calcolatrice Bitcoin"; +"widgets__calculator__description" = "Converti importi ₿ in {fiatSymbol} o viceversa."; +"widgets__weather__name" = "Meteo Bitcoin"; +"widgets__weather__description" = "Scopri quando e' un buon momento per effettuare transazioni sulla blockchain Bitcoin."; "widgets__weather__condition__good__title" = "Condizioni Favorevoli"; "widgets__weather__condition__good__description" = "Ora sarebbe un buon momento per transare sulla blockchain."; "widgets__weather__condition__average__title" = "Condizioni Medie"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index f56438151..70628f1c9 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -1155,7 +1155,6 @@ "wallet__receive_insufficient_text" = "Onvoldoende ontvangstcapaciteit om dit bedrag via Lightning te ontvangen."; "wallet__receive_foreground_title" = "Bitkit im Vordergrund halten"; "wallet__receive_foreground_msg" = "Zahlungen auf Ihr Guthaben können fehlschlagen, wenn Sie zwischen Apps wechseln."; -"widgets__widgets" = "Widgets"; "widgets__onboarding__title" = "Hallo,\nWidgets"; "widgets__onboarding__description" = "Geniet van gedecentraliseerde feeds van uw favoriete webdiensten, door leuke en nuttige widgets aan uw Bitkit wallet toe te voegen."; "widgets__nav_title" = "Widgets"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index c76dd7f05..8004cc27b 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -1162,7 +1162,6 @@ "wallet__receive_insufficient_text" = "Niewystarczająca zdolność odbiorcza, aby otrzymać tę kwotę przez Lightning."; "wallet__receive_foreground_title" = "Zachowaj Bitkit na pierwszym planie"; "wallet__receive_foreground_msg" = "Płatności na saldo wydatków mogą się nie powieść, jeśli przełączą się Państwo między aplikacjami."; -"widgets__widgets" = "Widgety"; "widgets__onboarding__title" = "Hej,\nWidgety"; "widgets__onboarding__description" = "Korzystaj ze zdecentralizowanych kanałów swoich ulubionych usług internetowych, dodając do portfela Bitkit przydatne i interaktywne widgety."; "widgets__nav_title" = "Widgety"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index b3197bebe..bcad5ec92 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -1163,7 +1163,6 @@ "wallet__receive_insufficient_text" = "Limite de recebimento insuficiente para esse valor."; "wallet__receive_foreground_title" = "Mantenha a Bitkit em primeiro plano"; "wallet__receive_foreground_msg" = "Os pagamentos ao seu saldo de gastos podem falhar se o usuário alternar entre aplicativos."; -"widgets__widgets" = "Widgets"; "widgets__onboarding__title" = "Olá,\nWidgets"; "widgets__onboarding__description" = "Aproveite os feeds descentralizados de seus serviços da web favoritos, adicionando widgets divertidos e úteis à sua carteira Bitkit."; "widgets__nav_title" = "Widgets"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index 11bab719e..c99570f00 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -6,3 +6,34 @@ "slashtags__profile_link_label_placeholder" = "Por exemplo, \"Website\"."; "wallet__error_broadcast_tx_connection" = "Por favor, verifique sua conexão e tente novamente.\n{message}"; "wallet__drawer__support" = "Suporte"; +"widgets__onboarding__title" = "Olá,\nWidgets"; +"widgets__onboarding__description" = "Aproveite os feeds descentralizados de seus serviços da web favoritos, adicionando widgets divertidos e úteis à sua carteira Bitkit."; +"widgets__widget__nav_title" = "Widget"; +"widgets__widget__edit" = "Widget Feed"; +"widgets__widget__edit_default" = "Padrão"; +"widgets__widget__edit_custom" = "Personalizada"; +"widgets__widget__edit_description" = "Por favor, selecione quais campos você deseja exibir no widget {name}."; +"widgets__widget__source" = "Fonte"; +"widgets__add" = "Adicionar Widget"; +"widgets__delete__title" = "Excluir o Widget?"; +"widgets__delete__description" = "Tem certeza de que deseja excluir '{name}' dos seus widgets?"; +"widgets__price__name" = "Preço do Bitcoin"; +"widgets__price__description" = "Verifique as últimas taxas de câmbio de Bitcoin para uma variedade de moedas fiduciárias."; +"widgets__news__name" = "Manchetes sobre Bitcoin"; +"widgets__news__description" = "Leia as melhores e mais recentes manchetes sobre Bitcoin de vários sites de notícias."; +"widgets__blocks__name" = "Blocos do Bitcoin"; +"widgets__blocks__description" = "Examinar várias estatísticas sobre blocos de Bitcoin recém-minerados."; +"widgets__facts__name" = "Fatos sobre o Bitcoin"; +"widgets__facts__description" = "Descubra fatos divertidos sobre o Bitcoin, sempre que abrir sua carteira."; +"widgets__calculator__name" = "Calculadora de Bitcoin"; +"widgets__calculator__description" = "Converta ₿ para {fiatSymbol} ou vice-versa."; +"widgets__weather__name" = "Tempo do Bitcoin"; +"widgets__weather__description" = "Descubra quando é um bom momento para fazer transações na blockchain do Bitcoin."; +"widgets__weather__condition__good__title" = "Condições favoráveis"; +"widgets__weather__condition__good__description" = "Tudo limpo. Agora seria um bom momento para fazer transações na blockchain."; +"widgets__weather__condition__average__title" = "Condições normais"; +"widgets__weather__condition__average__description" = "A taxa do próximo bloco está próxima das médias mensais."; +"widgets__weather__condition__poor__title" = "Condições ruins"; +"widgets__weather__condition__poor__description" = "Se você não estiver com pressa para fazer uma transação, talvez seja melhor esperar um pouco."; +"widgets__weather__current_fee" = "Tarifa média atual"; +"widgets__weather__next_block" = "Inclusão no próximo bloco"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index 3a4aa86e1..8be22b534 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -1149,7 +1149,6 @@ "wallet__ldk_sync_error_title" = "Ошибка Синхронизации Lightning"; "wallet__receive_insufficient_title" = "Недостаточный входящий баланс."; "wallet__receive_insufficient_text" = "Недостаточно входящей ликвидности для получения данной суммы через Lightning."; -"widgets__widgets" = "Виджеты"; "widgets__onboarding__title" = "Привет,\nВиджеты"; "widgets__onboarding__description" = "Наслаждайтесь децентрализованными лентами ваших любимых веб-сервисов, добавляя интересные и полезные виджеты в ваш кошелёк Bitkit."; "widgets__nav_title" = "Виджеты"; @@ -1163,12 +1162,20 @@ "widgets__delete__title" = "Удалить виджет?"; "widgets__delete__description" = "Вы уверены, что хотите удалить «{name}» из ваших виджетов?"; "widgets__price__name" = "Биткоин Цена"; +"widgets__price__description" = "Проверяйте последние курсы обмена Bitcoin для различных фиатных валют."; "widgets__price__error" = "Не удалось получить данные о цене"; "widgets__news__name" = "Биткоин Заголовки"; +"widgets__news__description" = "Читайте последние и лучшие заголовки о Bitcoin из различных новостных сайтов."; "widgets__news__error" = "Не удалось получить последние новости"; "widgets__blocks__name" = "Биткоин Блоки"; +"widgets__blocks__description" = "Изучайте различную статистику по недавно добытым блокам Bitcoin."; "widgets__blocks__error" = "Не удалось получить данные о блоках"; "widgets__facts__name" = "Биткоин Факты"; +"widgets__facts__description" = "Открывайте интересные факты о Bitcoin каждый раз, когда открываете кошелёк."; +"widgets__calculator__name" = "Калькулятор Bitcoin"; +"widgets__calculator__description" = "Конвертируйте суммы ₿ в {fiatSymbol} или наоборот."; +"widgets__weather__name" = "Погода Bitcoin"; +"widgets__weather__description" = "Узнайте, когда хорошее время для транзакций в блокчейне Bitcoin."; "widgets__weather__condition__good__title" = "Благоприятные Условия"; "widgets__weather__condition__good__description" = "Сейчас подходящее время для транзакций в блокчейне."; "widgets__weather__condition__average__title" = "Средние Условия"; diff --git a/Bitkit/Views/Widgets/BlocksWidgetPreviewView.swift b/Bitkit/Views/Widgets/BlocksWidgetPreviewView.swift index 55796e081..6e3ad948a 100644 --- a/Bitkit/Views/Widgets/BlocksWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/BlocksWidgetPreviewView.swift @@ -124,6 +124,9 @@ struct BlocksWidgetPreviewView: View { Group { if let data = viewModel.blockData { BlocksWidgetCompactContent(data: data, options: currentOptions) + .padding(16) + .background(Color.gray6) + .cornerRadius(16) } else { placeholderCompact } @@ -140,6 +143,7 @@ struct BlocksWidgetPreviewView: View { Group { if let data = viewModel.blockData { BlocksWidgetWideContent(data: data, options: currentOptions) + .frame(height: BlocksWidgetWideContent.inAppContentHeight) .padding(16) .background(Color.gray6) .cornerRadius(16) @@ -159,10 +163,12 @@ struct BlocksWidgetPreviewView: View { } private var placeholderWide: some View { - Color.gray6 + ProgressView() + .frame(maxWidth: .infinity) + .frame(height: BlocksWidgetWideContent.inAppContentHeight) + .padding(16) + .background(Color.gray6) .cornerRadius(16) - .frame(height: 180) - .overlay(ProgressView()) } // MARK: - Size label & page indicator diff --git a/Bitkit/Views/Widgets/FactsWidgetPreviewView.swift b/Bitkit/Views/Widgets/FactsWidgetPreviewView.swift index 109d3c45f..6a0655f5a 100644 --- a/Bitkit/Views/Widgets/FactsWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/FactsWidgetPreviewView.swift @@ -71,6 +71,9 @@ struct FactsWidgetPreviewView: View { VStack { Spacer(minLength: 0) FactsWidgetCompactContent(fact: viewModel.fact) + .padding(16) + .background(Color.gray6) + .cornerRadius(16) .frame(width: 163, height: 192) Spacer(minLength: 0) } diff --git a/Bitkit/Views/Widgets/NewsWidgetPreviewView.swift b/Bitkit/Views/Widgets/NewsWidgetPreviewView.swift index ad05b9686..7618a77db 100644 --- a/Bitkit/Views/Widgets/NewsWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/NewsWidgetPreviewView.swift @@ -126,7 +126,10 @@ struct NewsWidgetPreviewView: View { Spacer(minLength: 0) Group { if let data = viewModel.widgetData { - NewsWidgetCompactContent(data: data, options: currentOptions) + NewsWidgetCompactContent(title: data.title, timeAgo: data.timeAgo, options: currentOptions) + .padding(16) + .background(Color.gray6) + .cornerRadius(16) } else { placeholderCompact } @@ -142,10 +145,16 @@ struct NewsWidgetPreviewView: View { Spacer(minLength: 0) Group { if let data = viewModel.widgetData { - NewsWidgetWideContent(data: data, options: currentOptions) - .padding(16) - .background(Color.gray6) - .cornerRadius(16) + NewsWidgetWideContent( + title: data.title, + publisher: data.publisher, + timeAgo: data.timeAgo, + options: currentOptions + ) + .frame(height: NewsWidgetWideContent.inAppContentHeight) + .padding(16) + .background(Color.gray6) + .cornerRadius(16) } else { placeholderWide } @@ -162,10 +171,12 @@ struct NewsWidgetPreviewView: View { } private var placeholderWide: some View { - Color.gray6 + ProgressView() + .frame(maxWidth: .infinity) + .frame(height: NewsWidgetWideContent.inAppContentHeight) + .padding(16) + .background(Color.gray6) .cornerRadius(16) - .frame(height: 118) - .overlay(ProgressView()) } // MARK: - Size label & page indicator diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index 1b95fdc66..464995ef0 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -137,6 +137,9 @@ struct PriceWidgetPreviewView: View { Group { if let data = primaryPrice { PriceWidgetCompactContent(data: data, period: currentOptions.selectedPeriod) + .padding(16) + .background(Color.gray6) + .cornerRadius(16) } else { placeholderCompact } diff --git a/Bitkit/Assets.xcassets/icons/bitcoin.imageset/Contents.json b/BitkitWidget/Assets.xcassets/bitcoin-symbol.imageset/Contents.json similarity index 52% rename from Bitkit/Assets.xcassets/icons/bitcoin.imageset/Contents.json rename to BitkitWidget/Assets.xcassets/bitcoin-symbol.imageset/Contents.json index 8a6583348..27cf96d8c 100644 --- a/Bitkit/Assets.xcassets/icons/bitcoin.imageset/Contents.json +++ b/BitkitWidget/Assets.xcassets/bitcoin-symbol.imageset/Contents.json @@ -1,12 +1,15 @@ { "images" : [ { - "filename" : "bitcoin.pdf", + "filename" : "bitcoin-symbol.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Bitkit/Assets.xcassets/icons/bitcoin.imageset/bitcoin.pdf b/BitkitWidget/Assets.xcassets/bitcoin-symbol.imageset/bitcoin-symbol.pdf similarity index 80% rename from Bitkit/Assets.xcassets/icons/bitcoin.imageset/bitcoin.pdf rename to BitkitWidget/Assets.xcassets/bitcoin-symbol.imageset/bitcoin-symbol.pdf index caaae04e5..8085e5f48 100644 Binary files a/Bitkit/Assets.xcassets/icons/bitcoin.imageset/bitcoin.pdf and b/BitkitWidget/Assets.xcassets/bitcoin-symbol.imageset/bitcoin-symbol.pdf differ diff --git a/BitkitWidget/BlocksHomeScreenWidget.swift b/BitkitWidget/BlocksHomeScreenWidget.swift index 6f3c348a9..315cd0bcb 100644 --- a/BitkitWidget/BlocksHomeScreenWidget.swift +++ b/BitkitWidget/BlocksHomeScreenWidget.swift @@ -93,9 +93,13 @@ struct BlocksHomeScreenWidgetEntryView: View { var entry: BlocksWidgetProvider.Entry + private var palette: WidgetPalette { + WidgetPalette(renderingMode: widgetRenderingMode) + } + var body: some View { content - .containerBackground(for: .widget) { backgroundView } + .containerBackground(for: .widget) { palette.background } } @ViewBuilder @@ -105,9 +109,9 @@ struct BlocksHomeScreenWidgetEntryView: View { } else if let block = entry.block { switch widgetFamily { case .systemSmall: - compactLayout(block: block) + BlocksWidgetCompactContent(data: block, options: entry.options) default: - wideLayout(block: block, fields: entry.options.enabledFields) + BlocksWidgetWideContent(data: block, options: entry.options) } } else { ProgressView() @@ -115,82 +119,10 @@ struct BlocksHomeScreenWidgetEntryView: View { } } - // MARK: - Layouts - - /// Compact (`.systemSmall`): icon + value rows for the selected fields. - private func compactLayout(block: CachedBlock) -> some View { - VStack(alignment: .leading, spacing: 16) { - ForEach(entry.options.enabledFields, id: \.self) { field in - HStack(alignment: .center, spacing: 8) { - iconImage(field: field) - Text(field.value(from: block)) - .font(Fonts.semiBold(size: 15)) - .foregroundColor(titleTextColor) - .lineLimit(1) - .truncationMode(.middle) - .widgetAccentable() - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - - /// Wide layout (`.systemMedium`): icon + label + value rows for the selected fields. - private func wideLayout(block: CachedBlock, fields: [BlocksWidgetField]) -> some View { - VStack(alignment: .leading, spacing: 16) { - ForEach(fields, id: \.self) { field in - HStack(alignment: .center, spacing: 8) { - iconImage(field: field) - Text(field.label) - .font(Fonts.regular(size: 17)) - .foregroundColor(labelTextColor) - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) - Text(field.value(from: block)) - .font(Fonts.semiBold(size: 17)) - .foregroundColor(titleTextColor) - .lineLimit(1) - .truncationMode(.middle) - .widgetAccentable() - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - - private func iconImage(field: BlocksWidgetField) -> some View { - Image(field.iconName) - .resizable() - .renderingMode(.template) - .foregroundColor(iconColor) - .frame(width: 20, height: 20) - .widgetAccentable() - } - private var errorView: some View { - Text("Couldn’t load blocks data.") - .font(Fonts.regular(size: 13)) - .foregroundColor(labelTextColor) + BodySText(t("widgets__blocks__error"), textColor: palette.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - - // MARK: - Colors - - private var backgroundView: some View { - widgetRenderingMode == .fullColor ? Color.gray6 : Color.clear - } - - private var titleTextColor: Color { - widgetRenderingMode == .fullColor ? .white : .primary - } - - private var labelTextColor: Color { - widgetRenderingMode == .fullColor ? .white.opacity(0.8) : .secondary - } - - private var iconColor: Color { - widgetRenderingMode == .fullColor ? .brandAccent : .primary - } } // MARK: - Widget Configuration @@ -203,8 +135,8 @@ struct BitkitBlocksWidget: Widget { ) { entry in BlocksHomeScreenWidgetEntryView(entry: entry) } - .configurationDisplayName("Bitcoin Blocks") - .description("Latest mined Bitcoin block, mirroring the in-app blocks widget.") + .configurationDisplayName(t("widgets__blocks__name")) + .description(t("widgets__blocks__description")) .supportedFamilies([.systemSmall, .systemMedium]) } } diff --git a/BitkitWidget/FactsHomeScreenWidget.swift b/BitkitWidget/FactsHomeScreenWidget.swift index c51b1381d..e1300e571 100644 --- a/BitkitWidget/FactsHomeScreenWidget.swift +++ b/BitkitWidget/FactsHomeScreenWidget.swift @@ -42,85 +42,25 @@ struct FactsHomeScreenWidgetEntryView: View { var entry: FactsWidgetProvider.Entry + private var palette: WidgetPalette { + WidgetPalette(renderingMode: widgetRenderingMode) + } + var body: some View { content - .containerBackground(for: .widget) { backgroundView } + .containerBackground(for: .widget) { palette.background } } @ViewBuilder private var content: some View { switch widgetFamily { case .systemSmall: - compactLayout + FactsWidgetCompactContent(fact: entry.fact) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 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() - } + FactsWidgetWideContent(fact: entry.fact) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - .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 } } @@ -134,8 +74,8 @@ struct BitkitFactsWidget: Widget { ) { entry in FactsHomeScreenWidgetEntryView(entry: entry) } - .configurationDisplayName("widgets__facts__name") - .description("widgets__facts__description") + .configurationDisplayName(t("widgets__facts__name")) + .description(t("widgets__facts__description")) .supportedFamilies([.systemSmall, .systemMedium]) } } diff --git a/BitkitWidget/NewsHomeScreenWidget.swift b/BitkitWidget/NewsHomeScreenWidget.swift index 4f8887ad5..6b436be98 100644 --- a/BitkitWidget/NewsHomeScreenWidget.swift +++ b/BitkitWidget/NewsHomeScreenWidget.swift @@ -133,6 +133,10 @@ struct NewsHomeScreenWidgetEntryView: View { var entry: NewsWidgetProvider.Entry + private var palette: WidgetPalette { + WidgetPalette(renderingMode: widgetRenderingMode) + } + var body: some View { Group { if let url = articleURL { @@ -142,7 +146,7 @@ struct NewsHomeScreenWidgetEntryView: View { } } .widgetURL(articleURL) - .containerBackground(for: .widget) { backgroundView } + .containerBackground(for: .widget) { palette.background } } private var articleURL: URL? { @@ -157,9 +161,19 @@ struct NewsHomeScreenWidgetEntryView: View { } else if let article = entry.article { switch widgetFamily { case .systemSmall: - compactLayout(article: article) + NewsWidgetCompactContent( + title: article.title, + timeAgo: entry.timeAgo, + options: entry.options + ) default: - wideLayout(article: article) + NewsWidgetWideContent( + title: article.title, + publisher: article.publisher, + timeAgo: entry.timeAgo, + options: entry.options, + titleLineLimit: 3 + ) } } else { ProgressView() @@ -167,97 +181,10 @@ struct NewsHomeScreenWidgetEntryView: View { } } - // MARK: - Compact (small widget — 163×192) - - private func compactLayout(article: CachedNewsArticle) -> some View { - VStack(alignment: .leading, spacing: 0) { - titleText(article.title) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer(minLength: 8) - - if entry.options.showDate { - HStack { - Spacer(minLength: 0) - Text(entry.timeAgo) - .font(Fonts.semiBold(size: 13)) - .tracking(0.4) - .foregroundColor(secondaryTextColor) - .lineLimit(1) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - - // MARK: - Wide (medium widget — 343×118) - - private func wideLayout(article: CachedNewsArticle) -> some View { - VStack(alignment: .leading, spacing: 0) { - titleText(article.title) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer(minLength: 8) - - if entry.options.showSource || entry.options.showDate { - HStack(alignment: .center, spacing: 8) { - if entry.options.showSource { - Text(article.publisher) - .font(Fonts.semiBold(size: 13)) - .tracking(0.4) - .foregroundColor(sourceTextColor) - .lineLimit(1) - } - Spacer(minLength: 0) - if entry.options.showDate { - Text(entry.timeAgo) - .font(Fonts.semiBold(size: 13)) - .tracking(0.4) - .foregroundColor(secondaryTextColor) - .lineLimit(1) - } - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - - // MARK: - Sub-views - - private func titleText(_ value: String) -> some View { - Text(value) - .font(Fonts.bold(size: 22)) - .foregroundColor(titleTextColor) - .lineLimit(4) - .minimumScaleFactor(0.85) - .widgetAccentable() - } - private var errorView: some View { - Text("Couldn’t load headlines.") - .font(Fonts.regular(size: 13)) - .foregroundColor(secondaryTextColor) + BodySText(t("widgets__news__error"), textColor: palette.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - - // MARK: - Colors - - private var backgroundView: some View { - widgetRenderingMode == .fullColor ? Color.gray6 : Color.clear - } - - private var titleTextColor: Color { - widgetRenderingMode == .fullColor ? .white : .primary - } - - private var sourceTextColor: Color { - guard widgetRenderingMode == .fullColor else { return .primary } - return .brandAccent - } - - private var secondaryTextColor: Color { - widgetRenderingMode == .fullColor ? .white.opacity(0.64) : .secondary - } } // MARK: - Widget Configuration @@ -270,8 +197,8 @@ struct BitkitNewsWidget: Widget { ) { entry in NewsHomeScreenWidgetEntryView(entry: entry) } - .configurationDisplayName("Bitcoin Headlines") - .description("Latest Bitcoin news headlines, mirroring the in-app headlines widget.") + .configurationDisplayName(t("widgets__news__name")) + .description(t("widgets__news__description")) .supportedFamilies([.systemSmall, .systemMedium]) } } diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index f30057c11..17152d781 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -1,4 +1,3 @@ -import Charts import SwiftUI import WidgetKit @@ -105,9 +104,13 @@ struct PriceHomeScreenWidgetEntryView: View { var entry: PriceWidgetProvider.Entry + private var palette: WidgetPalette { + WidgetPalette(renderingMode: widgetRenderingMode) + } + var body: some View { content - .containerBackground(for: .widget) { backgroundView } + .containerBackground(for: .widget) { palette.background } } @ViewBuilder @@ -117,9 +120,10 @@ struct PriceHomeScreenWidgetEntryView: View { } else if let primary = primaryPrice { switch widgetFamily { case .systemSmall: - compactLayout(data: primary) + PriceWidgetCompactContent(data: primary, period: entry.options.selectedPeriod) default: - wideLayout(data: primary) + PriceWidgetWideContent(data: primary, period: entry.options.selectedPeriod) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } else { ProgressView() @@ -134,141 +138,10 @@ struct PriceHomeScreenWidgetEntryView: View { return entry.prices.first } - // MARK: - Compact (small widget — 163×192) - - private func compactLayout(data: PriceData) -> some View { - VStack(alignment: .leading, spacing: 0) { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 0) { - CaptionMText(data.name, textColor: secondaryTextColor) - Spacer(minLength: 0) - CaptionMText(entry.options.selectedPeriod.rawValue, textColor: secondaryTextColor) - } - - priceText(data.price, size: 22) - - Text(data.change.formatted) - .font(Fonts.semiBold(size: 15)) - .foregroundColor(changeColor(isPositive: data.change.isPositive)) - .lineLimit(1) - .widgetAccentable() - } - - Spacer(minLength: 8) - - chart(values: data.pastValues, isPositive: data.change.isPositive, idealHeight: 64) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - - // MARK: - Wide (medium widget — 343×152) - - private func wideLayout(data: PriceData) -> some View { - VStack(alignment: .leading, spacing: 0) { - VStack(alignment: .leading, spacing: 4) { - HStack(alignment: .center, spacing: 16) { - CaptionMText("\(data.name) \(entry.options.selectedPeriod.rawValue)", textColor: secondaryTextColor) - .frame(maxWidth: .infinity, alignment: .leading) - - Text(data.change.formatted) - .font(Fonts.bold(size: 22)) - .foregroundColor(changeColor(isPositive: data.change.isPositive)) - .lineLimit(1) - .widgetAccentable() - } - - priceText(data.price, size: 34) - } - - Spacer(minLength: 4) - - chart(values: data.pastValues, isPositive: data.change.isPositive, idealHeight: 48, minHeight: 24) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - - // MARK: - Sub-views - - private func priceText(_ value: String, size: CGFloat) -> some View { - Text(value) - .font(Fonts.bold(size: size)) - .foregroundColor(valueTextColor) - .lineLimit(1) - .widgetAccentable() - } - - private func chart(values: [Double], isPositive: Bool, idealHeight: CGFloat, minHeight: CGFloat = 32) -> some View { - PriceWidgetChart( - values: values, - isPositive: isPositive, - renderingMode: widgetRenderingMode - ) - .frame(minHeight: minHeight, maxHeight: idealHeight) - .widgetAccentable() - } - private var errorView: some View { - BodySText("Couldn’t load price.", textColor: secondaryTextColor) + BodySText(t("widgets__price__error"), textColor: palette.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - - // MARK: - Colors - - private var backgroundView: some View { - widgetRenderingMode == .fullColor ? Color.gray6 : Color.clear - } - - private var secondaryTextColor: Color { - widgetRenderingMode == .fullColor ? .white.opacity(0.64) : .secondary - } - - private var valueTextColor: Color { - widgetRenderingMode == .fullColor ? .white : .primary - } - - private func changeColor(isPositive: Bool) -> Color { - guard widgetRenderingMode == .fullColor else { return .primary } - return isPositive ? .greenAccent : .redAccent - } -} - -// MARK: - Chart - -private struct PriceWidgetChart: View { - let values: [Double] - let isPositive: Bool - let renderingMode: WidgetRenderingMode - - private var normalizedValues: [Double] { - guard values.count > 1 else { return values } - let minValue = values.min() ?? 0 - let maxValue = values.max() ?? 0 - let range = maxValue - minValue - guard range > 0 else { return values.map { _ in 0.5 } } - return values.map { 0.15 + (($0 - minValue) / range) * 0.7 } - } - - private var lineColor: Color { - guard renderingMode == .fullColor else { return .primary } - return isPositive ? .greenAccent : .redAccent - } - - var body: some View { - Chart { - ForEach(Array(normalizedValues.enumerated()), id: \.offset) { index, value in - LineMark( - x: .value("Index", index), - y: .value("Price", value) - ) - .foregroundStyle(lineColor) - .lineStyle(StrokeStyle(lineWidth: 1.3)) - .interpolationMethod(.catmullRom) - } - } - .chartXAxis(.hidden) - .chartYAxis(.hidden) - .chartYScale(domain: 0.1 ... 0.9) - } } // MARK: - Widget Configuration @@ -281,8 +154,8 @@ struct BitkitPriceWidget: Widget { ) { entry in PriceHomeScreenWidgetEntryView(entry: entry) } - .configurationDisplayName("Bitcoin Price") - .description("Latest Bitcoin price and chart, mirroring the in-app price widget.") + .configurationDisplayName(t("widgets__price__name")) + .description(t("widgets__price__description")) .supportedFamilies([.systemSmall, .systemMedium]) } } diff --git a/BitkitWidget/WeatherHomeScreenWidget.swift b/BitkitWidget/WeatherHomeScreenWidget.swift index 6aec753a4..de98aa49c 100644 --- a/BitkitWidget/WeatherHomeScreenWidget.swift +++ b/BitkitWidget/WeatherHomeScreenWidget.swift @@ -72,9 +72,13 @@ struct WeatherHomeScreenWidgetEntryView: View { var entry: WeatherWidgetProvider.Entry + private var palette: WidgetPalette { + WidgetPalette(renderingMode: widgetRenderingMode) + } + var body: some View { content - .containerBackground(for: .widget) { backgroundView } + .containerBackground(for: .widget) { palette.background } } @ViewBuilder