Skip to content

Commit e3e85f8

Browse files
committed
Merge remote-tracking branch 'origin/main' into bbc-main
2 parents 6a4693c + 73d1391 commit e3e85f8

8 files changed

Lines changed: 332 additions & 7 deletions

File tree

meteor/eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const tmpRules = {
1414

1515
const extendedRules = await generateEslintConfig({
1616
// tsconfigName: 'tsconfig.eslint.json',
17-
ignores: ['.meteor', 'public', 'scripts', 'server/_force_restart.js', '/packages/'],
17+
ignores: ['.meteor', 'public', 'scripts', 'server/_force_restart.js', '/packages/', '_build'],
1818

1919
// disableNodeRules: true,
2020
})

packages/documentation/docs/user-guide/installation/quick-install.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ services:
4848

4949
core:
5050
hostname: core
51-
image: sofietv/tv-automation-server-core:release52
51+
image: sofietv/tv-automation-server-core:v26.3.0-1
5252
restart: always
5353
ports:
5454
- '3000:3000' # Same port as meteor uses by default
@@ -69,7 +69,7 @@ services:
6969
condition: service_healthy
7070

7171
playout-gateway:
72-
image: sofietv/tv-automation-playout-gateway:release52
72+
image: sofietv/tv-automation-playout-gateway:v26.3.0-1
7373
restart: always
7474
environment:
7575
DEVICE_ID: playoutGateway0
@@ -96,7 +96,7 @@ services:
9696
# - core
9797

9898
# mos-gateway:
99-
# image: sofietv/tv-automation-mos-gateway:release52
99+
# image: sofietv/tv-automation-mos-gateway:v26.3.0-1
100100
# restart: always
101101
# ports:
102102
# - "10540:10540" # MOS Lower port

packages/documentation/releases/releases.mdx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,30 @@ hide_table_of_contents: true
44
slug: /
55
---
66

7+
import LatestVersionNumber from '../src/components/LatestVersionNumber.jsx'
8+
79
<!-- Warning: This file is intentionally not in the docs folder as it makes no sense to be versioned. If it does get versioned, then the import below breaks -->
810

9-
import GitHubReleases from '../src/components/GitHubReleases'
11+
Sofie project is working in a quarterly release cycle. The releases are done at the end of every March, June, September,
12+
and beginning of December. The main Sofie components do not follow semantic versioning and instead use a `year.month`
13+
system. For example, the June release of year 2026 will be versioned as `26.06.0`. Fixes for bugs found after a release
14+
is done are published as patch releases, with the patch number increased for every bugfix.
15+
16+
:::info
17+
<div style={{fontSize: "200%"}}>
18+
The latest version is: **<LatestVersionNumber />**
19+
</div>
20+
21+
Use our **[Quick install](/docs/user-guide/installation/quick-install)** guide to install Sofie using **Docker**.
22+
23+
Find Sofie release images on [DockerHub](https://hub.docker.com/u/sofietv) and [GitHub Container Registry](https://github.com/orgs/Sofie-Automation/packages).
24+
:::
25+
26+
When a version is released, the `HEAD` of the Git repositories is branched off into a new branch called `releases/YY.mm`
27+
and a Git tag is created for it.
1028

11-
Current, future, and past releases of _Sofie_ are all tracked on [**our GitHub repository**](https://github.com/Sofie-Automation/Sofie-TV-automation/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3ARelease).
29+
## Historic versioning
1230

13-
<GitHubReleases org="Sofie-Automation" repo="Sofie-TV-Automation" releaseLabel="Release" state="all" />
31+
The new versioning system was implemented in March 2026 by the Sofie Technical Steering Committee. All earlier versions follow a different versioning system,
32+
where `1.xx.xx` is used, with the major field fixed at `1`, the minor field indicating a development cycle of
33+
variable length, and the patch field used for bugfix releases, starting at `0`.

packages/documentation/src/components/HomepageFeatures.js renamed to packages/documentation/src/components/HomepageFeatures.jsx

File renamed without changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
.homepage-pr-gallery {
2+
margin-top: 2em;
3+
margin-bottom: 2em;
4+
}
5+
6+
.homepage-prs-loading {
7+
display: grid;
8+
align-items: center;
9+
justify-content: center;
10+
min-height: 15em;
11+
text-align: center;
12+
font-size: 1.5em;
13+
color: var(--ifm-color-gray-400);
14+
}
15+
16+
.homepage-prs-error {
17+
display: grid;
18+
align-items: center;
19+
justify-content: center;
20+
min-height: 15em;
21+
text-align: center;
22+
font-size: 1.5em;
23+
color: var(--ifm-color-danger);
24+
}
25+
26+
.homepage-prs-title {
27+
text-align: center;
28+
font-size: 4em;
29+
margin-top: 1em;
30+
margin-bottom: 1em;
31+
background-clip: text;
32+
color: transparent;
33+
background-image: linear-gradient(to bottom, var(--ifm-color-primary), var(--ifm-color-primary-darkest));
34+
}
35+
36+
.homepage-pr-gallery-row {
37+
margin-bottom: 1em;
38+
overflow-x: hidden;
39+
}
40+
41+
.homepage-pr-gallery-row-inner {
42+
display: flex;
43+
flex-direction: row;
44+
gap: 1em;
45+
opacity: 1;
46+
}
47+
48+
.homepage-pr-gallery-pull {
49+
display: flex;
50+
flex-direction: column;
51+
border: var(--ifm-global-border-width) solid var(--ifm-color-emphasis-300);
52+
border-radius: var(--ifm-global-radius);
53+
gap: 0.5em;
54+
flex: 0 0 400px;
55+
padding: 0.5em;
56+
}
57+
58+
.homepage-pr-gallery-pull-header {
59+
display: flex;
60+
align-items: flex-start;
61+
gap: 0.5em;
62+
max-width: 100%;
63+
min-width: 0;
64+
}
65+
66+
.homepage-pr-gallery-pull-number {
67+
color: var(--ifm-color-gray-600);
68+
}
69+
70+
.homepage-pr-gallery-pull-title {
71+
display: inline-block;
72+
white-space: nowrap;
73+
overflow: hidden;
74+
text-overflow: ellipsis;
75+
max-width: 40ch;
76+
}
77+
78+
.homepage-pr-gallery-pull-title a {
79+
color: inherit;
80+
text-decoration: none;
81+
}
82+
83+
.homepage-pr-gallery-pull-title a:hover {
84+
color: inherit;
85+
text-decoration: underline;
86+
}
87+
88+
.homepage-pr-gallery-pull-sponsor {
89+
align-self: flex-start;
90+
background-color: var(--label-color, var(--ifm-color-primary));
91+
border-radius: 1em;
92+
padding: 0 0.5em;
93+
color: contrast-color(var(--label-color, var(--ifm-color-primary)));
94+
}
95+
96+
.homepage-pr-gallery-pull-author {
97+
display: flex;
98+
flex-direction: row;
99+
gap: 0.25em;
100+
}
101+
102+
.homepage-pr-gallery-pull-author-avatar {
103+
width: 1.5em;
104+
height: 1.5em;
105+
border-radius: 50%;
106+
background-image: var(--user-avatar);
107+
background-size: cover;
108+
overflow: hidden;
109+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React, { useEffect, useLayoutEffect, useState, useRef } from "react";
2+
import './HomepagePRs.css';
3+
4+
const GITHUB_API_URL = 'https://api.github.com'
5+
6+
const GITHUB_ORGANIZATION = "Sofie-Automation"
7+
const GITHUB_REPO = "sofie-core"
8+
9+
export function HomepagePRs() {
10+
const [isReady, setIsReady] = useState("error")
11+
const [pulls, setPulls] = useState([])
12+
13+
useEffect(() => {
14+
let mounted = true
15+
fetch(`${GITHUB_API_URL}/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/pulls?state=all&sort=updated&direction=desc&per_page=100`, {
16+
headers: [['Accept', 'application/vnd.github.text+json']],
17+
})
18+
.then((value) => {
19+
if (value.ok) {
20+
return value.json()
21+
} else {
22+
throw new Error(value.status)
23+
}
24+
})
25+
.then((data) => {
26+
if (!mounted) return
27+
setPulls(data.filter((pull) => !pull.draft && (pull.state === 'closed' && pull.merged_at || pull.state === 'open')))
28+
setIsReady("done")
29+
})
30+
.catch((error) => {
31+
if (!mounted) return
32+
console.error(error)
33+
setIsReady("error")
34+
})
35+
36+
return () => {
37+
mounted = false
38+
}
39+
}, [])
40+
41+
return (
42+
<React.Fragment>
43+
<div className="container">
44+
<h2 className="homepage-prs-title">What's the latest in Sofie?</h2>
45+
</div>
46+
{isReady === "error" && <div className="homepage-prs-error">Failed to load recent pull requests from GitHub.</div>}
47+
{isReady === "loading" && <div className="homepage-prs-loading">Loading recent pull requests...</div>}
48+
{isReady === "done" && <div className="homepage-pr-gallery">
49+
{chunkArray(pulls, 3).map((chunk, chunkIndex) => (
50+
<AnimatedRow pulls={chunk} key={chunkIndex} speed={0.025 * (chunkIndex + 1)} />
51+
))}
52+
</div>}
53+
</React.Fragment>
54+
)
55+
}
56+
57+
function AnimatedRow(props) {
58+
const { pulls, speed } = props
59+
const ref = useRef()
60+
const pausedRef = useRef(false)
61+
62+
const [elementWidth, setElementWidth] = useState(0)
63+
const [containerWidth, setContainerWidth] = useState(0)
64+
65+
useLayoutEffect(() => {
66+
const element = ref.current
67+
if (!element) return
68+
69+
console.log(elementWidth, containerWidth)
70+
71+
let animationFrameId
72+
let lastTimestamp = null
73+
74+
let scrollAmount = 0
75+
76+
function animate(timestamp) {
77+
if (elementWidth <= containerWidth) return // No need to scroll if content fits
78+
if (elementWidth - scrollAmount < containerWidth) {
79+
scrollAmount = 0 // Reset scroll to start when we've scrolled through all content
80+
}
81+
if (lastTimestamp !== null && !pausedRef.current) {
82+
const delta = timestamp - lastTimestamp
83+
scrollAmount += delta * (speed || 0.05) // Adjust the speed here (0.05 is the default speed factor)
84+
element.style.transform = `translateX(-${scrollAmount}px)`;
85+
}
86+
lastTimestamp = timestamp
87+
animationFrameId = requestAnimationFrame(animate)
88+
}
89+
90+
animationFrameId = requestAnimationFrame(animate)
91+
92+
return () => {
93+
cancelAnimationFrame(animationFrameId)
94+
}
95+
}, [speed, elementWidth, containerWidth])
96+
97+
useLayoutEffect(() => {
98+
const element = ref.current
99+
if (!element) return
100+
101+
const updateWidth = () => {
102+
setElementWidth(element.scrollWidth)
103+
}
104+
const updateContainerWidth = () => {
105+
setContainerWidth(element.clientWidth)
106+
}
107+
108+
updateWidth()
109+
updateContainerWidth()
110+
111+
window.addEventListener('resize', updateWidth)
112+
window.addEventListener('resize', updateContainerWidth)
113+
return () => {
114+
window.removeEventListener('resize', updateWidth)
115+
window.removeEventListener('resize', updateContainerWidth)
116+
}
117+
}, [pulls])
118+
119+
function handleMouseEnter() {
120+
pausedRef.current = true
121+
}
122+
123+
function handleMouseLeave() {
124+
pausedRef.current = false
125+
}
126+
127+
return (
128+
<div className="homepage-pr-gallery-row" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
129+
<div className="homepage-pr-gallery-row-inner" ref={ref}>
130+
{pulls.map((pull) => {
131+
return (
132+
<div className="homepage-pr-gallery-pull" key={String(pull.id)}>
133+
<div className="homepage-pr-gallery-pull-header">
134+
<div className="homepage-pr-gallery-pull-number">#{pull['number']}</div>
135+
<div className="homepage-pr-gallery-pull-title">
136+
<a href={pull.html_url} target="_blank" rel="noopener noreferrer">{pull.title}</a>
137+
</div>
138+
</div>
139+
<SponsorBadge sponsorLabel={pull.labels.find((label) => label.name.match(/contr(\w+) (from|by)/i))} />
140+
<div className="homepage-pr-gallery-pull-author">
141+
<div className="homepage-pr-gallery-pull-author-avatar" style={{ "--user-avatar": `url("${pull.user.avatar_url}")` }}></div>
142+
<div className="homepage-pr-gallery-pull-author-name">{pull.user.login}</div>
143+
</div>
144+
</div>
145+
)
146+
})}
147+
</div>
148+
</div>
149+
)
150+
}
151+
152+
function chunkArray(arr, chunkCount) {
153+
const arrLength = arr.length;
154+
const chunkSize = Math.floor(arrLength / chunkCount);
155+
156+
const result = [];
157+
for (let i = 0; i < chunkCount - 1; i++) {
158+
result.push(arr.slice(i * chunkSize, (i + 1) * chunkSize))
159+
}
160+
161+
// last chunk contains the remainer of stuff
162+
result.push(arr.slice((chunkCount - 1) * chunkSize))
163+
return result
164+
}
165+
166+
function SponsorBadge(props) {
167+
if (!props.sponsorLabel) return null
168+
169+
const { sponsorLabel: label } = props
170+
171+
return (
172+
<div className="homepage-pr-gallery-pull-sponsor" style={{ '--label-color': `#${label.color}` }}>{label.name}</div>
173+
)
174+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React, { useState, useEffect } from 'react'
2+
3+
import versions from '../../versions.json';
4+
5+
const FALLBACK_VERSION = "26.03.0"; // Fallback version if versions.json cannot be loaded
6+
7+
export default function LatestVersionNumber() {
8+
try {
9+
// The first item in versions.json is always the latest archived version
10+
const latestVersion = versions[0];
11+
12+
if (!latestVersion) {
13+
return <span>{FALLBACK_VERSION}</span>; // Fallback if versions.json is empty
14+
}
15+
16+
return <span>{latestVersion}</span>;
17+
} catch (error) {
18+
return <span>{FALLBACK_VERSION}</span>;
19+
}
20+
}

packages/documentation/src/pages/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link from '@docusaurus/Link'
55
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
66
import styles from './index.module.css'
77
import HomepageFeatures from '../components/HomepageFeatures'
8+
import { HomepagePRs } from '../components/HomepagePRs'
89

910
function HomepageHeader() {
1011
const { siteConfig } = useDocusaurusContext()
@@ -33,6 +34,7 @@ export default function Home() {
3334
<HomepageHeader />
3435
<main>
3536
<HomepageFeatures />
37+
<HomepagePRs />
3638
</main>
3739
</Layout>
3840
)

0 commit comments

Comments
 (0)