diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8ae93e..183864d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1d40b23..2b973dd 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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")) diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 8b1d2e3..638510c 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -1089,7 +1089,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "efir-desktop" -version = "1.3.2" +version = "1.4.0" dependencies = [ "axum", "cpal", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index acbb799..24627e8 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -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" diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 4e7a4c8..e064a94 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "efir-desktop", - "version": "1.3.2", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "efir-desktop", - "version": "1.3.2", + "version": "1.4.0", "devDependencies": { "@tauri-apps/cli": "2.12.0" } diff --git a/desktop/package.json b/desktop/package.json index 21b7cbd..d8ce184 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "efir-desktop", - "version": "1.3.2", + "version": "1.4.0", "private": true, "scripts": { "dev": "tauri dev", diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index cff320e..bf12c28 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -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" diff --git a/docs/development.md b/docs/development.md index c1bc6d4..c9e3edd 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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. diff --git a/docs/releases/1.4.0.md b/docs/releases/1.4.0.md new file mode 100644 index 0000000..dd2a65b --- /dev/null +++ b/docs/releases/1.4.0.md @@ -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. diff --git a/mac/Info.plist b/mac/Info.plist index 02c26dd..b05e424 100644 --- a/mac/Info.plist +++ b/mac/Info.plist @@ -26,9 +26,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.2 + 1.4.0 CFBundleVersion - 6 + 7 NSAppTransportSecurity NSAllowsArbitraryLoadsInWebContent diff --git a/mac/project.yml b/mac/project.yml index a68d207..7aac062 100644 --- a/mac/project.yml +++ b/mac/project.yml @@ -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: Эфир раздаёт суфлёр устройствам в вашей локальной сети. diff --git a/script/build_android.py b/script/build_android.py index ed824fc..95034a4 100644 --- a/script/build_android.py +++ b/script/build_android.py @@ -12,8 +12,8 @@ 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' @@ -21,7 +21,7 @@ def local_signing(env): 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.') @@ -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' @@ -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'] diff --git a/script/build_site.py b/script/build_site.py index 5c79648..e1a29d1 100644 --- a/script/build_site.py +++ b/script/build_site.py @@ -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" @@ -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'' for code, tag in LOCALES.items() @@ -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"}, diff --git a/site/template.html b/site/template.html index 60abb55..7414097 100644 --- a/site/template.html +++ b/site/template.html @@ -38,9 +38,9 @@

$heading

$features_title

$features

$start_title

    $steps

$android

-

$download

$downloads

$requirements

$release_notes · SHA-256

+

$download

$downloads

$requirements

$release_notes · SHA-256

$faq_title

$faq
- + diff --git a/tests/RustServerHarness/Cargo.lock b/tests/RustServerHarness/Cargo.lock index 37f6d25..89a49ed 100644 --- a/tests/RustServerHarness/Cargo.lock +++ b/tests/RustServerHarness/Cargo.lock @@ -177,7 +177,7 @@ dependencies = [ [[package]] name = "efir-desktop" -version = "1.3.2" +version = "1.4.0" dependencies = [ "axum", "futures-util",