Skip to content

Commit 7e1442d

Browse files
authored
Merge branch 'main' into feat/eval-gauge-neutral-indicator
2 parents 5a29203 + c8d0b4e commit 7e1442d

File tree

76 files changed

+1693
-53
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1693
-53
lines changed

CLAUDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ dart format --output=none --set-exit-if-changed $(find test -name "*.dart" -not
7979
dart format lib/src test
8080
```
8181

82+
### Formatting Rules (CRITICAL)
83+
84+
**Always run `dart format` on every file you edit before finishing.** CI will fail if formatting is wrong.
85+
86+
```bash
87+
dart format path/to/file.dart
88+
```
89+
90+
The formatter is configured via `analysis_options.yaml` (`formatter: page_width: 100`) and `dart format` picks this up automatically. Key rules enforced by the formatter:
91+
92+
- **Page width: 100 characters** — lines exceeding 100 chars will be reflowed
93+
- **Trailing commas drive formatting**: a trailing comma after the last argument/parameter forces the formatter to expand the list to one-item-per-line; omitting it allows the formatter to keep items on one line if they fit within 100 chars
94+
- **Do not manually wrap lines** — let the formatter decide based on trailing commas and line length; hand-wrapping without trailing commas will be reformatted by the tool
95+
- The formatter may reformat code you didn't touch in the same expression if you change surrounding structure
96+
8297
## Translations (i18n)
8398

8499
**CRITICAL**: Never manually edit `lib/l10n/app_*.arb` files - they are generated.

ios/EXTENSIONS.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# iOS Extensions
2+
3+
## Building extensions as a contributor
4+
5+
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.
6+
7+
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.
8+
9+
### Testing locally
10+
11+
You can still build and test extensions using your own Apple Developer account and iOS Simulator. A free account is enough for device testing.
12+
13+
1. Open `ios/Runner.xcworkspace` in Xcode.
14+
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`).
15+
3. Do the same for the extension target (e.g. **LichessWidgetsExtension**), using a matching sub-ID (e.g. `com.yourname.lichess.widget`).
16+
4. Build and run from Xcode — the extension will be signed with your profile and work on your device.
17+
18+
These changes are local only and should not be committed.
19+
20+
### Merging new extensions
21+
22+
When a PR that adds a new extension is ready to merge, a Lichess org member with Apple Developer access needs to:
23+
24+
1. Register the new App ID (for the extension's bundle ID) in the Apple Developer portal.
25+
2. Create a provisioning profile for it.
26+
27+
## Deploying extensions via fastlane
28+
29+
Extensions have their own bundle ID (`org.lichess.mobileV2.<ExtensionName>`) and require a separate provisioning profile managed by `match`. The `Matchfile` and `Fastfile` list all extension bundle IDs alongside the main app.
30+
31+
### Adding a new extension to the fastlane setup
32+
33+
After registering the App ID in the developer portal (see above), run `match` once to create and store the provisioning profile:
34+
35+
```sh
36+
cd ios
37+
bundle exec fastlane match appstore --app_identifier org.lichess.mobileV2.<ExtensionName>
38+
```
39+
40+
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.
41+
42+
Also add the new bundle ID to both `app_identifier` arrays in `fastlane/Matchfile` and the `sync_code_signing` call in `fastlane/Fastfile`.

ios/Gemfile.lock

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,33 @@ GEM
33
specs:
44
CFPropertyList (3.0.8)
55
abbrev (0.1.2)
6-
addressable (2.8.8)
6+
addressable (2.8.9)
77
public_suffix (>= 2.0.2, < 8.0)
88
artifactory (3.0.17)
99
atomos (0.1.3)
1010
aws-eventstream (1.4.0)
11-
aws-partitions (1.1213.0)
12-
aws-sdk-core (3.242.0)
11+
aws-partitions (1.1232.0)
12+
aws-sdk-core (3.244.0)
1313
aws-eventstream (~> 1, >= 1.3.0)
1414
aws-partitions (~> 1, >= 1.992.0)
1515
aws-sigv4 (~> 1.9)
1616
base64
1717
bigdecimal
1818
jmespath (~> 1, >= 1.6.1)
1919
logger
20-
aws-sdk-kms (1.121.0)
21-
aws-sdk-core (~> 3, >= 3.241.4)
20+
aws-sdk-kms (1.123.0)
21+
aws-sdk-core (~> 3, >= 3.244.0)
2222
aws-sigv4 (~> 1.5)
23-
aws-sdk-s3 (1.213.0)
24-
aws-sdk-core (~> 3, >= 3.241.4)
23+
aws-sdk-s3 (1.217.1)
24+
aws-sdk-core (~> 3, >= 3.244.0)
2525
aws-sdk-kms (~> 1)
2626
aws-sigv4 (~> 1.5)
2727
aws-sigv4 (1.12.1)
2828
aws-eventstream (~> 1, >= 1.0.2)
2929
babosa (1.0.4)
3030
base64 (0.2.0)
3131
benchmark (0.5.0)
32-
bigdecimal (4.0.1)
32+
bigdecimal (4.1.0)
3333
claide (1.1.0)
3434
colored (1.2)
3535
colored2 (3.1.2)
@@ -68,11 +68,11 @@ GEM
6868
faraday-net_http_persistent (1.2.0)
6969
faraday-patron (1.0.0)
7070
faraday-rack (1.0.0)
71-
faraday-retry (1.0.3)
71+
faraday-retry (1.0.4)
7272
faraday_middleware (1.2.1)
7373
faraday (~> 1.0)
74-
fastimage (2.4.0)
75-
fastlane (2.232.0)
74+
fastimage (2.4.1)
75+
fastlane (2.232.2)
7676
CFPropertyList (>= 2.3, < 4.0.0)
7777
abbrev (~> 0.1.2)
7878
addressable (>= 2.8, < 3.0.0)
@@ -125,7 +125,7 @@ GEM
125125
fastlane-sirp (1.0.0)
126126
sysrandom (~> 1.0)
127127
gh_inspector (1.1.3)
128-
google-apis-androidpublisher_v3 (0.96.0)
128+
google-apis-androidpublisher_v3 (0.98.0)
129129
google-apis-core (>= 0.15.0, < 2.a)
130130
google-apis-core (0.18.0)
131131
addressable (~> 2.5, >= 2.5.1)
@@ -139,15 +139,15 @@ GEM
139139
google-apis-core (>= 0.15.0, < 2.a)
140140
google-apis-playcustomapp_v1 (0.17.0)
141141
google-apis-core (>= 0.15.0, < 2.a)
142-
google-apis-storage_v1 (0.60.0)
142+
google-apis-storage_v1 (0.61.0)
143143
google-apis-core (>= 0.15.0, < 2.a)
144144
google-cloud-core (1.8.0)
145145
google-cloud-env (>= 1.0, < 3.a)
146146
google-cloud-errors (~> 1.0)
147147
google-cloud-env (2.1.1)
148148
faraday (>= 1.0, < 3.a)
149-
google-cloud-errors (1.5.0)
150-
google-cloud-storage (1.58.0)
149+
google-cloud-errors (1.6.0)
150+
google-cloud-storage (1.59.0)
151151
addressable (~> 2.8)
152152
digest-crc (~> 0.4)
153153
google-apis-core (>= 0.18, < 2)
@@ -169,7 +169,7 @@ GEM
169169
httpclient (2.9.0)
170170
mutex_m
171171
jmespath (1.6.2)
172-
json (2.19.2)
172+
json (2.19.3)
173173
jwt (2.10.2)
174174
base64
175175
logger (1.7.0)
@@ -185,13 +185,13 @@ GEM
185185
os (1.1.4)
186186
ostruct (0.6.3)
187187
plist (3.7.2)
188-
public_suffix (7.0.2)
188+
public_suffix (7.0.5)
189189
rake (13.3.1)
190190
representable (3.2.0)
191191
declarative (< 0.1.0)
192192
trailblazer-option (>= 0.1.1, < 0.2.0)
193193
uber (< 0.2.0)
194-
retriable (3.1.2)
194+
retriable (3.4.1)
195195
rexml (3.4.4)
196196
rouge (3.28.0)
197197
ruby2_keywords (0.0.5)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import FeedKit
2+
import UIKit
3+
import WidgetKit
4+
internal import XMLKit
5+
6+
struct BlogFeedFetcher {
7+
static var nextUpdateDate: Date {
8+
Calendar.current.date(byAdding: .hour, value: 1, to: .now)!
9+
}
10+
11+
private func fetchThumbnail(urlString: String, spec: BlogThumbnailSpec) async -> Data? {
12+
guard let url = URL(string: urlString),
13+
let (data, _) = try? await URLSession.shared.data(from: url),
14+
let source = UIImage(data: data)
15+
else { return nil }
16+
let scale = UITraitCollection.current.displayScale
17+
let size = CGSize(width: spec.width * scale, height: spec.height * scale)
18+
return await source.byPreparingThumbnail(ofSize: size)?.jpegData(compressionQuality: 0.85)
19+
}
20+
21+
func fetchEntry(feed: BlogFeedChoice, username: String?, family: WidgetFamily) async -> BlogFeedEntry {
22+
let (items, error) = await fetchFeed(feed: feed, username: username, family: family)
23+
return BlogFeedEntry(date: .now, feed: feed, username: username, items: items, error: error)
24+
}
25+
26+
private func fetchFeed(feed: BlogFeedChoice,
27+
username: String?,
28+
family: WidgetFamily) async -> (items: [BlogFeedItem], error: String?) {
29+
guard let urlString = feed.feedURL(username: username) else {
30+
return ([], "Enter a username in widget settings")
31+
}
32+
do {
33+
guard case .atom(let atomFeed) = try await Feed(urlString: urlString) else {
34+
return ([], "Unexpected feed format")
35+
}
36+
let thumbSpec = family.thumbnailSpec
37+
let items = await withTaskGroup(of: (Int, BlogFeedItem).self) { group in
38+
for (index, entry) in (atomFeed.entries ?? []).prefix(family.maxItems).enumerated() {
39+
group.addTask {
40+
let thumbData: Data? = if let thumbSpec,
41+
let thumbURL = entry.media?.thumbnails?.first?.attributes?.url {
42+
await fetchThumbnail(urlString: thumbURL, spec: thumbSpec)
43+
} else {
44+
nil
45+
}
46+
let entryURL = entry.links?
47+
.first(where: { $0.attributes?.rel == "alternate" })?
48+
.attributes?.href
49+
?? entry.links?.first?.attributes?.href
50+
return (index, BlogFeedItem(
51+
id: entry.id ?? "\(index)",
52+
title: entry.title ?? "Untitled",
53+
url: entryURL,
54+
publishedDate: entry.published,
55+
author: entry.authors?.first?.name,
56+
thumbnailData: thumbData,
57+
thumbnailImageName: nil
58+
))
59+
}
60+
}
61+
var results: [(Int, BlogFeedItem)] = []
62+
for await result in group { results.append(result) }
63+
return results.sorted { $0.0 < $1.0 }.map(\.1)
64+
}
65+
return (items, nil)
66+
} catch {
67+
return ([], error.localizedDescription)
68+
}
69+
}
70+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import Foundation
2+
import WidgetKit
3+
4+
/// Static placeholder data shown in the widget gallery and while a widget loads.
5+
enum BlogFeedPlaceholder {
6+
static func entry(feed: BlogFeedChoice, username: String? = nil, family: WidgetFamily) -> BlogFeedEntry {
7+
BlogFeedEntry(
8+
date: .now,
9+
feed: feed,
10+
username: username,
11+
items: Array(items(for: feed).prefix(family.maxItems)),
12+
error: nil
13+
)
14+
}
15+
16+
private static func items(for feed: BlogFeedChoice) -> [BlogFeedItem] {
17+
switch feed {
18+
case .officialBlog: return officialBlogItems
19+
case .communityBlog: return communityBlogItems
20+
case .userBlog: return userBlogItems
21+
}
22+
}
23+
24+
// MARK: Official Blog
25+
26+
private static let officialBlogItems: [BlogFeedItem] = [
27+
BlogFeedItem(id: "1",
28+
title: "Lichess Mobile App Update",
29+
url: nil,
30+
publishedDate: daysAgo(1),
31+
author: nil,
32+
thumbnailData: nil,
33+
thumbnailImageName: "OfficialBlogPlaceholderImage1"),
34+
BlogFeedItem(id: "2",
35+
title: "Queens' Online Chess Festival",
36+
url: nil,
37+
publishedDate: daysAgo(31),
38+
author: nil,
39+
thumbnailData: nil,
40+
thumbnailImageName: "OfficialBlogPlaceholderImage2"),
41+
BlogFeedItem(id: "3",
42+
title: "Announcing the ChessMood 20/20 Grand Prix 2026",
43+
url: nil,
44+
publishedDate: daysAgo(35),
45+
author: nil,
46+
thumbnailData: nil,
47+
thumbnailImageName: "OfficialBlogPlaceholderImage3"),
48+
BlogFeedItem(id: "4",
49+
title: "Streamer Arenas Announcement — February to July 2026",
50+
url: nil,
51+
publishedDate: daysAgo(39),
52+
author: nil,
53+
thumbnailData: nil,
54+
thumbnailImageName: "OfficialBlogPlaceholderImage4"),
55+
]
56+
57+
// MARK: Community Blog
58+
59+
private static let communityBlogItems: [BlogFeedItem] = [
60+
BlogFeedItem(id: "1",
61+
title: "How To Analyse Your Game Like a 2000-Rated Player",
62+
url: nil,
63+
publishedDate: daysAgo(0),
64+
author: "VihaanRathodBhuj",
65+
thumbnailData: nil,
66+
thumbnailImageName: "CommunityBlogPlaceholderImage1"),
67+
BlogFeedItem(id: "2",
68+
title: "What a Figure Skater Can Teach You About Chess Improvement",
69+
url: nil,
70+
publishedDate: daysAgo(0),
71+
author: "FM MattyDPerrine",
72+
thumbnailData: nil,
73+
thumbnailImageName: "CommunityBlogPlaceholderImage2"),
74+
BlogFeedItem(id: "3", title: "Slow Growth in Chess: The Truth Most People Don't Want to Hear",
75+
url: nil, publishedDate: daysAgo(2),
76+
author: "IM nikhildixit",
77+
thumbnailData: nil,
78+
thumbnailImageName: "CommunityBlogPlaceholderImage3"),
79+
BlogFeedItem(id: "4", title: "Everyone's favorite: A modern Kortchnoi seeking to change his history",
80+
url: nil,
81+
publishedDate: daysAgo(0),
82+
author: "FM Reynold9402",
83+
thumbnailData: nil,
84+
thumbnailImageName: "CommunityBlogPlaceholderImage4"),
85+
]
86+
87+
// MARK: User Blog
88+
89+
private static let userBlogItems: [BlogFeedItem] = [
90+
BlogFeedItem(id: "1",
91+
title: "Did you know Lichess can do this? Opening Explorer and Tablebase",
92+
url: nil,
93+
publishedDate: daysAgo(2),
94+
author: nil,
95+
thumbnailData: nil,
96+
thumbnailImageName: "UserBlogPlaceholderImage1"),
97+
BlogFeedItem(id: "2",
98+
title: "Annotated: Sicilian Accelerated Dragon Deep Dive",
99+
url: nil,
100+
publishedDate: daysAgo(7),
101+
author: nil,
102+
thumbnailData: nil, thumbnailImageName: "UserBlogPlaceholderImage2"),
103+
BlogFeedItem(id: "3",
104+
title: "Tournament Report: City Open 2026",
105+
url: nil,
106+
publishedDate: daysAgo(14),
107+
author: nil,
108+
thumbnailData: nil, thumbnailImageName: "UserBlogPlaceholderImage3"),
109+
BlogFeedItem(id: "4",
110+
title: "Endgame Studies I Recommend",
111+
url: nil,
112+
publishedDate: daysAgo(21),
113+
author: nil,
114+
thumbnailData: nil,
115+
thumbnailImageName: "UserBlogPlaceholderImage4"),
116+
]
117+
118+
private static func daysAgo(_ days: Int) -> Date? {
119+
Calendar.current.date(byAdding: .day, value: -days, to: .now)
120+
}
121+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import SwiftUI
2+
import WidgetKit
3+
4+
struct CommunityBlogWidget: Widget {
5+
let kind = "CommunityBlogWidget"
6+
7+
var body: some WidgetConfiguration {
8+
StaticConfiguration(kind: kind,
9+
provider: GenericBlogFeedProvider(feed: .communityBlog)) { entry in
10+
BlogFeedWidgetEntryView(entry: entry)
11+
.containerBackground(.background, for: .widget)
12+
}
13+
.configurationDisplayName("Community Blog")
14+
.description("Latest posts from the Lichess community blog.")
15+
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
16+
}
17+
}

0 commit comments

Comments
 (0)