Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,53 @@ jobs:
working-directory: desktop
- run: npx tauri build --bundles ${{ matrix.bundle }}
working-directory: desktop
- name: Package portable Windows app
if: runner.os == 'Windows'
shell: pwsh
run: |
$release = (Resolve-Path 'desktop/target/release').Path
$portable = "$release/bundle/portable/Efir-Windows-x64"
New-Item -ItemType Directory -Force -Path $portable | Out-Null
Copy-Item "$release/efir.exe" $portable
Get-ChildItem $release -Filter '*.dll' | Copy-Item -Destination $portable
Copy-Item 'desktop/resources/web' "$portable/web" -Recurse
Copy-Item 'desktop/resources/speech' "$portable/speech" -Recurse
Copy-Item 'LICENSE' "$portable/LICENSE.txt"
$version = (Get-Content 'desktop/package.json' | ConvertFrom-Json).version
@'
Extract this whole folder, then run efir.exe. Keep web/ and speech/ beside it.
Windows 10/11 x64 and Microsoft WebView2 Runtime are required.
Vosk language models are selected separately in voice settings.
'@ | Set-Content "$portable/START.txt" -Encoding utf8
$stdout = "$release/portable-stdout.log"
$stderr = "$release/portable-stderr.log"
$app = Start-Process "$portable/efir.exe" -WorkingDirectory $portable -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
try {
$ready = $false
$diagnostic = 'No HTTP response yet'
for ($attempt = 0; $attempt -lt 30; $attempt++) {
if ($app.HasExited) { throw "Portable app exited: $($app.ExitCode)" }
try {
$info = Invoke-RestMethod 'http://127.0.0.1:8765/efir-local.json' -NoProxy -TimeoutSec 1
$diagnostic = $info | ConvertTo-Json -Compress
if ($info.local -eq $true -and $info.platform -eq 'windows' -and $info.version -eq $version) { $ready = $true; break }
} catch { $diagnostic = $_.Exception.Message }
Start-Sleep -Milliseconds 500
}
if (-not $ready) { throw "Portable app did not start its local server: $diagnostic" }
} finally {
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object OwningProcess -eq $app.Id | Format-Table LocalAddress, LocalPort, OwningProcess
Get-Content $stdout, $stderr -ErrorAction SilentlyContinue
if (-not $app.HasExited) { Stop-Process -Id $app.Id -Force }
}
Compress-Archive -Path $portable -DestinationPath "$release/bundle/portable/Efir-Windows-x64.zip"
- uses: actions/upload-artifact@v4
with:
name: Efir-${{ matrix.runtime }}
path: |
desktop/target/release/bundle/nsis/*.exe
desktop/target/release/bundle/deb/*.deb
desktop/target/release/bundle/portable/*.zip
if-no-files-found: error
android:
runs-on: ubuntu-24.04
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ android {
applicationId = "app.efir.android"
minSdk = 26
targetSdk = 35
versionCode = 6
versionName = "1.3.2"
versionCode = 7
versionName = "1.4.0"
testInstrumentationRunner = "app.efir.android.EfirTestRunner"
}
sourceSets["main"].assets.srcDir(layout.buildDirectory.dir("generated/assets"))
Expand Down
2 changes: 1 addition & 1 deletion desktop/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "efir-desktop"
version = "1.3.2"
version = "1.4.0"
edition = "2021"
license = "MIT"
description = "Free teleprompter with remote control over local Wi-Fi"
Expand Down
4 changes: 2 additions & 2 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "efir-desktop",
"version": "1.3.2",
"version": "1.4.0",
"private": true,
"scripts": {
"dev": "tauri dev",
Expand Down
2 changes: 1 addition & 1 deletion desktop/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Efir",
"version": "1.3.2",
"version": "1.4.0",
"identifier": "app.efir.desktop",
"build": {
"frontendDist": "bootstrap"
Expand Down
7 changes: 6 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,15 @@ export JAVA_HOME=/path/to/jdk17
export ANDROID_HOME=/path/to/android-sdk
python3 script/build_android.py --debug --test
python3 script/build_android.py --local-sign --output ./work/releases/Efir-Android.apk
# Reuse a key stored outside the checkout:
python3 script/build_android.py --local-sign --signing-dir /path/to/private/android \
--output ./work/releases/Efir-Android.apk
```

`--local-sign` creates or reuses a key under `.private/android/`. Back up that
folder: updates must keep the same signing key. Passwords are passed through
folder: updates must keep the same signing key. `--signing-dir` selects a persistent
private directory outside disposable checkouts. Keep both `efir-release.jks` and
`signing.json` together. Passwords are passed through
environment variables, not command-line arguments. For an existing key, set
`EFIR_KEYSTORE`, `EFIR_STORE_PASSWORD`, `EFIR_KEY_ALIAS`, `EFIR_KEY_PASSWORD` and
omit `--local-sign`. Without a key, release output is explicitly named unsigned.
Expand Down
59 changes: 59 additions & 0 deletions docs/releases/1.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Efir 1.4.0

## What's new

- Map keyboard, media-key and pedal remotes to prompting actions. Learn buttons
from browser Gamepad devices, save mappings and use held forward/backward
nudges. Editing text and using dialogs do not trigger prompting shortcuts.
- Connect several readers to one native server. Each screen keeps its own name,
font size, margins, top/bottom padding, marker position and mirror settings.
Word-based synchronization follows the same passage across different wraps.
- Select a screen preview or see the readers together in a grid. Previews use
each screen's real geometry. Format guides shade the extra area when readers
have different aspect ratios, including 16:10 and 16:9.
- Reconnect through saved screen links, select the timing leader, and keep a
useful frozen preview when a reader disconnects.
- Support the project through the localized GitHub star button on the home page.

## Resource improvements

- Stream native desktop HTTP assets with backpressure instead of loading a
complete response into memory for every download.
- Reuse reader geometry, avoid repeated screen-list DOM writes and omit duplicate
display metadata from frequent messages while keeping the existing update cadence.
- Stop Gamepad polling when the controller is hidden or unfocused. Stop the home
page demo when hidden, outside the viewport or using reduced motion.
- Release cancelled Android WebSocket tasks, cache native desktop translations,
and avoid duplicate macOS speech status messages.

See the [shared web audit](../performance-audit.md) and
[native resource audit](../native-resource-audit.md) for measured scenarios and
remaining device checks. These measurements do not establish battery-life gains.

## Downloads and installation

| Platform | File | Requirements |
| --- | --- | --- |
| macOS | `Efir-macOS.zip` | Apple Silicon or Intel, macOS 14+ |
| Windows | `Efir-Windows-x64.zip` | Windows 10/11 x64, Microsoft WebView2 Runtime |
| Linux x64 | `Efir-Linux-x64.deb` | Ubuntu 24.04+ or compatible dependencies |
| Linux ARM64 | `Efir-Linux-arm64.deb` | Ubuntu 24.04+ or compatible dependencies |
| Android | `Efir-Android.apk` | Android 8+ |

Extract the entire Windows folder before launching `efir.exe`; keep `web/`,
`speech/` and any accompanying DLLs together. Offline Vosk language models are
selected separately in voice settings. An NSIS installer is also provided.

The macOS app is ad hoc signed and not notarized. Follow the documented
[macOS opening steps](../macos-distribution.md). Windows packages do not have an
Authenticode signature.

Android 1.4.0 starts a new release signing certificate because the original
private key is unavailable. If 1.3.2 is installed, export scripts and settings,
uninstall it, then install 1.4.0 and restore the export. Future APK updates will
reuse the new key, which is kept outside the source checkout.

`Efir-1.4.0-SHA256.txt` lists package checksums. Keyboard HID and browser Gamepad
support depends on the signals the remote exposes; proprietary BLE, MIDI and
vendor-app protocols are not automatically supported. Physical remote models
still need individual checks.
4 changes: 2 additions & 2 deletions mac/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.3.2</string>
<string>1.4.0</string>
<key>CFBundleVersion</key>
<string>6</string>
<string>7</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
Expand Down
4 changes: 2 additions & 2 deletions mac/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ targets:
path: Info.plist
properties:
CFBundleDisplayName: Эфир
CFBundleShortVersionString: '1.3.2'
CFBundleVersion: '6'
CFBundleShortVersionString: '1.4.0'
CFBundleVersion: '7'
CFBundleDevelopmentRegion: ru
CFBundleLocalizations: ['ru', 'en', 'es', 'zh-Hans', 'hi', 'ar']
NSLocalNetworkUsageDescription: Эфир раздаёт суфлёр устройствам в вашей локальной сети.
Expand Down
11 changes: 7 additions & 4 deletions script/build_android.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
ROOT = Path(__file__).resolve().parent.parent


def local_signing(env):
private = ROOT / '.private/android'
def local_signing(env, directory=None):
private = directory.expanduser().resolve() if directory else ROOT / '.private/android'
private.mkdir(parents=True, exist_ok=True)
private.chmod(0o700)
credentials = private / 'signing.json'
key = private / 'efir-release.jks'
if credentials.exists():
config = json.loads(credentials.read_text())
if not key.exists():
raise SystemExit('Signing key is missing; restore .private/android from your backup.')
raise SystemExit(f'Signing key is missing; restore {private} from your backup.')
else:
if key.exists():
raise SystemExit('Signing password is missing; restore signing.json from your backup.')
Expand All @@ -48,11 +48,14 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--debug', action='store_true')
parser.add_argument('--local-sign', action='store_true')
parser.add_argument('--signing-dir', type=Path, help='Persistent private key directory (requires --local-sign)')
parser.add_argument('--test', action='store_true', help='Also build the instrumentation APK')
parser.add_argument('--output', type=Path)
args = parser.parse_args()
if args.test and not args.debug:
parser.error('--test requires --debug')
if args.signing_dir and not args.local_sign:
parser.error('--signing-dir requires --local-sign')

env = dict(os.environ)
tools = ROOT / 'work/android-tools'
Expand All @@ -63,7 +66,7 @@ def main():
if not env.get('ANDROID_HOME') and (tools / 'sdk').exists():
env['ANDROID_HOME'] = str(tools / 'sdk')
if args.local_sign:
local_signing(env)
local_signing(env, args.signing_dir)

kind = 'debug' if args.debug else 'release'
tasks = ['assembleDebug', 'lintDebug'] if args.debug else ['assembleRelease', 'lintRelease']
Expand Down
5 changes: 3 additions & 2 deletions script/build_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from string import Template

ROOT = Path(__file__).resolve().parent.parent
VERSION = json.loads((ROOT / "desktop/package.json").read_text())["version"]
OUT = ROOT / "work" / "site"
BASE = "https://fedyalight.github.io/efir"
REPO = "https://github.com/FedyaLight/efir"
Expand Down Expand Up @@ -49,7 +50,7 @@ def build():
canonical = f"{BASE}/{page_path(language)}"
data = {key: html.escape(value, quote=True) for key, value in text.items() if isinstance(value, str)}
data.update(lang=locale, direction="rtl" if language == "ar" else "ltr", prefix=prefix,
canonical=canonical, base=BASE, repo=REPO)
canonical=canonical, base=BASE, repo=REPO, version=VERSION)
data["alternates"] = "\n ".join(
f'<link rel="alternate" hreflang="{tag}" href="{BASE}/{page_path(code)}">'
for code, tag in LOCALES.items()
Expand Down Expand Up @@ -78,7 +79,7 @@ def build():
"@context": "https://schema.org", "@type": "SoftwareApplication", "name": "Efir",
"alternateName": "Эфир", "url": canonical, "description": text["description"],
"applicationCategory": "MultimediaApplication", "operatingSystem": "macOS, Windows, Linux, Android, Web",
"softwareVersion": "1.3.2", "inLanguage": list(LOCALES.values()),
"softwareVersion": VERSION, "inLanguage": list(LOCALES.values()),
"license": f"{REPO}/blob/main/LICENSE", "sameAs": REPO,
"downloadUrl": f"{REPO}/releases/latest", "image": f"{BASE}/assets/social.png",
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"},
Expand Down
4 changes: 2 additions & 2 deletions site/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ <h1>$heading</h1>
</section>
<section aria-labelledby="features"><h2 id="features">$features_title</h2><div class="features">$features</div></section>
<section aria-labelledby="start"><h2 id="start">$start_title</h2><ol class="steps">$steps</ol><p class="note">$android</p></section>
<section aria-labelledby="downloads"><h2 id="downloads">$download</h2><div class="downloads">$downloads</div><p class="note">$requirements</p><aside>$mac_warning <a href="$repo/blob/main/docs/macos-distribution.md#opening-the-current-build">$instructions <span aria-hidden="true">↗</span></a></aside><p><a href="$repo/releases/latest">$release_notes</a> · <a href="$repo/releases/latest/download/Efir-1.3.2-SHA256.txt">SHA-256</a></p></section>
<section aria-labelledby="downloads"><h2 id="downloads">$download</h2><div class="downloads">$downloads</div><p class="note">$requirements</p><aside>$mac_warning <a href="$repo/blob/main/docs/macos-distribution.md#opening-the-current-build">$instructions <span aria-hidden="true">↗</span></a></aside><p><a href="$repo/releases/latest">$release_notes</a> · <a href="$repo/releases/latest/download/Efir-${version}-SHA256.txt">SHA-256</a></p></section>
<section aria-labelledby="questions"><h2 id="questions">$faq_title</h2><dl>$faq</dl></section>
</main>
<footer><a href="$repo">GitHub</a><a href="$repo/blob/main/docs/usage.md">$guide</a><a href="$repo/blob/main/LICENSE">MIT</a><a href="$prefix/sitemap.xml">Sitemap</a><span>Efir 1.3.2 · © 2026 FedyaLight</span></footer>
<footer><a href="$repo">GitHub</a><a href="$repo/blob/main/docs/usage.md">$guide</a><a href="$repo/blob/main/LICENSE">MIT</a><a href="$prefix/sitemap.xml">Sitemap</a><span>Efir ${version} · © 2026 FedyaLight</span></footer>
</body>
</html>
2 changes: 1 addition & 1 deletion tests/RustServerHarness/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading