diff --git a/ios/EXTENSIONS.md b/ios/EXTENSIONS.md new file mode 100644 index 0000000000..9d7779608f --- /dev/null +++ b/ios/EXTENSIONS.md @@ -0,0 +1,42 @@ +# iOS Extensions + +## Building extensions as a contributor + +Each iOS app extension (e.g. the Lichess widgets) is a separate Xcode target with its own bundle ID. For the extension to be installed on a device, that bundle ID must be registered in the Lichess Apple Developer account with a matching provisioning profile. + +As an external contributor you won't have access to that account, so `flutter run` will silently drop the extension from the bundle — you can add the app to your home screen but the widget won't appear in the widget catalog. + +### Testing locally + +You can still build and test extensions using your own Apple Developer account and iOS Simulator. A free account is enough for device testing. + +1. Open `ios/Runner.xcworkspace` in Xcode. +2. Select the **Runner** target → **Signing & Capabilities** → set **Team** to your account and change the bundle ID to something you own (e.g. `com.yourname.lichess`). +3. Do the same for the extension target (e.g. **LichessWidgetsExtension**), using a matching sub-ID (e.g. `com.yourname.lichess.widget`). +4. Build and run from Xcode — the extension will be signed with your profile and work on your device. + +These changes are local only and should not be committed. + +### Merging new extensions + +When a PR that adds a new extension is ready to merge, a Lichess org member with Apple Developer access needs to: + +1. Register the new App ID (for the extension's bundle ID) in the Apple Developer portal. +2. Create a provisioning profile for it. + +## Deploying extensions via fastlane + +Extensions have their own bundle ID (`org.lichess.mobileV2.`) and require a separate provisioning profile managed by `match`. The `Matchfile` and `Fastfile` list all extension bundle IDs alongside the main app. + +### Adding a new extension to the fastlane setup + +After registering the App ID in the developer portal (see above), run `match` once to create and store the provisioning profile: + +```sh +cd ios +bundle exec fastlane match appstore --app_identifier org.lichess.mobileV2. +``` + +This will generate the profile, push it to the certificates repo, and set the correct `PROVISIONING_PROFILE_SPECIFIER` in the Xcode project. After that, `fastlane beta` handles signing for all targets automatically — including in CI. + +Also add the new bundle ID to both `app_identifier` arrays in `fastlane/Matchfile` and the `sync_code_signing` call in `fastlane/Fastfile`. diff --git a/ios/LichessWidgets/Blog Feed Widget/BlogFeedFetcher.swift b/ios/LichessWidgets/Blog Feed Widget/BlogFeedFetcher.swift new file mode 100644 index 0000000000..afc3ea5aa8 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/BlogFeedFetcher.swift @@ -0,0 +1,70 @@ +import FeedKit +import UIKit +import WidgetKit +internal import XMLKit + +struct BlogFeedFetcher { + static var nextUpdateDate: Date { + Calendar.current.date(byAdding: .hour, value: 1, to: .now)! + } + + private func fetchThumbnail(urlString: String, spec: BlogThumbnailSpec) async -> Data? { + guard let url = URL(string: urlString), + let (data, _) = try? await URLSession.shared.data(from: url), + let source = UIImage(data: data) + else { return nil } + let scale = UITraitCollection.current.displayScale + let size = CGSize(width: spec.width * scale, height: spec.height * scale) + return await source.byPreparingThumbnail(ofSize: size)?.jpegData(compressionQuality: 0.85) + } + + func fetchEntry(feed: BlogFeedChoice, username: String?, family: WidgetFamily) async -> BlogFeedEntry { + let (items, error) = await fetchFeed(feed: feed, username: username, family: family) + return BlogFeedEntry(date: .now, feed: feed, username: username, items: items, error: error) + } + + private func fetchFeed(feed: BlogFeedChoice, + username: String?, + family: WidgetFamily) async -> (items: [BlogFeedItem], error: String?) { + guard let urlString = feed.feedURL(username: username) else { + return ([], "Enter a username in widget settings") + } + do { + guard case .atom(let atomFeed) = try await Feed(urlString: urlString) else { + return ([], "Unexpected feed format") + } + let thumbSpec = family.thumbnailSpec + let items = await withTaskGroup(of: (Int, BlogFeedItem).self) { group in + for (index, entry) in (atomFeed.entries ?? []).prefix(family.maxItems).enumerated() { + group.addTask { + let thumbData: Data? = if let thumbSpec, + let thumbURL = entry.media?.thumbnails?.first?.attributes?.url { + await fetchThumbnail(urlString: thumbURL, spec: thumbSpec) + } else { + nil + } + let entryURL = entry.links? + .first(where: { $0.attributes?.rel == "alternate" })? + .attributes?.href + ?? entry.links?.first?.attributes?.href + return (index, BlogFeedItem( + id: entry.id ?? "\(index)", + title: entry.title ?? "Untitled", + url: entryURL, + publishedDate: entry.published, + author: entry.authors?.first?.name, + thumbnailData: thumbData, + thumbnailImageName: nil + )) + } + } + var results: [(Int, BlogFeedItem)] = [] + for await result in group { results.append(result) } + return results.sorted { $0.0 < $1.0 }.map(\.1) + } + return (items, nil) + } catch { + return ([], error.localizedDescription) + } + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/BlogFeedPlaceholder.swift b/ios/LichessWidgets/Blog Feed Widget/BlogFeedPlaceholder.swift new file mode 100644 index 0000000000..2883cdaa12 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/BlogFeedPlaceholder.swift @@ -0,0 +1,121 @@ +import Foundation +import WidgetKit + +/// Static placeholder data shown in the widget gallery and while a widget loads. +enum BlogFeedPlaceholder { + static func entry(feed: BlogFeedChoice, username: String? = nil, family: WidgetFamily) -> BlogFeedEntry { + BlogFeedEntry( + date: .now, + feed: feed, + username: username, + items: Array(items(for: feed).prefix(family.maxItems)), + error: nil + ) + } + + private static func items(for feed: BlogFeedChoice) -> [BlogFeedItem] { + switch feed { + case .officialBlog: return officialBlogItems + case .communityBlog: return communityBlogItems + case .userBlog: return userBlogItems + } + } + + // MARK: Official Blog + + private static let officialBlogItems: [BlogFeedItem] = [ + BlogFeedItem(id: "1", + title: "Lichess Mobile App Update", + url: nil, + publishedDate: daysAgo(1), + author: nil, + thumbnailData: nil, + thumbnailImageName: "OfficialBlogPlaceholderImage1"), + BlogFeedItem(id: "2", + title: "Queens' Online Chess Festival", + url: nil, + publishedDate: daysAgo(31), + author: nil, + thumbnailData: nil, + thumbnailImageName: "OfficialBlogPlaceholderImage2"), + BlogFeedItem(id: "3", + title: "Announcing the ChessMood 20/20 Grand Prix 2026", + url: nil, + publishedDate: daysAgo(35), + author: nil, + thumbnailData: nil, + thumbnailImageName: "OfficialBlogPlaceholderImage3"), + BlogFeedItem(id: "4", + title: "Streamer Arenas Announcement — February to July 2026", + url: nil, + publishedDate: daysAgo(39), + author: nil, + thumbnailData: nil, + thumbnailImageName: "OfficialBlogPlaceholderImage4"), + ] + + // MARK: Community Blog + + private static let communityBlogItems: [BlogFeedItem] = [ + BlogFeedItem(id: "1", + title: "How To Analyse Your Game Like a 2000-Rated Player", + url: nil, + publishedDate: daysAgo(0), + author: "VihaanRathodBhuj", + thumbnailData: nil, + thumbnailImageName: "CommunityBlogPlaceholderImage1"), + BlogFeedItem(id: "2", + title: "What a Figure Skater Can Teach You About Chess Improvement", + url: nil, + publishedDate: daysAgo(0), + author: "FM MattyDPerrine", + thumbnailData: nil, + thumbnailImageName: "CommunityBlogPlaceholderImage2"), + BlogFeedItem(id: "3", title: "Slow Growth in Chess: The Truth Most People Don't Want to Hear", + url: nil, publishedDate: daysAgo(2), + author: "IM nikhildixit", + thumbnailData: nil, + thumbnailImageName: "CommunityBlogPlaceholderImage3"), + BlogFeedItem(id: "4", title: "Everyone's favorite: A modern Kortchnoi seeking to change his history", + url: nil, + publishedDate: daysAgo(0), + author: "FM Reynold9402", + thumbnailData: nil, + thumbnailImageName: "CommunityBlogPlaceholderImage4"), + ] + + // MARK: User Blog + + private static let userBlogItems: [BlogFeedItem] = [ + BlogFeedItem(id: "1", + title: "Did you know Lichess can do this? Opening Explorer and Tablebase", + url: nil, + publishedDate: daysAgo(2), + author: nil, + thumbnailData: nil, + thumbnailImageName: "UserBlogPlaceholderImage1"), + BlogFeedItem(id: "2", + title: "Annotated: Sicilian Accelerated Dragon Deep Dive", + url: nil, + publishedDate: daysAgo(7), + author: nil, + thumbnailData: nil, thumbnailImageName: "UserBlogPlaceholderImage2"), + BlogFeedItem(id: "3", + title: "Tournament Report: City Open 2026", + url: nil, + publishedDate: daysAgo(14), + author: nil, + thumbnailData: nil, thumbnailImageName: "UserBlogPlaceholderImage3"), + BlogFeedItem(id: "4", + title: "Endgame Studies I Recommend", + url: nil, + publishedDate: daysAgo(21), + author: nil, + thumbnailData: nil, + thumbnailImageName: "UserBlogPlaceholderImage4"), + ] + + private static func daysAgo(_ days: Int) -> Date? { + Calendar.current.date(byAdding: .day, value: -days, to: .now) + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/CommunityBlogWidget.swift b/ios/LichessWidgets/Blog Feed Widget/CommunityBlogWidget.swift new file mode 100644 index 0000000000..f40654026d --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/CommunityBlogWidget.swift @@ -0,0 +1,17 @@ +import SwiftUI +import WidgetKit + +struct CommunityBlogWidget: Widget { + let kind = "CommunityBlogWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, + provider: GenericBlogFeedProvider(feed: .communityBlog)) { entry in + BlogFeedWidgetEntryView(entry: entry) + .containerBackground(.background, for: .widget) + } + .configurationDisplayName("Community Blog") + .description("Latest posts from the Lichess community blog.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/GenericBlogFeedProvider.swift b/ios/LichessWidgets/Blog Feed Widget/GenericBlogFeedProvider.swift new file mode 100644 index 0000000000..b64b9a8aa4 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/GenericBlogFeedProvider.swift @@ -0,0 +1,30 @@ +import SwiftUI +import WidgetKit + +struct GenericBlogFeedProvider: TimelineProvider { + let feed: BlogFeedChoice + private let fetcher = BlogFeedFetcher() + + func placeholder(in context: Context) -> BlogFeedEntry { + BlogFeedPlaceholder.entry(feed: feed, family: context.family) + } + + func getSnapshot(in context: Context, completion: @escaping (BlogFeedEntry) -> Void) { + if context.isPreview { completion(placeholder(in: context)); return } + Task { + completion(await fetcher.fetchEntry(feed: feed, + username: nil, + family: context.family)) + } + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + Task { + let entry = await fetcher.fetchEntry(feed: feed, + username: nil, + family: context.family) + let nextUpdate = BlogFeedFetcher.nextUpdateDate + completion(Timeline(entries: [entry], policy: .after(nextUpdate))) + } + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedChoice.swift b/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedChoice.swift new file mode 100644 index 0000000000..8e7c67acc1 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedChoice.swift @@ -0,0 +1,25 @@ +import Foundation + +enum BlogFeedChoice: String { + case officialBlog + case communityBlog + case userBlog + + var displayName: String { + switch self { + case .officialBlog: return "Official Blog" + case .communityBlog: return "Community Blog" + case .userBlog: return "User Blog" + } + } + + func feedURL(username: String?) -> String? { + switch self { + case .officialBlog: return "https://lichess.org/@/Lichess/blog.atom" + case .communityBlog: return "https://lichess.org/blog/community.atom" + case .userBlog: + guard let username, !username.isEmpty else { return nil } + return "https://lichess.org/@/\(username)/blog.atom" + } + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedEntry.swift b/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedEntry.swift new file mode 100644 index 0000000000..50dab141b8 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedEntry.swift @@ -0,0 +1,17 @@ +import WidgetKit + +struct BlogFeedEntry: TimelineEntry { + let date: Date + let feed: BlogFeedChoice + let username: String? + let items: [BlogFeedItem] + let error: String? + + /// Display name for the widget header. + var headerTitle: String { + if feed == .userBlog, let username, !username.isEmpty { + return "@\(username)" + } + return feed.displayName + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedItem.swift b/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedItem.swift new file mode 100644 index 0000000000..fa23b4d20d --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Model/BlogFeedItem.swift @@ -0,0 +1,12 @@ +import Foundation + +struct BlogFeedItem: Identifiable { + let id: String + let title: String + let url: String? + let publishedDate: Date? + let author: String? + let thumbnailData: Data? + /// Asset catalog image name, used for static placeholder items only. + let thumbnailImageName: String? +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Model/BlogThumbnailSpec.swift b/ios/LichessWidgets/Blog Feed Widget/Model/BlogThumbnailSpec.swift new file mode 100644 index 0000000000..fb2ea8b97d --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Model/BlogThumbnailSpec.swift @@ -0,0 +1,7 @@ +import Foundation + +struct BlogThumbnailSpec { + let width: CGFloat + let aspectRatio: CGFloat // height = width * aspectRatio + var height: CGFloat { width * aspectRatio } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/OfficialBlogWidget.swift b/ios/LichessWidgets/Blog Feed Widget/OfficialBlogWidget.swift new file mode 100644 index 0000000000..462781582a --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/OfficialBlogWidget.swift @@ -0,0 +1,17 @@ +import SwiftUI +import WidgetKit + +struct OfficialBlogWidget: Widget { + let kind = "OfficialBlogWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, + provider: GenericBlogFeedProvider(feed: .officialBlog)) { entry in + BlogFeedWidgetEntryView(entry: entry) + .containerBackground(.background, for: .widget) + } + .configurationDisplayName("Official Blog") + .description("Latest posts from the Lichess official blog.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/UserBlogFeedProvider.swift b/ios/LichessWidgets/Blog Feed Widget/UserBlogFeedProvider.swift new file mode 100644 index 0000000000..77365f3b32 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/UserBlogFeedProvider.swift @@ -0,0 +1,24 @@ +import WidgetKit + +struct UserBlogFeedProvider: AppIntentTimelineProvider { + private let fetcher = BlogFeedFetcher() + + func placeholder(in context: Context) -> BlogFeedEntry { + BlogFeedPlaceholder.entry(feed: .userBlog, username: "ChessNoob2009", family: context.family) + } + + func snapshot(for configuration: UserBlogFeedIntent, in context: Context) async -> BlogFeedEntry { + if context.isPreview { return placeholder(in: context) } + return await fetcher.fetchEntry(feed: .userBlog, + username: configuration.username, + family: context.family) + } + + func timeline(for configuration: UserBlogFeedIntent, in context: Context) async -> Timeline { + let entry = await fetcher.fetchEntry(feed: .userBlog, + username: configuration.username, + family: context.family) + let nextUpdate = BlogFeedFetcher.nextUpdateDate + return Timeline(entries: [entry], policy: .after(nextUpdate)) + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/UserBlogFeedWidget.swift b/ios/LichessWidgets/Blog Feed Widget/UserBlogFeedWidget.swift new file mode 100644 index 0000000000..520fd0897d --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/UserBlogFeedWidget.swift @@ -0,0 +1,31 @@ +import AppIntents +import SwiftUI +import WidgetKit + +struct UserBlogFeedWidget: Widget { + let kind = "UserBlogFeedWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: UserBlogFeedIntent.self, + provider: UserBlogFeedProvider()) { entry in + BlogFeedWidgetEntryView(entry: entry) + .containerBackground(.background, for: .widget) + } + .configurationDisplayName("User Blog") + .description("Shows the latest posts from a Lichess user blog.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} + +struct UserBlogFeedIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "User Blog" + static var description = IntentDescription("Choose which Lichess user blog to display.") + + @Parameter(title: "Username", default: "Lichess") + var username: String + + static var parameterSummary: some ParameterSummary { + Summary("Show blog for \(\.$username)") + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedItemRow.swift b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedItemRow.swift new file mode 100644 index 0000000000..3b6c519f78 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedItemRow.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct BlogFeedItemRow: View { + let item: BlogFeedItem + let spec: BlogThumbnailSpec? + let lineLimit: Int + let showDate: Bool + var showAuthor: Bool = false + + var body: some View { + HStack(alignment: .top, spacing: BlogFeedWidgetLayout.itemHSpacing) { + VStack(alignment: .leading, spacing: 0) { + Text(item.title) + .font(.system(size: BlogFeedWidgetLayout.titleFontSize, weight: .semibold)) + .lineLimit(lineLimit) + .fixedSize(horizontal: false, vertical: true) + .foregroundStyle(.primary) + if showDate, let date = item.publishedDate { + Spacer(minLength: BlogFeedWidgetLayout.dateSpacerMinLength) + Group { + if showAuthor, let author = item.author { + Text("\(date, format: date.widgetDateFormat) · \(author)") + } else { + Text(date, format: date.widgetDateFormat) + } + } + .font(.system(size: BlogFeedWidgetLayout.metaFontSize)) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + if let spec { + BlogItemThumbnail(data: item.thumbnailData, + imageName: item.thumbnailImageName, spec: spec) + } + } + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetEntryView.swift b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetEntryView.swift new file mode 100644 index 0000000000..989993d760 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetEntryView.swift @@ -0,0 +1,96 @@ +import SwiftUI +import WidgetKit + +struct BlogFeedWidgetEntryView: View { + var entry: BlogFeedEntry + @Environment(\.widgetFamily) var family + + private var showDate: Bool { family == .systemLarge } + private var lineLimit: Int { + switch family { + case .systemSmall: 4 + case .systemLarge: 2 + default: 3 + } + } + + /// Computes a thumbnail spec that makes items fill `availableHeight` without exceeding the static size. + private func spec(for availableHeight: CGFloat) -> BlogThumbnailSpec? { + guard let staticSpec = family.thumbnailSpec else { return nil } + let count = CGFloat(max(entry.items.count, 1)) + let overhead = count * BlogFeedWidgetLayout.itemTopPadding + + (count - 1) * BlogFeedWidgetLayout.dividerTotalHeight + let thumbHeight = min(max((availableHeight - overhead) / count, BlogFeedWidgetLayout.minThumbHeight), + staticSpec.height) + return BlogThumbnailSpec(width: thumbHeight / staticSpec.aspectRatio, + aspectRatio: staticSpec.aspectRatio) + } + + @ViewBuilder + private func itemsContent(spec: BlogThumbnailSpec?) -> some View { + if let error = entry.error { + VStack(spacing: BlogFeedWidgetLayout.errorStackSpacing) { + Image(systemName: "exclamationmark.circle") + .font(.system(size: BlogFeedWidgetLayout.errorIconSize)) + .foregroundStyle(.secondary) + Text(error) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if entry.items.isEmpty { + Text("No items available") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.top, BlogFeedWidgetLayout.itemTopPadding) + } else { + VStack(alignment: .leading, spacing: 0) { + let showAuthor = showDate && entry.feed == .communityBlog + ForEach(Array(entry.items.enumerated()), id: \.element.id) { index, item in + let row = BlogFeedItemRow(item: item, + spec: spec, + lineLimit: lineLimit, + showDate: showDate, + showAuthor: showAuthor) + .padding(.top, BlogFeedWidgetLayout.itemTopPadding) + if let dest = item.url?.lichessWebURL { + Link(destination: dest) { row } + } else { + row + } + if index < entry.items.count - 1 { + Divider() + .padding(.top, BlogFeedWidgetLayout.itemTopPadding) + } + } + } + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + BlogFeedWidgetHeader(feedName: entry.headerTitle, + updatedAt: entry.date, + showTimestamp: family != .systemSmall) + Divider() + .padding(.top, BlogFeedWidgetLayout.itemTopPadding) + + if family == .systemSmall { + itemsContent(spec: nil) + .frame(maxWidth: .infinity, alignment: .leading) + Spacer() + Text("Updated at \(entry.date.shortTime)") + .font(.system(size: BlogFeedWidgetLayout.secondaryFontSize)) + .foregroundStyle(.secondary) + .padding(.top, BlogFeedWidgetLayout.smallFooterTopPadding) + } else { + // GeometryReader measures remaining height so thumbnails fill the available area. + GeometryReader { geo in + itemsContent(spec: spec(for: geo.size.height)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetHeader.swift b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetHeader.swift new file mode 100644 index 0000000000..e275544b93 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetHeader.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct BlogFeedWidgetHeader: View { + let feedName: String + let updatedAt: Date + var showTimestamp: Bool = true + + var body: some View { + HStack(spacing: BlogFeedWidgetLayout.headerSpacing) { + Image("LichessLogo") + .resizable() + .frame(width: BlogFeedWidgetLayout.logoSize, height: BlogFeedWidgetLayout.logoSize) + Text(feedName) + .font(.system(size: BlogFeedWidgetLayout.titleFontSize, weight: .semibold)) + .foregroundStyle(.primary) + .lineLimit(1) + if showTimestamp { + Spacer() + Text("Updated at \(updatedAt.shortTime)") + .font(.system(size: BlogFeedWidgetLayout.secondaryFontSize)) + .foregroundStyle(.secondary) + } + } + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetLayout.swift b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetLayout.swift new file mode 100644 index 0000000000..7e06d800b0 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Views/BlogFeedWidgetLayout.swift @@ -0,0 +1,34 @@ +import CoreGraphics + +/// Layout constants shared across blog feed widget views. +enum BlogFeedWidgetLayout { + // Typography + static let titleFontSize: CGFloat = 15 + static let metaFontSize: CGFloat = 12 + static let secondaryFontSize: CGFloat = 11 + + // Header + static let headerSpacing: CGFloat = 6 + static let logoSize: CGFloat = 20 + + // Item row + static let itemHSpacing: CGFloat = 10 + static let dateSpacerMinLength: CGFloat = 4 + + // Item list — these constants are used both in `spec(for:)` and the actual view layout, + // keeping the geometry calculation in sync with what's rendered. + static let itemTopPadding: CGFloat = 8 + /// Divider (~1pt) plus its 8pt top padding — total vertical space consumed between items. + static let dividerTotalHeight: CGFloat = 9 + + // Thumbnail + static let minThumbHeight: CGFloat = 20 + static let thumbnailCornerRadius: CGFloat = 6 + + // Small widget footer + static let smallFooterTopPadding: CGFloat = 6 + + // Error state + static let errorIconSize: CGFloat = 20 + static let errorStackSpacing: CGFloat = 6 +} diff --git a/ios/LichessWidgets/Blog Feed Widget/Views/BlogItemThumbnail.swift b/ios/LichessWidgets/Blog Feed Widget/Views/BlogItemThumbnail.swift new file mode 100644 index 0000000000..b845b822b8 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/Views/BlogItemThumbnail.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct BlogItemThumbnail: View { + let data: Data? + let imageName: String? + let spec: BlogThumbnailSpec + + var body: some View { + Group { + if let data, let uiImage = UIImage(data: data) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else if let imageName { + Image(imageName) + .resizable() + .aspectRatio(contentMode: .fill) + } else { + Color.secondary.opacity(0.15) + } + } + .frame(width: spec.width, height: spec.height) + .clipShape(RoundedRectangle(cornerRadius: BlogFeedWidgetLayout.thumbnailCornerRadius)) + } +} diff --git a/ios/LichessWidgets/Blog Feed Widget/WidgetFamily+BlogFeed.swift b/ios/LichessWidgets/Blog Feed Widget/WidgetFamily+BlogFeed.swift new file mode 100644 index 0000000000..55aae64018 --- /dev/null +++ b/ios/LichessWidgets/Blog Feed Widget/WidgetFamily+BlogFeed.swift @@ -0,0 +1,19 @@ +import WidgetKit + +extension WidgetFamily { + var thumbnailSpec: BlogThumbnailSpec? { + switch self { + case .systemSmall: return nil + case .systemMedium: return BlogThumbnailSpec(width: 72, aspectRatio: 0.5625) // 16:9 + default: return BlogThumbnailSpec(width: 72, aspectRatio: 0.75) // 4:3 + } + } + + var maxItems: Int { + switch self { + case .systemSmall: return 1 + case .systemMedium: return 2 + default: return 4 + } + } +} diff --git a/ios/LichessWidgets/Deeplinks.swift b/ios/LichessWidgets/Deeplinks.swift new file mode 100644 index 0000000000..062bd7572f --- /dev/null +++ b/ios/LichessWidgets/Deeplinks.swift @@ -0,0 +1,9 @@ +import Foundation + +extension String { + /// Encodes the string into the custom scheme the app listens for to open it in the in-app browser. + var lichessWebURL: URL? { + guard let encoded = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil } + return URL(string: "org.lichess.mobile://open-web?url=\(encoded)") + } +} diff --git a/ios/LichessWidgets/Extensions/Date+Formatting.swift b/ios/LichessWidgets/Extensions/Date+Formatting.swift new file mode 100644 index 0000000000..edc584fbde --- /dev/null +++ b/ios/LichessWidgets/Extensions/Date+Formatting.swift @@ -0,0 +1,12 @@ +import Foundation + +extension Date { + /// "14:53" — used for "Updated at" timestamps. + var shortTime: String { formatted(.dateTime.hour().minute()) } + + /// "Mar 19" for the current year, "Mar 19, 2025" for a past/future year. + var widgetDateFormat: FormatStyle { + let sameYear = Calendar.current.isDate(self, equalTo: .now, toGranularity: .year) + return sameYear ? .dateTime.month(.abbreviated).day() : .dateTime.month(.abbreviated).day().year() + } +} diff --git a/ios/LichessWidgets/LichessWidgetsBundle.swift b/ios/LichessWidgets/LichessWidgetsBundle.swift new file mode 100644 index 0000000000..8e07bf1785 --- /dev/null +++ b/ios/LichessWidgets/LichessWidgetsBundle.swift @@ -0,0 +1,11 @@ +import WidgetKit +import SwiftUI + +@main +struct LichessWidgetsBundle: WidgetBundle { + var body: some Widget { + CommunityBlogWidget() + UserBlogFeedWidget() + OfficialBlogWidget() + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..2cd469830a --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.141", + "green" : "0.600", + "red" : "0.384" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage1.imageset/CommunityBlogPlaceholderImage1.png b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage1.imageset/CommunityBlogPlaceholderImage1.png new file mode 100644 index 0000000000..941394510c Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage1.imageset/CommunityBlogPlaceholderImage1.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage1.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage1.imageset/Contents.json new file mode 100644 index 0000000000..1cac6e8e1e --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "CommunityBlogPlaceholderImage1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage2.imageset/CommunityBlogPlaceholderImage2.png b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage2.imageset/CommunityBlogPlaceholderImage2.png new file mode 100644 index 0000000000..71405c9eee Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage2.imageset/CommunityBlogPlaceholderImage2.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage2.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage2.imageset/Contents.json new file mode 100644 index 0000000000..f983424b19 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "CommunityBlogPlaceholderImage2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage3.imageset/CommunityBlogPlaceholderImage3.png b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage3.imageset/CommunityBlogPlaceholderImage3.png new file mode 100644 index 0000000000..34006a8f59 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage3.imageset/CommunityBlogPlaceholderImage3.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage3.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage3.imageset/Contents.json new file mode 100644 index 0000000000..a6a8464dea --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "CommunityBlogPlaceholderImage3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage4.imageset/CommunityBlogPlaceholderImage4.png b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage4.imageset/CommunityBlogPlaceholderImage4.png new file mode 100644 index 0000000000..1b10c9e94a Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage4.imageset/CommunityBlogPlaceholderImage4.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage4.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage4.imageset/Contents.json new file mode 100644 index 0000000000..7537aaa728 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/CommunityBlogPlaceholderImage4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "CommunityBlogPlaceholderImage4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/CommunityBlog/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/Contents.json new file mode 100644 index 0000000000..f3387d4ae7 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage.png b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage.png new file mode 100644 index 0000000000..82ab8a2811 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage@2x.png b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000..24745bd7b3 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage@2x.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage@3x.png b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000..47bd1e86c1 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImage@3x.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark.png b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark.png new file mode 100644 index 0000000000..2ee65260d2 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark@2x.png b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark@2x.png new file mode 100644 index 0000000000..02be51e5aa Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark@2x.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark@3x.png b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark@3x.png new file mode 100644 index 0000000000..b6170c6462 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/LichessLogo.imageset/LaunchImageDark@3x.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage1.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage1.imageset/Contents.json new file mode 100644 index 0000000000..0a4a1c9fbb --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "OfficialBlogPlaceholderImage1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage1.imageset/OfficialBlogPlaceholderImage1.png b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage1.imageset/OfficialBlogPlaceholderImage1.png new file mode 100644 index 0000000000..acc34d0551 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage1.imageset/OfficialBlogPlaceholderImage1.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage2.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage2.imageset/Contents.json new file mode 100644 index 0000000000..d607e5239a --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "OfficialBlogPlaceholderImage2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage2.imageset/OfficialBlogPlaceholderImage2.png b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage2.imageset/OfficialBlogPlaceholderImage2.png new file mode 100644 index 0000000000..35da99980a Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage2.imageset/OfficialBlogPlaceholderImage2.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage3.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage3.imageset/Contents.json new file mode 100644 index 0000000000..3d60c1d9fc --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "OfficialBlogPlaceholderImage3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage3.imageset/OfficialBlogPlaceholderImage3.png b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage3.imageset/OfficialBlogPlaceholderImage3.png new file mode 100644 index 0000000000..72ff26cd26 Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage3.imageset/OfficialBlogPlaceholderImage3.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage4.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage4.imageset/Contents.json new file mode 100644 index 0000000000..d8784ba57d --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "OfficialBlogPlaceholderImage4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage4.imageset/OfficialBlogPlaceholderImage4.png b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage4.imageset/OfficialBlogPlaceholderImage4.png new file mode 100644 index 0000000000..303bbe398c Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/OfficialBlog/OfficialBlogPlaceholderImage4.imageset/OfficialBlogPlaceholderImage4.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage1.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage1.imageset/Contents.json new file mode 100644 index 0000000000..c91bc044ad --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "UserBlogPlaceholderImage1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage1.imageset/UserBlogPlaceholderImage1.png b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage1.imageset/UserBlogPlaceholderImage1.png new file mode 100644 index 0000000000..bd9cb0f29b Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage1.imageset/UserBlogPlaceholderImage1.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage2.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage2.imageset/Contents.json new file mode 100644 index 0000000000..ef48be27f4 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "UserBlogPlaceholderImage2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage2.imageset/UserBlogPlaceholderImage2.png b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage2.imageset/UserBlogPlaceholderImage2.png new file mode 100644 index 0000000000..2b40a660ef Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage2.imageset/UserBlogPlaceholderImage2.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage3.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage3.imageset/Contents.json new file mode 100644 index 0000000000..90a2991c84 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "UserBlogPlaceholderImage3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage3.imageset/UserBlogPlaceholderImage3.png b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage3.imageset/UserBlogPlaceholderImage3.png new file mode 100644 index 0000000000..f21fd7a61f Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage3.imageset/UserBlogPlaceholderImage3.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage4.imageset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage4.imageset/Contents.json new file mode 100644 index 0000000000..0715e8b44b --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "UserBlogPlaceholderImage4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage4.imageset/UserBlogPlaceholderImage4.png b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage4.imageset/UserBlogPlaceholderImage4.png new file mode 100644 index 0000000000..bd1f3bc21e Binary files /dev/null and b/ios/LichessWidgets/Resources/Assets.xcassets/UserBlog/UserBlogPlaceholderImage4.imageset/UserBlogPlaceholderImage4.png differ diff --git a/ios/LichessWidgets/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json b/ios/LichessWidgets/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000000..1eb4a776e4 --- /dev/null +++ b/ios/LichessWidgets/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.929", + "green" : "0.949", + "red" : "0.961" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.106", + "green" : "0.141", + "red" : "0.149" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/LichessWidgets/Resources/Info.plist b/ios/LichessWidgets/Resources/Info.plist new file mode 100644 index 0000000000..0f118fb75e --- /dev/null +++ b/ios/LichessWidgets/Resources/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9bd97494bb..edd3aae43a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,11 +3,16 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2F2A90DE2F6DF5F3008DA3C7 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F2A90DD2F6DF5F3008DA3C7 /* WidgetKit.framework */; }; + 2F2A90E02F6DF5F3008DA3C7 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F2A90DF2F6DF5F3008DA3C7 /* SwiftUI.framework */; }; + 2F2A90EB2F6DF5F4008DA3C7 /* LichessWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2F2A90DC2F6DF5F3008DA3C7 /* LichessWidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2F2A90F42F6DFF0F008DA3C7 /* FeedKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2F2A90F32F6DFF0F008DA3C7 /* FeedKit */; }; + 2F2A90F62F6DFF0F008DA3C7 /* XMLKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2F2A90F52F6DFF0F008DA3C7 /* XMLKit */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 4C1B4CBC5F6D1FB9AB4B2E2D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 150C426DBDFD9997221AABF1 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -17,7 +22,28 @@ C2F75327C3BF9CDCF3AB3FB9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D30EA23BD9D2932936A0A1CD /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 2F2A90E92F6DF5F4008DA3C7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2F2A90DB2F6DF5F3008DA3C7; + remoteInfo = LichessWidgetsExtension; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ + 2F2A90EC2F6DF5F4008DA3C7 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 2F2A90EB2F6DF5F4008DA3C7 /* LichessWidgetsExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -35,6 +61,10 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 150C426DBDFD9997221AABF1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2F10B1A97F0DD3F455EA4185 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 2F2A90DC2F6DF5F3008DA3C7 /* LichessWidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LichessWidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 2F2A90DD2F6DF5F3008DA3C7 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 2F2A90DF2F6DF5F3008DA3C7 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 2F884A662F70988D00F27CB2 /* EXTENSIONS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = EXTENSIONS.md; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -52,7 +82,32 @@ DE46A6181315EEBBE774A7A7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 2F2A90F02F6DF5F4008DA3C7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Resources/Info.plist, + ); + target = 2F2A90DB2F6DF5F3008DA3C7 /* LichessWidgetsExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2F2A90E12F6DF5F3008DA3C7 /* LichessWidgets */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2F2A90F02F6DF5F4008DA3C7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = LichessWidgets; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ + 2F2A90D92F6DF5F3008DA3C7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2F2A90F42F6DFF0F008DA3C7 /* FeedKit in Frameworks */, + 2F2A90E02F6DF5F3008DA3C7 /* SwiftUI.framework in Frameworks */, + 2F2A90DE2F6DF5F3008DA3C7 /* WidgetKit.framework in Frameworks */, + 2F2A90F62F6DFF0F008DA3C7 /* XMLKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -78,8 +133,10 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + 2F884A662F70988D00F27CB2 /* EXTENSIONS.md */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 2F2A90E12F6DF5F3008DA3C7 /* LichessWidgets */, 97C146EF1CF9000F007C117D /* Products */, F7DEDEB5E675E58B05C28E05 /* Pods */, 9A4FA943AFDC088B6B1831BF /* Frameworks */, @@ -91,6 +148,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 2F2A90DC2F6DF5F3008DA3C7 /* LichessWidgetsExtension.appex */, ); name = Products; sourceTree = ""; @@ -115,6 +173,8 @@ isa = PBXGroup; children = ( 150C426DBDFD9997221AABF1 /* Pods_Runner.framework */, + 2F2A90DD2F6DF5F3008DA3C7 /* WidgetKit.framework */, + 2F2A90DF2F6DF5F3008DA3C7 /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -132,6 +192,30 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 2F2A90DB2F6DF5F3008DA3C7 /* LichessWidgetsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2F2A90F12F6DF5F4008DA3C7 /* Build configuration list for PBXNativeTarget "LichessWidgetsExtension" */; + buildPhases = ( + 2F2A90D82F6DF5F3008DA3C7 /* Sources */, + 2F2A90D92F6DF5F3008DA3C7 /* Frameworks */, + 2F2A90DA2F6DF5F3008DA3C7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2F2A90E12F6DF5F3008DA3C7 /* LichessWidgets */, + ); + name = LichessWidgetsExtension; + packageProductDependencies = ( + 2F2A90F32F6DFF0F008DA3C7 /* FeedKit */, + 2F2A90F52F6DFF0F008DA3C7 /* XMLKit */, + ); + productName = LichessWidgetsExtension; + productReference = 2F2A90DC2F6DF5F3008DA3C7 /* LichessWidgetsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -141,6 +225,7 @@ 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, + 2F2A90EC2F6DF5F4008DA3C7 /* Embed Foundation Extensions */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 1584E18F136EB9BC694994CC /* [CP] Embed Pods Frameworks */, @@ -150,6 +235,7 @@ buildRules = ( ); dependencies = ( + 2F2A90EA2F6DF5F4008DA3C7 /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -163,9 +249,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 2630; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { + 2F2A90DB2F6DF5F3008DA3C7 = { + CreatedOnToolsVersion = 26.3; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -181,16 +271,27 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 2F2A90F22F6DFF0F008DA3C7 /* XCRemoteSwiftPackageReference "FeedKit" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 2F2A90DB2F6DF5F3008DA3C7 /* LichessWidgetsExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 2F2A90DA2F6DF5F3008DA3C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -214,10 +315,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -304,10 +409,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -316,6 +425,13 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 2F2A90D82F6DF5F3008DA3C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -327,6 +443,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 2F2A90EA2F6DF5F4008DA3C7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2F2A90DB2F6DF5F3008DA3C7 /* LichessWidgetsExtension */; + targetProxy = 2F2A90E92F6DF5F4008DA3C7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -432,6 +556,134 @@ }; name = Profile; }; + 2F2A90ED2F6DF5F4008DA3C7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LichessWidgets/Resources/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LichessWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.lichess.mobileV2.LichessWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2F2A90EE2F6DF5F4008DA3C7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LichessWidgets/Resources/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LichessWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.lichess.mobileV2.LichessWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2F2A90EF2F6DF5F4008DA3C7 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LichessWidgets/Resources/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LichessWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.lichess.mobileV2.LichessWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -615,6 +867,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2F2A90F12F6DF5F4008DA3C7 /* Build configuration list for PBXNativeTarget "LichessWidgetsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2F2A90ED2F6DF5F4008DA3C7 /* Debug */, + 2F2A90EE2F6DF5F4008DA3C7 /* Release */, + 2F2A90EF2F6DF5F4008DA3C7 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -636,6 +898,30 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2F2A90F22F6DFF0F008DA3C7 /* XCRemoteSwiftPackageReference "FeedKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nmdias/FeedKit"; + requirement = { + kind = exactVersion; + version = 10.4.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2F2A90F32F6DFF0F008DA3C7 /* FeedKit */ = { + isa = XCSwiftPackageProductDependency; + package = 2F2A90F22F6DFF0F008DA3C7 /* XCRemoteSwiftPackageReference "FeedKit" */; + productName = FeedKit; + }; + 2F2A90F52F6DFF0F008DA3C7 /* XMLKit */ = { + isa = XCSwiftPackageProductDependency; + package = 2F2A90F22F6DFF0F008DA3C7 /* XCRemoteSwiftPackageReference "FeedKit" */; + productName = XMLKit; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/LichessWidgetsExtension.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/LichessWidgetsExtension.xcscheme new file mode 100644 index 0000000000..9ffacb730f --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/LichessWidgetsExtension.xcscheme @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index d2067137a2..47aaae3f3d 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -33,6 +33,7 @@ platform :ios do sync_code_signing( type: "appstore", readonly: is_ci, + app_identifier: ["org.lichess.mobileV2", "org.lichess.mobileV2.LichessWidgets"], api_key: ENV['APP_STORE_KEY_JSON'], keychain_name: ENV['MATCH_KEYCHAIN_NAME'], keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"], diff --git a/ios/fastlane/Matchfile b/ios/fastlane/Matchfile index 251715d738..52562d4bd0 100644 --- a/ios/fastlane/Matchfile +++ b/ios/fastlane/Matchfile @@ -4,7 +4,7 @@ storage_mode("git") type("appstore") # The default type, can be: appstore, adhoc, enterprise or development -app_identifier(["org.lichess.mobileV2"]) +app_identifier(["org.lichess.mobileV2", "org.lichess.mobileV2.LichessWidgets"]) username(ENV['APPLE_ID']) # Your Apple Developer Portal username # For all available options run `fastlane match --help` diff --git a/lib/src/app.dart b/lib/src/app.dart index d198cc4f52..7303d1e562 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -164,10 +164,14 @@ class _AppState extends ConsumerState { if (uri.scheme == 'file' || uri.scheme == 'content') { return; } - if (uri.scheme == kOAuthRedirectUriScheme && uri.host == kOAuthRedirectUriHost) { + if (uri.scheme == kLichessUriScheme && uri.host == kOAuthRedirectUriHost) { ref.read(oauthCallbackProvider).add(uri); return; } + if (uri.scheme == kLichessUriScheme && uri.host == 'open-web') { + handleOpenWebLink(uri); + return; + } final context = _navigatorKey.currentContext; if (context != null && context.mounted) { handleAppLink(context, uri); diff --git a/lib/src/app_links.dart b/lib/src/app_links.dart index 780ca7850b..a1fa9a1a3c 100644 --- a/lib/src/app_links.dart +++ b/lib/src/app_links.dart @@ -73,6 +73,18 @@ List>? resolveAppLinkUri(BuildContext context, Uri appLinkUri) { return null; } +/// Handles an `org.lichess.mobile://open-web?url=...` link (e.g. from the platform widget) +/// by opening the encoded URL in the platform in-app browser. +void handleOpenWebLink(Uri uri) { + final target = uri.queryParameters['url']; + if (target != null) { + final targetUri = Uri.tryParse(target); + if (targetUri != null) { + launchUrl(targetUri, mode: LaunchMode.inAppBrowserView); + } + } +} + /// Handles an app link [Uri] by navigating to the corresponding screen(s). void handleAppLink(BuildContext context, Uri uri) { final routes = resolveAppLinkUri(context, uri); diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index 8fba025fac..010c8da89e 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -14,9 +14,9 @@ import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:url_launcher/url_launcher.dart'; -const kOAuthRedirectUriScheme = 'org.lichess.mobile'; +const kLichessUriScheme = 'org.lichess.mobile'; const kOAuthRedirectUriHost = 'login-callback'; -const kOAuthRedirectUri = '$kOAuthRedirectUriScheme://$kOAuthRedirectUriHost'; +const kOAuthRedirectUri = '$kLichessUriScheme://$kOAuthRedirectUriHost'; const oauthScopes = ['web:mobile']; final authRepositoryProvider = Provider((Ref ref) { @@ -71,7 +71,7 @@ class AuthRepository { .listen( (uri) { if (!callbackCompleter.isCompleted && - uri.scheme == kOAuthRedirectUriScheme && + uri.scheme == kLichessUriScheme && uri.host == kOAuthRedirectUriHost && uri.queryParameters['state'] == state) { callbackCompleter.complete(uri);